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 4967a6899..29d54843e 100644 --- a/fluxer_docs/src/content/docs/admin-api/api-keys.mdx +++ b/fluxer_docs/src/content/docs/admin-api/api-keys.mdx @@ -126,7 +126,7 @@ A key past its expiry is never returned. -Creates a key and returns an [Admin API key creation](#admin-api-key-creation-object) object that has the raw credential, with HTTP 200 rather than 201. Requires `admin_api_key:manage`. +Creates a key and returns an [Admin API key creation](#admin-api-key-creation-object) object that has the raw credential, with HTTP 200, not 201. Requires `admin_api_key:manage`. The acting credential must already have every value in `acls`, unless it has `*`. The key authenticates immediately, and its effective permission is the intersection described under [Admin API key object](#admin-api-key-object). @@ -142,7 +142,7 @@ The acting credential must already have every value in `acls`, unless it has `*` 2 The stored expiry is the request instant plus this many days. Omitting the field creates a key that does not expire -3 An empty array produces a key that satisfies no operation. Fluxer compares `acls` against the presenting key's own ACLs, so a key cannot mint a broader key +3 An empty array produces a key that satisfies no operation. Fluxer compares `acls` against the presenting key's own ACLs, so a key cannot issue a broader key Fluxer rejects the request with 403 `MISSING_ACL` on the first ungrantable value. A request that names several ungrantable ACLs reports only that one. @@ -221,7 +221,7 @@ An update never rotates the credential, and no field on this route changes the e | 4031 | [error response](/admin-api/#error-response) | Credential type is refused, `admin_api_key:manage` is absent, or `acls` names a value the acting credential does not have | | 404 | [error response](/admin-api/#error-response) | `ADMIN_API_KEY_NOT_FOUND`, which also covers an expired key and a key created by another account | -1 The ownership check runs before the ACL grant check, so a key belonging to another account returns 404 rather than 403 +1 The ownership check runs before the ACL grant check, so a key belonging to another account returns 404, not 403 ### Rate limit diff --git a/fluxer_docs/src/content/docs/admin-api/applications.mdx b/fluxer_docs/src/content/docs/admin-api/applications.mdx index ec68ed35c..6502f6593 100644 --- a/fluxer_docs/src/content/docs/admin-api/applications.mdx +++ b/fluxer_docs/src/content/docs/admin-api/applications.mdx @@ -8,10 +8,10 @@ import RouteHeader from '@/components/RouteHeader.astro'; An application is an OAuth2 client, and it can own one bot account. These routes report its ownership, bot state, redirect configuration, and non-secret credential metadata. [Transfer application ownership](#transfer-application-ownership) is the only one that writes. -The records are the same ones the public [Applications](/http-api/applications/) resource serves. Creation, deletion, renaming, bot provisioning, redirect URI editing, and credential rotation stay there. +These records are the same ones the public [Applications](/http-api/applications/) resource serves. Creation, deletion, renaming, bot creation, redirect URI editing, and credential rotation stay there. :::note[Admin operations report credential metadata only] -The Admin application object reports whether a client secret or bot token hash is stored, when it was provisioned, and the non-secret bot token preview. +The Admin application object reports whether a client secret or bot token hash is stored, when it was created, and the non-secret bot token preview. ::: ## Admin application object @@ -38,8 +38,8 @@ The Admin view of one application. An application and its bot account share one | has_client_secret | boolean | Whether a client secret hash is stored | | has_bot_token | boolean | Whether a bot token hash is stored | | bot_token_preview | ?string | The non-secret trailing preview of the bot token, or null when none is stored | -| bot_token_created_at | ?ISO8601 timestamp | When the bot token was provisioned, or null when none is stored | -| client_secret_created_at4 | ?ISO8601 timestamp | When the client secret was provisioned | +| bot_token_created_at | ?ISO8601 timestamp | When the bot token was created, or null when none is stored | +| client_secret_created_at4 | ?ISO8601 timestamp | When the client secret was created | | version5 | integer | The optimistic locking version of the stored record (0-2147483647) | 1 Null when the owner account can no longer be read @@ -83,8 +83,7 @@ The Admin view of one application. An application and its bot account share one Fluxer serves one synthetic application for its own Admin OAuth2 client. The application has no bot, so every `bot_` field is null or false and `has_bot_token` is false. It also reports these values: - `id` is the fixed constant `1234567890123456789`. -- `name` is `Fluxer Admin`. -- `owner_user_id` is the system account `0`. +- `name` is `Fluxer Admin`, and `owner_user_id` is the system account `0`. - `oauth2_redirect_uris` has one entry, the configured Admin endpoint followed by `/oauth2_callback`. - `has_client_secret` is true on every response that has it, and `client_secret_created_at` is null. - `version` is always 1. diff --git a/fluxer_docs/src/content/docs/admin-api/archives.mdx b/fluxer_docs/src/content/docs/admin-api/archives.mdx index b6096ae86..c9c43d71e 100644 --- a/fluxer_docs/src/content/docs/admin-api/archives.mdx +++ b/fluxer_docs/src/content/docs/admin-api/archives.mdx @@ -173,7 +173,7 @@ Returns one [archive](#archive-object) object. Requires an ACL covering the subj | Status | Body | Condition | | --- | --- | --- | -| 200 | response body | A lookup was performed, whether or not it resolved | +| 200 | response body | A lookup was done, whether or not it resolved | | 403 | [error response](/admin-api/#error-response) | `MISSING_ACL` without an ACL covering the subject type | ### Side effects @@ -191,7 +191,7 @@ The read issues no download grant. Issues a temporary download URL for a completed archive. Returns an [archive download](#archive-download-object) object on success. Requires an ACL covering the subject type. :::caution[The grant is an unauthenticated bearer URL] -The URL has its own signature and works for seven days for anyone who holds it. No operation revokes it, and it is bound to no Admin account. Each repeat of the operation issues a further independent grant. +The URL has its own signature and works for seven days for anyone who holds it. It is bound to no Admin account, and no operation revokes it. Each repeat of the operation issues a further independent grant. ::: ### Path parameters @@ -213,7 +213,7 @@ The URL has its own signature and works for seven days for anyone who holds it. 1 `HARVEST_FAILED` while `failed_at` is set, `HARVEST_NOT_READY` while the archive has no completion time or no stored object, and `HARVEST_EXPIRED` once `expires_at` has passed, tested in that order -2 The archive record stops being readable at `expires_at`, so an archive past its expiry answers `UNKNOWN_HARVEST` rather than `HARVEST_EXPIRED` +2 The archive record stops being readable at `expires_at`, so an archive past its expiry answers `UNKNOWN_HARVEST`, not `HARVEST_EXPIRED` ### Side effects diff --git a/fluxer_docs/src/content/docs/admin-api/blocklists.mdx b/fluxer_docs/src/content/docs/admin-api/blocklists.mdx index adc588e69..10201890f 100644 --- a/fluxer_docs/src/content/docs/admin-api/blocklists.mdx +++ b/fluxer_docs/src/content/docs/admin-api/blocklists.mdx @@ -8,7 +8,7 @@ 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, so 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 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. 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`. @@ -18,19 +18,19 @@ Fluxer synchronises disposable email domains from external feeds every six hours ## Blocklist types -`ip`, `email`, and `email-domain-suspicious` gate account access and registration. `phrase`, `url`, `url-domain`, `file-sha`, `avatar-hash`, and `profile-substring` gate what an account may post, link, upload, or display. `email` and `email-domain-suspicious` store nothing but the value, while an `ip` row also stores a ban kind, a reason, an expiry, and its creation time. +`ip`, `email`, and `email-domain-suspicious` gate account access and registration. `phrase`, `url`, `url-domain`, `file-sha`, `avatar-hash`, and `profile-substring` gate what an account may post, link, upload, or display. `email` and `email-domain-suspicious` store nothing but the value. An `ip` row also stores a ban kind, a reason, an expiry, and its creation time. | Value | Description | | --- | --- | | ip1 | IPv4 addresses, IPv6 addresses, and CIDR ranges denied service | -| email2 | Exact email addresses barred from registration and from being set on an account | +| email2 | Exact email addresses blocked from registration and from being set on an account | | email-domain-suspicious2 3 | Email domains that allow registration and require the new account to verify a phone number first | -| phrase4 | Phrases barred from content | -| url5 | Absolute `http` and `https` URLs barred from being posted | -| url-domain6 | Domains barred from being linked | -| file-sha7 | SHA-256 hashes barred from being uploaded | -| avatar-hash8 | Avatar hashes barred from being set | -| profile-substring4 9 | Substrings barred from one named profile field | +| phrase4 | Phrases blocked from content | +| url5 | Absolute `http` and `https` URLs blocked from being posted | +| url-domain6 | Domains blocked from being linked | +| file-sha7 | SHA-256 hashes blocked from being uploaded | +| avatar-hash8 | Avatar hashes blocked from being set | +| profile-substring4 9 | Substrings blocked from one named profile field | 1 Fluxer refuses an address with 400 `IP_BAN_DECLINED` when it is on the instance exemption list, or when the high blast-radius carrier network check matches, and records both refusals in the Admin audit log @@ -38,7 +38,7 @@ Fluxer synchronises disposable email domains from external feeds every six hours 3 Fluxer never shows the list to the account holder, who sees only the verified-phone gate. A domain must match `^[a-zA-Z0-9][a-zA-Z0-9\-.]*\.[a-zA-Z]{2,}$` -4 Canonicalised by NFKC normalisation, removal of control, format and variation-selector characters, lowercasing, and trimming. Match time also normalises inserted whitespace, punctuation, and compatibility glyphs +4 Canonicalised by NFKC normalisation, removal of control, format and variation-selector characters, lowercasing, and trimming. Match time also normalises inserted whitespace, punctuation, and compatibility characters 5 Canonicalised before storage. A value Fluxer cannot canonicalise returns 400 `INVALID_FORM_BODY` naming `url` @@ -46,11 +46,11 @@ Fluxer synchronises disposable email domains from external feeds every six hours 7 Stored as lowercase hexadecimal -8 Stored as the 8-character hash with any `a_` animation prefix stripped and the remainder lowercased, so the animated and static forms of one avatar are the same row +8 Stored as the 8-character hash with any `a_` animation prefix stripped and the rest lowercased, so the animated and static forms of one avatar are the same row 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 a list accepts differ from list to list, and so does the operation set. 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 three `supports_` flags of a [blocklist object](#blocklist-object) before writing to a list. ## Content blocklist categories @@ -92,14 +92,14 @@ Every `url`, `url-domain`, `file-sha`, and `avatar-hash` row has a severity. A r ## Blocklist object -One entry of the blocklist catalogue returned by [List blocklists](#list-blocklists). The catalogue is a constant of the release and reports no counts. +One entry of the blocklist catalogue returned by [List blocklists](#list-blocklists). The catalogue is fixed in each release and reports no counts. ### Structure | Field | Type | Description | | --- | --- | --- | | list_type | string | The [blocklist type](#blocklist-types) this entry describes | -| description | string | The human-readable summary of what the blocklist matches and how it matches | +| description | string | The human-readable summary of what the blocklist matches and how | | value_field1 | string | The request body field that has the value when adding | | fields2 | array[string] | The field names rows of this blocklist accept beyond the value (at most 8) | | scoped3 | boolean | Whether rows are scoped to a profile field and every operation has a scope | @@ -160,7 +160,7 @@ One stored row of one blocklist. Every field is present on every entry, and a fi 2 Non-null only on `profile-substring` -3 Non-null only on `url`, `url-domain`, `file-sha`, and `avatar-hash`, where `category` and `severity` are always populated +3 Non-null only on `url`, `url-domain`, `file-sha`, and `avatar-hash`, where `category` and `severity` are always set 4 Non-null only on `url`, `url-domain`, `file-sha`, `avatar-hash`, and `profile-substring` @@ -170,7 +170,7 @@ One stored row of one blocklist. Every field is present on every entry, and a fi 7 Non-null only on `ip`. An address added through [Add blocklist entry](#add-blocklist-entry) is written as a permanent ban, so it reads back with the reason `platform_admin_enforcement` and a null `expires_at` -8 Both null on `email`, `email-domain-suspicious`, and `phrase`, which store nothing but the value. `created_by_user_id` is additionally null on `ip` +8 Both null on `email`, `email-domain-suspicious`, and `phrase`, which store nothing but the value. `created_by_user_id` is also null on `ip` :::note[The audit entry records the accepted reason] An `avatar-hash` or `profile-substring` write accepts `reason`, and both lists then report `reason` as null in this object. @@ -236,7 +236,7 @@ The body of [Add blocklist entry](#add-blocklist-entry) is a union resolved by t Each field is a string, except `hashes` and `substrings`, which are `array[string]`. -`avatar-hash` deduplicates the array after canonicalisation. `profile-substring` does not, so a substring repeated in one request is written once and audited once per occurrence, and a value that canonicalises to an empty string is skipped. +`avatar-hash` deduplicates the array after canonicalisation. `profile-substring` does not, so a substring repeated in one request is written once and audited once per occurrence. A `profile-substring` value that canonicalises to an empty string is skipped. ### Additional fields @@ -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 maintains. 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 nine `check` permissions. The response is identical for every Admin. @@ -373,7 +373,7 @@ The response has no body, so it does not report the canonical form that was stor The written rows take effect for subsequent blocklist decisions. For every list except `email` and `email-domain-suspicious`, other nodes see the rows after a short propagation delay. No Gateway Dispatch is emitted. -One [Admin audit entry](/admin-api/#admin-audit-entry-object) is recorded per written value, with that value in its metadata. Fluxer records an entry for a refused `ip` too, under the action `ban_ip_skipped_exempt` or `ban_ip_skipped_cgnat`, before it returns the 400. +Fluxer records one [Admin audit entry](/admin-api/#admin-audit-entry-object) per written value, with that value in its metadata. It records an entry for a refused `ip` too, under the action `ban_ip_skipped_exempt` or `ban_ip_skipped_cgnat`, before it returns the 400. ### Rate limit @@ -383,7 +383,7 @@ One [Admin audit entry](/admin-api/#admin-audit-entry-object) is recorded per wr -Enqueues a background job that adds up to 10000 SHA-256 hashes to the `file-sha` blocklist and returns the job ID immediately. Requires `ban:file_sha:add`. +Queues a background job that adds up to 10000 SHA-256 hashes to the `file-sha` blocklist and returns the job ID immediately. Requires `ban:file_sha:add`. Only `file-sha` accepts this operation, reported as `supports_bulk_create` by [List blocklists](#list-blocklists). Every other list type returns 400 `INVALID_FORM_BODY`. @@ -416,11 +416,11 @@ Only `file-sha` accepts this operation, reported as `supports_bulk_create` by [L The operation returns before any hash is written. Read progress and the final counts from [Get job](/admin-api/jobs/#get-job), and stop the job with [Cancel job](/admin-api/jobs/#cancel-job). The job reports progress every 50 hashes and finishes with a summary naming how many were written and how many failed. -Every hash is written with the category `manual`, the severity `2`, and a null content type, source URL, and note. The bulk path accepts no metadata, so a hash that needs any of those goes through [Add blocklist entry](#add-blocklist-entry) or takes a later [Update blocklist entry](#update-blocklist-entry). +Every hash is written with the category `manual`, the severity `2`, and a null content type, source URL, and note. The bulk path accepts no metadata, so a hash that needs any of those goes through [Add blocklist entry](#add-blocklist-entry) or a later [Update blocklist entry](#update-blocklist-entry). ### Side effects -The job lowercases each hash and writes it as an upsert. It publishes one refresh notice after the last hash, so a job cancelled part way leaves the hashes it wrote invisible to other nodes until a later write or the twelve-hourly feed sync publishes one. No Gateway Dispatch is emitted. +The job lowercases each hash and writes it as an upsert. It publishes one refresh notice after the last hash, so a job cancelled part way leaves the hashes it wrote invisible to other nodes. They become visible on a later write, or on the feed sync that runs every twelve hours. No Gateway Dispatch is emitted. The job records one aggregate [Admin audit entry](/admin-api/#admin-audit-entry-object) under the action `bulk_ban_file_shas`, with the submitted, successful, and failed counts. A cancelled job records none. @@ -465,7 +465,7 @@ The body is selected by `list_type`. The operation is idempotent and reports no counts. A value with no stored row still returns 204. Read the list before and after to tell a removal from a no-op. -`avatar-hash` deduplicates the array after canonicalisation. `profile-substring` does not, so a substring repeated in one request is removed once and audited once per occurrence, and a value that canonicalises to an empty string is skipped. +`avatar-hash` deduplicates the array after canonicalisation. `profile-substring` does not, so a substring repeated in one request is removed once and audited once per occurrence. A `profile-substring` value that canonicalises to an empty string is skipped. ### Side effects @@ -507,7 +507,7 @@ Reports whether one value is currently blocked by the selected blocklist and ret | ip3 4 | The address itself, any stored CIDR range containing it, and any stored address the instance treats as the same origin | | email | Exact match on the lowercased address | | email-domain-suspicious5 | Exact match on the lowercased domain | -| phrase | Normalised phrase matching, so an obfuscated form of a stored phrase still reads as blocked | +| phrase | Normalised phrase matching, so a disguised form of a stored phrase still reads as blocked | | url6 | Exact match on the canonicalised URL | | url-domain | Exact match on the lowercased hostname | | file-sha | Exact match on the lowercased hexadecimal digest | @@ -520,7 +520,7 @@ Reports whether one value is currently blocked by the selected blocklist and ret 5 A domain the account policy exempts from contact-domain reputation reads as not blocked even while a row exists -6 This check does not consult the `url-domain` list, so a URL blocked in practice by a stored domain reads as not blocked. Check the hostname separately +6 This check does not read the `url-domain` list, so a URL that a stored domain blocks reads as not blocked. Check the hostname separately ### Response @@ -567,7 +567,7 @@ The body is the creation shape of the selected blocklist with the value field re | reason?5 | string | The reason (1-1024 characters) | | notes?6 | string | The internal note (1-1024 characters) | -1 Required by `profile-substring`. This operation takes no query parameters, so the scope travels in the body, and every other blocklist ignores it there +1 Required by `profile-substring`. This operation takes no query parameters, so the scope goes in the body, and every other blocklist ignores it there 2 Accepted only by `url`, `url-domain`, `file-sha`, and `avatar-hash` @@ -600,7 +600,7 @@ Changing the `scope` of a `profile-substring` row writes a second row under the The written fields take effect for subsequent matches, and the write publishes a refresh notice. No Gateway Dispatch is emitted. -One [Admin audit entry](/admin-api/#admin-audit-entry-object) is recorded under the same action an add records, with the canonical value. The audit entry has no previous values of the changed fields. +The write records one [Admin audit entry](/admin-api/#admin-audit-entry-object) under the same action an add records, with the canonical value but no previous values of the changed fields. ### Rate limit @@ -638,13 +638,13 @@ Removes one row from the selected blocklist. Returns 204 with an empty body. Req On the `url` blocklist, an `entry_value` Fluxer cannot canonicalise returns 400 `INVALID_FORM_BODY` naming `url` in the `errors` array. -The operation is idempotent. A value with no stored row returns 204 and still records an Admin audit entry, so this operation never returns 404 and never reports whether anything was removed. +Removal is idempotent. A value with no stored row returns 204 and still records an Admin audit entry, so this operation never returns 404 and never reports whether anything was removed. ### Side effects -The removed row stops affecting subsequent blocklist decisions, and other nodes stop applying it after a short propagation delay for every list except `email` and `email-domain-suspicious`. A value can remain blocked by another matching row, such as a single address covered by a stored CIDR range. No Gateway Dispatch is emitted. +The removed row stops affecting subsequent blocklist decisions. For every list except `email` and `email-domain-suspicious`, other nodes stop applying it after a short propagation delay. A value can remain blocked by another matching row, such as a single address covered by a stored CIDR range. No Gateway Dispatch is emitted. -One [Admin audit entry](/admin-api/#admin-audit-entry-object) is recorded with the canonical value in its metadata. +The removal records one [Admin audit entry](/admin-api/#admin-audit-entry-object), with the canonical value in its metadata. ### Rate limit @@ -666,7 +666,7 @@ This is a shortcut over [Add blocklist entry](#add-blocklist-entry) for blocking ### JSON body -Both fields are optional. An empty body and `{}` are both valid requests. +Both fields are optional, so an empty body and `{}` are both valid requests. | Field | Type | Description | | --- | --- | --- | @@ -698,7 +698,7 @@ Every account using that image shares the same truncated prefix, so blocking the The hash takes effect for subsequent avatar uploads, and the write publishes a refresh notice. No Gateway Dispatch is emitted. -One [Admin audit entry](/admin-api/#admin-audit-entry-object) is recorded under the action `ban_avatar_hash`, with the stored hash and the supplied reason. +Fluxer records one [Admin audit entry](/admin-api/#admin-audit-entry-object) under the action `ban_avatar_hash`, with the stored hash and the supplied reason. ### Rate limit diff --git a/fluxer_docs/src/content/docs/admin-api/bulk-jobs.mdx b/fluxer_docs/src/content/docs/admin-api/bulk-jobs.mdx index 0d1b3a28b..8c668c710 100644 --- a/fluxer_docs/src/content/docs/admin-api/bulk-jobs.mdx +++ b/fluxer_docs/src/content/docs/admin-api/bulk-jobs.mdx @@ -22,7 +22,7 @@ A bulk job creation has the identifier of the queued job. | --- | --- | --- | | job_id1 | snowflake | The job that applies the requested operation, readable through [Get job](/admin-api/jobs/#get-job) | -1 Fluxer writes the ledger row before it enqueues the job, so the identifier in a 200 is always readable +1 Fluxer writes the ledger row before it queues the job, so the identifier in a 200 is always readable ### Example @@ -140,11 +140,11 @@ The `task` discriminator selects one of these structures. Every ID array has an | 403 | [error response](/admin-api/#error-response) | `MISSING_ACL` without the ACL selected by `task` | | 5001 | [error response](/admin-api/#error-response) | The job could not be queued | -1 A full queue rejects the job, which surfaces as `INTERNAL_SERVER_ERROR` +1 A full queue rejects the job and the request returns `INTERNAL_SERVER_ERROR` ### Side effects -This operation writes no Admin audit entry. It records one [Jobs](/admin-api/jobs/) ledger row naming the acting Admin as the requester and storing the audit reason, then enqueues the worker task. When the ledger row cannot be written, Fluxer returns 500 `INTERNAL_SERVER_ERROR` and enqueues nothing. +This operation writes no Admin audit entry. It records one [Jobs](/admin-api/jobs/) ledger row naming the acting Admin as the requester and storing the audit reason, then queues the worker task. When the ledger row cannot be written, Fluxer returns 500 `INTERNAL_SERVER_ERROR` and queues nothing. The worker processes entities in the submitted order, one at a time. A cancellation check runs before each entity, so [Cancel job](/admin-api/jobs/#cancel-job) stops the run between two entities and settles the job as `cancelled`. Every entity changed before cancellation or failure stays changed. An entity that fails, including an ID that resolves to nothing, is counted as failed and skipped, and the run continues through the rest of the set. @@ -163,7 +163,7 @@ Every task writes one summary Admin audit entry when it finishes, with the actio `schedule_user_deletion` marks each account deleted. The task stores the reason code, public reason, and audit reason on the account, and reschedules its pending deletion. It dispatches [User Update](/gateway/events/#user-update), writes one `schedule_deletion` entry for each account, and emails the account holder when an address is on file. A failed email is logged and does not fail the entity. :::caution[Bulk scheduling is narrower than the single-account operation] -The worker does not terminate sessions, cancel or refund a Stripe subscription, ban the account's identifiers, or resolve pending reports. [Schedule user deletion](/admin-api/users/#schedule-user-deletion) performs each of those for one account, the last two only when the reason is not `USER_REQUESTED`. +The worker does not terminate sessions, cancel or refund a Stripe subscription, ban the account's identifiers, or resolve pending reports. [Schedule user deletion](/admin-api/users/#schedule-user-deletion) does each of those for one account, the last two only when the reason is not `USER_REQUESTED`. ::: ### Rate limit diff --git a/fluxer_docs/src/content/docs/admin-api/discovery.mdx b/fluxer_docs/src/content/docs/admin-api/discovery.mdx index 44f694efc..0fb008208 100644 --- a/fluxer_docs/src/content/docs/admin-api/discovery.mdx +++ b/fluxer_docs/src/content/docs/admin-api/discovery.mdx @@ -92,7 +92,7 @@ An approved listing. It has every field of the [Admin pending application object ## Discovery application object -This is the [discovery application object](/http-api/discovery/#discovery-application-object) of the public Discovery resource. Every write on this page answers with it, so a client re-reads the listing after a write to render the enriched guild fields. +This is the [discovery application object](/http-api/discovery/#discovery-application-object) of the public Discovery resource. Every write on this page answers with it, so a client re-reads the listing after a write to render the enriched guild fields. No response on this page exposes the Admin that approved, rejected, or removed the application. ### Structure @@ -137,10 +137,6 @@ This is the [discovery application object](/http-api/discovery/#discovery-applic } ``` -## Reviewer identity - -The application records the Admin that approved, rejected, or removed it. No response on this page exposes that value. - ## Discovery categories Each release ships a fixed category set, and no Admin operation changes it. See [discovery categories](/http-api/discovery/#discovery-categories) for the set. @@ -309,7 +305,7 @@ Files every named guild under one [discovery category](/http-api/discovery/#disc | updated | integer | Number of distinct guilds that were moved | | failed_guild_ids1 | array[snowflake] | Distinct guilds that were not moved | -1 A guild lands here when it holds no application, when its application is rejected or removed, or when the write failed. The response does not say which, so retry the guild through [Update discovery listing](#update-discovery-listing) to read the error +1 A guild lands here when it holds no application, when its application is rejected or removed, or when the write failed. The response does not say which. Retry the guild through [Update discovery listing](#update-discovery-listing) to read the error ### Response @@ -397,7 +393,7 @@ Removes an approved listing from discovery, strips the guild's discoverable feat ### JSON body -The operation takes a required body even though it is a DELETE, so a request without one fails body validation. +The body is required even though this is a DELETE, so a request without one fails body validation. | Field | Type | Description | | --- | --- | --- | diff --git a/fluxer_docs/src/content/docs/admin-api/gateway.mdx b/fluxer_docs/src/content/docs/admin-api/gateway.mdx index 7852d4f93..ca443915d 100644 --- a/fluxer_docs/src/content/docs/admin-api/gateway.mdx +++ b/fluxer_docs/src/content/docs/admin-api/gateway.mdx @@ -110,7 +110,7 @@ One entry for each live guild process the read sampled. Every value except `nsfw | guild_name1 | string | The guild name the process holds in memory | | guild_icon | ?string | The icon hash the guild process holds in memory, or null when it has none | | nsfw_level2 | ?integer | The [NSFW level](/http-api/guilds/#nsfw-levels) resolved from stored guild data, or null when the guild could not be resolved | -| memory3 | string | The bytes the guild process occupies, as a decimal string | +| memory3 | string | The bytes the guild process uses, as a decimal string | | member_count | integer | The number of members the guild process holds | | session_count | integer | The number of sessions subscribed to the guild process | | presence_count | integer | The number of presences the guild process tracks | diff --git a/fluxer_docs/src/content/docs/admin-api/guilds.mdx b/fluxer_docs/src/content/docs/admin-api/guilds.mdx index 99bcf690f..13cc75d4e 100644 --- a/fluxer_docs/src/content/docs/admin-api/guilds.mdx +++ b/fluxer_docs/src/content/docs/admin-api/guilds.mdx @@ -6,7 +6,7 @@ description: Guild search, detail, mutation, deletion, membership, expressions, import RouteHeader from '@/components/RouteHeader.astro'; -Admin guild operations read and change the guilds the public [Guilds](/http-api/guilds/) resource exposes. Most of them need no membership in the guild and no guild permission, and [Remove guild member](#remove-guild-member) and [Ban guild member](#ban-guild-member) are the exceptions. +Admin guild operations read and change the guilds the public [Guilds](/http-api/guilds/) resource exposes. Most need no membership in the guild and no guild permission. [Remove guild member](#remove-guild-member) and [Ban guild member](#ban-guild-member) are the exceptions. [Archives](/admin-api/archives/) owns the archive lifecycle and the downloads. @@ -18,7 +18,7 @@ Reads use the `admin:lookup` bucket, which permits 200 requests per minute for e ## Admin guild object -The compact guild representation returned by [List guilds](#list-guilds) and by [List user guilds](/admin-api/users/#list-user-guilds). It has no channel or role state. +The compact guild representation returned by [List guilds](#list-guilds) and by [List user guilds](/admin-api/users/#list-user-guilds), with no channel or role state. ### Structure @@ -146,7 +146,7 @@ The full guild representation returned by [Get guild](#get-guild). It embeds eve ## Admin guild update object -The guild state [Update guild](#update-guild) reads back after applying the request. It has no channel, role, or owner identity state. +The guild state [Update guild](#update-guild) reads back after applying the request, with no channel, role, or owner identity state. ### Structure @@ -163,7 +163,7 @@ The guild state [Update guild](#update-guild) reads back after applying the requ ## Admin guild expression object -One custom emoji or one sticker of a guild, together 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 two listings return the same shape. ### Structure @@ -292,7 +292,7 @@ Returns one guild with its channels and roles. Requires `guild:lookup`. | Status | Body | Condition | | --- | --- | --- | -| 200 | response body | A lookup was performed, whether or not it resolved | +| 200 | response body | A lookup was done, whether or not it resolved | ### Rate limit @@ -308,11 +308,11 @@ This is the only way to change the owner of a guild without acting as its curren Fluxer evaluates authorisation in two stages and reads the body between them. The account first needs at least one of `guild:update:name`, `guild:update:settings`, `guild:update:features`, `guild:update:vanity`, and `guild:transfer_ownership`. The validated body then selects a set of ACLs and every one of them is required, so a body with `name` and `nsfw` needs both `guild:update:name` and `guild:update:settings`. The wildcard satisfies both stages. -- `guild:update:name` is selected by `name`. -- `guild:update:settings` is selected by `fields`, `verification_level`, `mfa_level`, `nsfw_level`, `nsfw`, `content_warning_level`, `content_warning_text`, `explicit_content_filter`, `default_message_notifications`, and `disabled_operations`. -- `guild:update:features` is selected by `add_features` and `remove_features`. -- `guild:update:vanity` is selected by `vanity_url_code`. -- `guild:transfer_ownership` is selected by `new_owner_id`. +- `name` selects `guild:update:name`. +- `fields`, `verification_level`, `mfa_level`, `nsfw_level`, `nsfw`, `content_warning_level`, `content_warning_text`, `explicit_content_filter`, `default_message_notifications`, and `disabled_operations` select `guild:update:settings`. +- `add_features` and `remove_features` select `guild:update:features`. +- `vanity_url_code` selects `guild:update:vanity`. +- `new_owner_id` selects `guild:transfer_ownership`. A body with no field at all selects nothing, so an empty patch applies no change. @@ -379,7 +379,7 @@ The groups run in a fixed order: image clears, settings, features, name, custom ### Side effects -Clearing an image field queues the previous stored object for deletion. Replacing the custom invite code deletes the invite record holding the previous code and creates one for the new code, while sending null deletes the previous record without creating another. +Clearing an image field queues the previous stored object for deletion. Replacing the custom invite code deletes the invite record holding the previous code and creates one for the new code. Sending null deletes the previous record without creating another. Supplying `add_features` or `remove_features` reconciles an existing discovery application. The application is approved when [DISCOVERABLE](/http-api/guilds/#guild-features) becomes present and it is not already approved, and it is marked removed when `DISCOVERABLE` becomes absent and it was approved. A guild that has never applied for [discovery](/admin-api/discovery/) gains no application. @@ -517,9 +517,9 @@ Only the Admin ACL is evaluated. ### Side effects -Fluxer creates the membership with the Admin force-add join source and restores a communication timeout still in force from a previous membership. The guild ban list is not consulted, so a banned user can be admitted. The suspicious activity phone gate does not run. The per-user guild limit and the guild member limit are still enforced. +Fluxer creates the membership with the Admin force-add join source and restores a communication timeout still in force from a previous membership. The guild ban list is not checked, so a banned user can be admitted. The suspicious activity phone gate does not run. The per-user guild limit and the guild member limit are still enforced. -[Guild Member Add](/gateway/events/#guild-member-add) fires to the guild, the user's sessions are joined to the guild on the main Gateway, and the member enters guild member search when the guild has an indexed member set. The ordinary join system message is created, and with it a [Message Create](/gateway/events/#message-create) Dispatch, unless the guild sets `SUPPRESS_JOIN_NOTIFICATIONS` or has no usable system channel. A bot target also records a `BOT_ADD` entry in the guild's own [audit log](/http-api/guild-audit-logs/#audit-actions). +[Guild Member Add](/gateway/events/#guild-member-add) fires to the guild, and the user's sessions are joined to the guild on the main Gateway. The member enters guild member search when the guild has an indexed member set. The ordinary join system message is created, and with it a [Message Create](/gateway/events/#message-create) Dispatch, unless the guild sets `SUPPRESS_JOIN_NOTIFICATIONS` or has no usable system channel. A bot target also records a `BOT_ADD` entry in the guild's own [audit log](/http-api/guild-audit-logs/#audit-actions). A user who is already a member keeps their existing membership. No membership is created, no counter moves, and no Dispatch is emitted. The Admin audit entry is still written. @@ -561,7 +561,7 @@ The acting account must see the guild, hold [KICK_MEMBERS](/http-api/permissions Fluxer snapshots the membership metadata, including any communication timeout, so that a later rejoin restores it. It then deletes the membership, decreases the recorded member count by one, and detaches the user from the guild on the main Gateway. The member is removed from guild member search when the guild has an indexed member set. -[Guild Member Remove](/gateway/events/#guild-member-remove) fires to the guild. A `MEMBER_KICK` entry is written to the guild's own [audit log](/http-api/guild-audit-logs/#audit-actions). The entry names the acting Admin account and has the audit reason, and the write fires [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create) to sessions holding [VIEW_AUDIT_LOG](/http-api/permissions/). +[Guild Member Remove](/gateway/events/#guild-member-remove) fires to the guild. A `MEMBER_KICK` entry is written to the guild's own [audit log](/http-api/guild-audit-logs/#audit-actions). The entry names the acting Admin account and has the audit reason. The write fires [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create) to sessions holding [VIEW_AUDIT_LOG](/http-api/permissions/). The removal creates no ban record, so the user can rejoin. @@ -611,18 +611,18 @@ The acting account must hold [BAN_MEMBERS](/http-api/permissions/) in the guild, | 404 | [error response](/admin-api/#error-response) | `UNKNOWN_MEMBER` because the target is the acting account, or `UNKNOWN_USER` because the user does not exist | :::caution[The ban can also delete messages] -The ban removes current membership and blocks later joins until its expiry or explicit removal. Messages inside the resolved deletion window are permanently deleted and are not restored when the ban ends. +A ban removes current membership and blocks later joins until its expiry or explicit removal. Messages inside the resolved deletion window are permanently deleted and are not restored when the ban ends. ::: ### Side effects -The ban record names the acting Admin account as moderator and has the expiry, the reason, the target's last known IP address unless that address is on the ban exemption list, and the target's lowercased email address. While the ban exists the guild also blocks that address and that email, as described by [Guild moderation](/http-api/guild-moderation/#guild-ban-object), and removing the ban releases both. +The ban record names the acting Admin account as moderator. It has the expiry, the reason, the target's lowercased email address, and the target's last known IP address unless that address is on the ban exemption list. While the ban exists the guild also blocks that address and that email, as described by [Guild moderation](/http-api/guild-moderation/#guild-ban-object), and removing the ban releases both. A positive deletion window queues a background job that deletes the target's matching messages after the response, which fires [Message Delete Bulk](/gateway/events/#message-delete-bulk) as deletion progresses. -[Guild Ban Add](/gateway/events/#guild-ban-add) fires to the guild. A target who was a member is then removed, which decreases the recorded member count, detaches the user from the guild on the main Gateway, removes the member from guild member search, and fires [Guild Member Remove](/gateway/events/#guild-member-remove). The ban path snapshots no membership metadata, so a communication timeout in force at the moment of the ban is not restored on a later rejoin. This operation writes no entry to the guild's own audit log, and it emits no [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create). +[Guild Ban Add](/gateway/events/#guild-ban-add) fires to the guild. A target who was a member is then removed. That decreases the recorded member count, detaches the user from the guild on the main Gateway, removes the member from guild member search, and fires [Guild Member Remove](/gateway/events/#guild-member-remove). The ban path snapshots no membership metadata, so a communication timeout in force at the moment of the ban is not restored on a later rejoin. This operation writes no entry to the guild's own audit log, and it emits no [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create). -One Admin audit entry is recorded with the action `ban_member`, the target type `guild_member`, the banned user as the target, and the guild ID, user ID, `delete_message_days` value, and any supplied reason and duration in the metadata. +One Admin audit entry is recorded with the action `ban_member`, the target type `guild_member`, and the banned user as the target. Its metadata has the guild ID, the user ID, the `delete_message_days` value, and any supplied reason and duration. ### Rate limit @@ -751,7 +751,7 @@ Every entry in `processed` records its own Admin audit entry, with the numeric I Returns one page of the guild's own in-app audit log, without requiring guild membership or [VIEW_AUDIT_LOG](/http-api/permissions/). Requires `guild:audit_log:view`. -The page has the same shape and the same semantics as the public [List guild audit logs](/http-api/guild-audit-logs/#list-guild-audit-logs) operation, including the message deletion consolidation that operation performs. +The page has the same shape and semantics as the public [List guild audit logs](/http-api/guild-audit-logs/#list-guild-audit-logs) operation, including its message deletion consolidation. ### Path parameters diff --git a/fluxer_docs/src/content/docs/admin-api/index.mdx b/fluxer_docs/src/content/docs/admin-api/index.mdx index 309f17335..1f340f369 100644 --- a/fluxer_docs/src/content/docs/admin-api/index.mdx +++ b/fluxer_docs/src/content/docs/admin-api/index.mdx @@ -16,12 +16,12 @@ Fluxer evaluates the credential in a fixed order. 1. A request that resolves no account is refused with 401 `UNAUTHORIZED`. 2. A credential presented with the `Bot` scheme is refused with 401 `UNAUTHORIZED`. -3. An access token issued to any OAuth2 application other than the built-in Admin application is refused with 403 `ACCESS_DENIED`. +3. Any access token issued to an OAuth2 application other than the built-in Admin application is refused with 403 `ACCESS_DENIED`. 4. An account holding neither `admin:authenticate` nor the wildcard `*` is refused with 403 `MISSING_PERMISSIONS`. 5. An account that clears both gates but satisfies none of the [ACLs](#acl-evaluation) the operation names is refused with 403 `MISSING_ACL`. :::caution[A denial names no requirement] -The `MISSING_ACL` body is a fixed sentence that has no ACL name and no requirement list, so a response never discloses which requirement failed. +The `MISSING_ACL` body is a fixed sentence with no ACL name and no requirement list, so a denial never says which requirement failed. ::: No Admin operation declares an OAuth2 scope, an MFA requirement, or a sudo requirement, so none consumes the `X-Fluxer-Sudo-Mode-JWT` header. An accepted credential together with the required ACLs is the complete authorisation boundary. @@ -60,7 +60,7 @@ A body with none of those fields resolves to no ACL at all and applies no change - `add_guild_members` maps to `bulk:add:guild_members`. - `schedule_user_deletion` maps to `bulk:delete:users`. -Fluxer bounds every grant separately. [Set user ACLs](/admin-api/users/#set-user-acls) and [Create Admin API key](/admin-api/api-keys/#create-admin-api-key) both refuse to write an ACL the acting Admin does not itself hold, with 403 `MISSING_ACL`. A wildcard holder is exempt. Set user ACLs additionally refuses the acting Admin's own account with 403 `ACCESS_DENIED`, and it resolves the target before the bound is evaluated, so an unknown ID fails first with 404 `UNKNOWN_USER`. +Fluxer bounds every grant separately. [Set user ACLs](/admin-api/users/#set-user-acls) and [Create Admin API key](/admin-api/api-keys/#create-admin-api-key) both refuse to write an ACL the acting Admin does not itself hold, with 403 `MISSING_ACL`. A wildcard holder is exempt. Set user ACLs also refuses the acting Admin's own account with 403 `ACCESS_DENIED`, and it resolves the target before the bound is evaluated, so an unknown ID fails first with 404 `UNKNOWN_USER`. [Set user ACLs](/admin-api/users/#set-user-acls), [Create Admin API key](/admin-api/api-keys/#create-admin-api-key), and [Update Admin API key](/admin-api/api-keys/#update-admin-api-key) each accept at most 111 ACLs and validate every entry against the registry, so a value outside it returns 400 `INVALID_FORM_BODY`. @@ -116,7 +116,7 @@ An entry stores an acting Admin, a target type, a target ID, an action name, an 7 Keys are 1 to 256 characters and values are 0 to 4,000 characters. The keys an operation records are stated by that operation -Fluxer populates the `related_` maps from the acting Admin, from the resolved target, and from the metadata. A metadata key contributes when its value is a decimal integer and its name is `user_id`, `guild_id`, `channel_id`, `target_user_id`, or `admin_user_id`, or ends in `_user_id`, `_guild_id`, or `_channel_id`. An identifier that no longer resolves is omitted from the map. +Fluxer sets the `related_` maps from the acting Admin, from the resolved target, and from the metadata. A metadata key contributes when its value is a decimal integer and its name is `user_id`, `guild_id`, `channel_id`, `target_user_id`, or `admin_user_id`, or ends in `_user_id`, `_guild_id`, or `_channel_id`. An identifier that no longer resolves is omitted from the map. An entry has no request method, request path, status, duration, IP address, or field change list. Fluxer never redacts an entry on read, so a caller holding `audit_log:view` reads `audit_log_reason` and every metadata value in full. @@ -222,11 +222,11 @@ An entry has no request method, request path, status, duration, IP address, or f 2 The target has no snowflake, so `target_id` is `0` and the entry's textual identity is in `metadata` -The field is a free-form string, so a client MUST tolerate a value outside this table. +The field is a free-form string, so a client MUST accept a value outside this table. ### Audit actions -An operation records one of the following actions. The field is a free-form string of 1 to 256 characters, so a client MUST tolerate a value outside this table. +An operation records one of the following actions. The field is a free-form string of 1 to 256 characters, so a client MUST accept a value outside this table. | Value | Description | | --- | --- | @@ -322,7 +322,7 @@ An operation records one of the following actions. The field is a free-form stri | update_voice_server | A registered voice server was updated | | verify_email | An email address was marked verified | -1 The same action name is recorded for two different resources, so `target_type` disambiguates the entry +1 The same action name is recorded for two different resources, so `target_type` tells them apart @@ -330,7 +330,7 @@ An operation records one of the following actions. The field is a free-form stri Fluxer reads the `X-Audit-Log-Reason` header once at the boundary, on every request to the API, Admin or not. The header is read raw and never percent-decoded, so a client that URI-encodes the value has the encoded form stored. -Fluxer normalises the value by stripping control and format characters and trimming surrounding whitespace. An absent header, a blank value, and a value whose normalised length exceeds 512 characters all resolve to null. Fluxer never fails a request during this normalisation, so an over-long reason is dropped silently. +Normalisation strips control and format characters and trims surrounding whitespace. An absent header, a blank value, and a value whose normalised length exceeds 512 characters all resolve to null. Fluxer never fails a request during this normalisation, so an over-long reason is dropped silently. Fluxer writes the resolved value to `audit_log_reason` on every entry the request records. An operation that records no entry accepts the header and does nothing with it. An Admin resource page marks an operation with the audit reason capability only where an entry stores the value. @@ -502,11 +502,11 @@ The registry is returned in this order by [List ACLs](#list-acls). A value outsi | user:view:dob6 | Unredacts user dates of birth | | user:view:email6 | Unredacts user email addresses and email verification state | | user:view:ip6 | Unredacts the last active IP address and the location derived from it | -| user:temp_ban | Applies and lifts an account ban | +| user:temp_ban | Applies and removes an account ban | | user:update:bot_status | Updates bot and system account state | | user:update:dob | Updates dates of birth | | user:update:email | Updates the email address, marks the address verified, resends verification, and sends a password reset | -| user:update:flags | Updates account flags and premium flags, and terminates every login session of an account | +| user:update:flags | Updates account flags and premium flags, and ends every login session of an account | | user:update:mfa | Reads and removes a user's WebAuthn credentials, and removes MFA state | | user:update:phone | Updates verified phone state | | user:update:profile | Clears profile fields | diff --git a/fluxer_docs/src/content/docs/admin-api/instance.mdx b/fluxer_docs/src/content/docs/admin-api/instance.mdx index d409510a2..e71c612d0 100644 --- a/fluxer_docs/src/content/docs/admin-api/instance.mdx +++ b/fluxer_docs/src/content/docs/admin-api/instance.mdx @@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro'; Instance configuration is everything an operator can change at runtime. It covers single sign-on, Gateway rollout, registration policy, branding and legal links, instance policy, third party integrations, media retention, and the ordered limit configuration. The [Instance](/http-api/instance/) resource serves the subset published to unauthenticated clients. -Every write is a merge over the stored configuration, and an omitted key leaves the stored value unchanged. The limit configuration write is the one exception, and it replaces the stored document. +Every write is a merge over the stored configuration, and an omitted key leaves the stored value unchanged. The limit configuration write is the one exception and replaces the stored document. Reading configuration requires the [Admin ACL](/admin-api/#acl-registry) `instance:config:view`, and writing it requires `instance:config:update`. The limit configuration has its own pair, `instance:limit_config:view` and `instance:limit_config:update`. The heap snapshot requires `system:heap_snapshot`. @@ -208,7 +208,7 @@ Community, direct message, premium and gating policy for the whole deployment. | Field | Type | Description | | --- | --- | --- | | single_community_enabled | boolean | Whether the deployment presents one community | -| single_community_guild_id | ?string | Guild used as that community, or null before one has been provisioned | +| single_community_guild_id | ?string | Guild used as that community, or null before one has been created | | direct_messages_disabled | boolean | Whether direct messages are disabled | | direct_messages_locked1 | boolean | Whether the direct message setting is locked against further change | | premium_mode | string | [Premium mode](#premium-modes) | @@ -221,7 +221,7 @@ Community, direct message, premium and gating policy for the whole deployment. 2 Each key is the operator override when one is set, and otherwise the matching `services_available` value -3 `gif` and `youtube` report whether an API key resolves from either the stored configuration or the deployment configuration. `bluesky` reports the integration's resolved enablement +3 `gif` and `youtube` report whether an API key resolves from the stored configuration or the deployment configuration. `bluesky` reports the integration's resolved enablement ## Premium modes @@ -232,15 +232,15 @@ Community, direct message, premium and gating policy for the whole deployment. ## Deferred phone gate object -A rule that imposes phone verification in a configured window of hours after registration. +A rule that requires phone verification in a configured window of hours after registration. ### Structure | Field | Type | Description | | --- | --- | --- | | enabled | boolean | Whether the delayed phone requirement is applied (default false) | -| window_hours | number | Hours after registration in which the requirement can be imposed (default 6) | -| member_threshold | number | Guild member count above which the requirement is imposed (default 50) | +| window_hours | number | Hours after registration in which the requirement can be applied (default 6) | +| member_threshold | number | Guild member count above which the requirement is applied (default 50) | ## Instance integrations object @@ -265,7 +265,7 @@ CAPTCHA provider settings, and the provider resolved from them. | Field | Type | Description | | --- | --- | --- | | provider | ?string | Operator override of `hcaptcha`, `turnstile`, or `none`, or null for no override | -| effective_provider | string | Provider actually in use, one of `hcaptcha`, `turnstile`, or `none` | +| effective_provider | string | Provider in use, one of `hcaptcha`, `turnstile`, or `none` | | hcaptcha_site_key | ?string | hCaptcha site key | | hcaptcha_secret_key_set | boolean | Whether an hCaptcha secret is stored or supplied by deployment configuration | | turnstile_site_key | ?string | Turnstile site key | @@ -283,7 +283,7 @@ Outbound email settings, and the provider resolved from them. | enabled | ?boolean | Operator override, or null for no override | | effective_enabled | boolean | Whether outbound email is in force | | provider | ?string | Operator override of `smtp` or `none`, or null for no override | -| effective_provider | string | Provider actually in use, either `smtp` or `none` | +| effective_provider | string | Provider in use, either `smtp` or `none` | | from_email | ?string | Envelope sender address | | from_name | ?string | Envelope sender name | | smtp1 | object | `host`, `port`, `username`, `secure`, and `password_set` | @@ -383,7 +383,7 @@ The stored limit configuration together with the deployment defaults and the met | limit_config_json | string | The same document rendered as JSON indented by two spaces, for an editor to display | | self_hosted | boolean | Whether the deployment runs in self-hosted mode | | defaults1 | map[string, map[string, integer]] | Deployment default limits, keyed by rule identifier and then by limit key | -| metadata | map[string, [limit key metadata](#limit-key-metadata-object) object] | Presentation metadata for each limit key | +| metadata | map[string, [limit key metadata](#limit-key-metadata-object) object] | Display metadata for each limit key | | categories2 | map[string, string] | Display label for each metadata category | | limit_keys | array[string] | Every [limit key](/http-api/instance/#limit-keys) in registry order | | bounds?3 | map[string, object] | Optional `min` and `max` pair for each limit key | @@ -424,7 +424,7 @@ One rule in that ordered set, with the filters that scope it and the limits it s ## Limit key metadata object -Presentation metadata for one [limit key](/http-api/instance/#limit-keys), which an editor uses to render its control. +Display metadata for one [limit key](/http-api/instance/#limit-keys), which an editor uses to render its control. ### Structure @@ -463,7 +463,7 @@ Returns the [instance configuration](#instance-configuration-object) object. Req Applies a merge patch to the stored configuration and returns the resulting [instance configuration](#instance-configuration-object) object. Requires `instance:config:update`, or a session credential until setup is marked complete. :::note[Every section is optional and merged independently] -The body has one optional object for each section, and Fluxer leaves an absent section alone. Within a section, an absent key keeps its stored value, and a key sent as null clears the stored value where the schema permits null. +The body has one optional object for each section. Fluxer leaves an absent section alone. Within a section, an absent key keeps its stored value, and a key sent as null clears the stored value where the schema permits null. ::: ### JSON body @@ -485,7 +485,7 @@ The body has one optional object for each section, and Fluxer leaves an absent s 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 that is 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 two endpoints and the claims source. ::: #### Instance policy update structure @@ -500,7 +500,7 @@ Fluxer skips URL validation while the merged configuration leaves single sign-on | services? | object | Nullable `gif_enabled`, `youtube_enabled`, and `bluesky_enabled` overrides | | deferred_phone_gate?3 | object | `enabled`, `window_hours`, and `member_threshold` | -1 Setting `single_community_enabled` to true adopts the already designated guild when one still exists, and otherwise provisions a community using `single_community_name` or the configured product name. On a deployment whose setup is already complete, enabling it while no guild is designated fails with 400 `INSTANCE_POLICY_TRANSITION_NOT_ALLOWED`, as does enabling it when the acting Admin account cannot be resolved. Setting it to false only clears the flag and leaves the guild in place +1 Setting `single_community_enabled` to true adopts the already designated guild when one still exists, and otherwise creates a community using `single_community_name` or the configured product name. On a deployment whose setup is already complete, enabling it while no guild is designated fails with 400 `INSTANCE_POLICY_TRANSITION_NOT_ALLOWED`, as does enabling it when the acting Admin account cannot be resolved. Setting it to false only clears the flag and leaves the guild in place 2 The setting can be changed only while `direct_messages_locked` is false, and a change attempted after the lock is set fails with 400 `INSTANCE_POLICY_TRANSITION_NOT_ALLOWED` unless the same request sets `direct_messages_locked` to false. Re-enabling direct messages sets the lock again @@ -700,7 +700,7 @@ Both decisions remove the pending registration, so the same account cannot be de Approval removes both the `registration_pending_approval` trait and the `registration_rejected` trait, and joins the account to the single community when that mode is enabled and a guild is designated. A join that fails is logged and does not fail the request. Rejection removes the `registration_pending_approval` trait and adds the `registration_rejected` trait, which blocks login and every later session creation. A session issued before the decision stays valid. The pending registration is removed either way. -One Admin audit entry with the action `approve_registration` or `reject_registration` targets the account and records the audit reason. It has no metadata. +One Admin audit entry with the action `approve_registration` or `reject_registration` targets the account, records the audit reason, and has no metadata. ### Rate limit @@ -786,7 +786,7 @@ The 200 has `Content-Type: application/octet-stream`, `Content-Disposition: atta A client MUST NOT decode the body as JSON. The published OpenAPI document declares a JSON object of `success`, `filename`, and `size_bytes` here, so a generated client has to be overridden. :::danger[A snapshot stalls the process and exposes memory] -Writing a snapshot blocks the process that serves the request for as long as the dump takes. The file contains whatever that process held in memory, including message content, tokens, and personal data. +Writing a snapshot blocks the serving process until the dump finishes. The file contains whatever that process held in memory, including message content, tokens, and personal data. ::: :::caution[Only the serving node is captured] diff --git a/fluxer_docs/src/content/docs/admin-api/jobs.mdx b/fluxer_docs/src/content/docs/admin-api/jobs.mdx index 97af6b212..71d9dab8c 100644 --- a/fluxer_docs/src/content/docs/admin-api/jobs.mdx +++ b/fluxer_docs/src/content/docs/admin-api/jobs.mdx @@ -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 cadence, 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 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 2 Written only when the job is dead-lettered. A single failed delivery that is redelivered leaves it null @@ -56,7 +56,7 @@ One object is one row of the ledger. No operation on this page changes any field 4 Only [Queue bulk job](/admin-api/bulk-jobs/#queue-bulk-job) and the file SHA bulk ban record one, so a system DM broadcast, an archive, and a search index rebuild all report null -5 The reason the queueing operation resolved from `X-Audit-Log-Reason`. A blank or over-length header resolves to null there, and the queueing operation still succeeds +5 The reason the queueing operation resolved from `X-Audit-Log-Reason`. A blank or too-long header resolves to null there, and the queueing operation still succeeds 6 Present as soon as the job is queued diff --git a/fluxer_docs/src/content/docs/admin-api/messages.mdx b/fluxer_docs/src/content/docs/admin-api/messages.mdx index 70e103be2..627c660c9 100644 --- a/fluxer_docs/src/content/docs/admin-api/messages.mdx +++ b/fluxer_docs/src/content/docs/admin-api/messages.mdx @@ -44,7 +44,7 @@ Every operation that returns Admin message objects also returns the matching pub 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 -2 Always absent from the objects these operations return. The same member is populated on the [report message context object](/admin-api/reports/#report-message-context-object) +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) 3 Written on every object these operations return, and empty when no NCMEC report has been filed against the author @@ -95,7 +95,7 @@ An attachment entry has the ordinary attachment metadata together with the NCMEC 5 Assigned only once NCMEC accepts the report, so it is null while `ncmec_status` is `not_submitted` and after a failed submission -6 Populated only when `ncmec_status` is `failed`, and it stores the raw upstream error. The sanitised wording appears in the failing response body instead +6 Set only when `ncmec_status` is `failed`, and it stores the raw upstream error. The sanitised wording appears in the failing response body instead #### NCMEC submission status @@ -363,7 +363,7 @@ Shred jobs are created by [Shred user messages](/admin-api/users/#shred-user-mes | --- | --- | --- | | job_id1 | snowflake | The message shred job ID, taken from the queueing response | -1 A snowflake issued when the job was queued, so a value that is not a decimal integer fails path validation rather than reporting `not_found` +1 A snowflake issued when the job was queued, so a value that is not a decimal integer fails path validation instead of reporting `not_found` ### Response @@ -383,7 +383,7 @@ Submits one image or video attachment to NCMEC and starts the account enforcemen 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. -This operation ignores `X-Audit-Log-Reason`. The reason on every audit entry the workflow produces is synthesised from the assigned NCMEC report ID and the channel and attachment IDs, and it is returned as `audit_log_reason`. +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`. ### JSON body @@ -407,7 +407,7 @@ This operation ignores `X-Audit-Log-Reason`. The reason on every audit entry the | --- | --- | --- | | success | boolean | Always true | | ncmec_report_id | string | The report ID NCMEC assigned to this submission (1-256 characters) | -| audit_log_reason3 | string | The synthesised reason recorded on every audit entry the workflow produced (1-4,000 characters) | +| audit_log_reason3 | string | The reason recorded on every audit entry the workflow produced (1-4,000 characters) | 3 The NCMEC report ID followed by the channel ID and attachment ID, so one string identifies the submission in the audit log and at NCMEC @@ -437,7 +437,7 @@ When the resolved attachment has an author who has not already been enforced aga Message content is deleted only after that archive completes. The archive is re-checked every 15 seconds by default, for at most 240 attempts. Once the archive is complete the reported message is deleted, its attachments are purged, and [Message Delete](/gateway/events/#message-delete) is sent. -A successful submission records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with the action `NCMEC Report`. It targets the reported user, or targets the source report when the attachment resolved to no author. Triggering the archive records a second entry with the action `trigger_user_archive` targeting the same user, skipped when the account already has an archive taken after the enforcement. Both entries have the synthesised reason returned as `audit_log_reason`. +A successful submission records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with the action `NCMEC Report`. It targets the reported user, or targets the source report when the attachment resolved to no author. Triggering the archive records a second entry with the action `trigger_user_archive` targeting the same user, skipped when the account already has an archive taken after the enforcement. Both entries record the reason returned as `audit_log_reason`. An attachment that resolved to no author is submitted to NCMEC and audited. No account is disabled, no archive is triggered, and no message content is deleted. diff --git a/fluxer_docs/src/content/docs/admin-api/reports.mdx b/fluxer_docs/src/content/docs/admin-api/reports.mdx index 8d2aaf6d6..bbf7f8ab7 100644 --- a/fluxer_docs/src/content/docs/admin-api/reports.mdx +++ b/fluxer_docs/src/content/docs/admin-api/reports.mdx @@ -165,12 +165,12 @@ A message report captures the reported message together with at most 25 messages | id | snowflake | The ID of the message | | channel_id1 | string | The channel the captured messages were read from | | channel_nsfw2 | ?boolean | The current NSFW state of that channel | -| channel_content_warning_level?3 4 | ?integer | The channel content warning level | +| channel_content_warning_level?3 4 | ?integer | The channel [content warning level](/http-api/channels/#content-warning-levels) | | channel_content_warning_text?3 4 | ?string | The channel content warning text | | guild_id5 | ?snowflake | The guild the report is scoped to, or null when the report names none | | guild_nsfw_level2 | ?integer | The current guild [NSFW level](/http-api/guilds/#nsfw-levels) | | guild_nsfw?3 4 | ?boolean | Whether the guild is marked NSFW | -| guild_content_warning_level?3 4 | ?integer | The guild content warning level | +| guild_content_warning_level?3 4 | ?integer | The guild [content warning level](/http-api/guilds/#guild-content-warning-levels) | | guild_content_warning_text?3 4 | ?string | The guild content warning text | | content | string | The captured message content, empty when the message had none | | timestamp6 | ISO8601 timestamp | When the message was sent | 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 d953473db..fc1d3d73e 100644 --- a/fluxer_docs/src/content/docs/admin-api/search-indexes.mdx +++ b/fluxer_docs/src/content/docs/admin-api/search-indexes.mdx @@ -35,9 +35,7 @@ Both routes require the [ACL](/admin-api/#acl-evaluation) `guild:lookup`. Every instance-wide name other than `discovery` deletes its documents before the first batch is written, so search over that index is incomplete for the whole run. -:::note[A `discovery` rebuild updates documents in place] -The guild documents stay searchable for the whole run. -::: +A `discovery` rebuild instead updates the existing guild documents in place, so those documents stay searchable for the whole run. ## Search index refresh object diff --git a/fluxer_docs/src/content/docs/admin-api/users.mdx b/fluxer_docs/src/content/docs/admin-api/users.mdx index cec4a410a..27bd8b388 100644 --- a/fluxer_docs/src/content/docs/admin-api/users.mdx +++ b/fluxer_docs/src/content/docs/admin-api/users.mdx @@ -6,9 +6,9 @@ description: Reading and editing accounts, including contact fields, flags, bans import RouteHeader from '@/components/RouteHeader.astro'; -These routes read and edit any account on the instance. One account has more here than the public [Users](/http-api/users/) resource returns, including its contact details, its network addresses, and its lifecycle state. +These routes read and edit any account on the instance. An account has more fields here than the public [Users](/http-api/users/) resource returns, including its contact details, its network addresses, and its lifecycle state. -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 consolidated update. +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). @@ -176,7 +176,7 @@ The `flags` field of the [Admin user object](#admin-user-object) is a 64-bit bit ## Suspicious activity flags -A 32-bit bitfield of verification requirements imposed on an account. [Update suspicious activity flags](#update-suspicious-activity-flags) and [Disable user for suspicious activity](#disable-user-for-suspicious-activity) both write the complete value, so a bit the request omits is cleared. +A 32-bit bitfield of verification requirements applied to an account. [Update suspicious activity flags](#update-suspicious-activity-flags) and [Disable user for suspicious activity](#disable-user-for-suspicious-activity) both write the complete value, so a bit the request omits is cleared. | Value | Name | Description | | --- | --- | --- | @@ -196,7 +196,7 @@ A 32-bit bitfield of verification requirements imposed on an account. [Update su 3 Deferrable alongside `REQUIRE_VERIFIED_PHONE`. [Update user phone verification](#update-user-phone-verification) leaves it set -Bit `1 << 16` sits outside this registry. It marks a phone requirement as deferred until the account joins a discoverable or large community, and the [Admin user object](#admin-user-object) reports it as `phone_verification_deferred`. [Update suspicious activity flags](#update-suspicious-activity-flags) and [Disable user for suspicious activity](#disable-user-for-suspicious-activity) bound `flags` only as a non-negative 32-bit integer, and neither masks the submitted value, so a request with that bit sets it directly. +Bit `1 << 16` sits outside this registry. It defers a phone requirement until the account joins a discoverable or large community, and the [Admin user object](#admin-user-object) reports it as `phone_verification_deferred`. [Update suspicious activity flags](#update-suspicious-activity-flags) and [Disable user for suspicious activity](#disable-user-for-suspicious-activity) bound `flags` only as a non-negative 32-bit integer, and neither masks the submitted value, so a request with that bit sets it directly. ## Deletion reasons @@ -250,7 +250,7 @@ The values [Clear user profile fields](#clear-user-profile-fields) accepts in `f | outgoing_request | A friend request the account has sent | | blocked | An account this account has blocked | -The first three categories are mirrored, so removing one also removes the corresponding row on the other account. A block has no mirror row. +The first three categories are mirrored, so removing one also removes the matching row on the other account. A block has no mirror row. ## Admin user session object @@ -395,7 +395,7 @@ One entry for each direct message or group direct message channel the account ha ## Admin user change log object -One recorded change to an identity or contact field. The account holder's own changes are recorded here, and so are [Change user username](#change-user-username) and [Change user email](#change-user-email). +One recorded change to an identity or contact field. The account holder's own changes appear here, and so do [Change user username](#change-user-username) and [Change user email](#change-user-email). ### Structure @@ -453,7 +453,7 @@ One recorded change to an identity or contact field. The account holder's own ch Lists and searches accounts. Requires `user:lookup`, and `user:view:email` or `user:view:ip` for the selectors marked below. The response has an array of [Admin user](#admin-user-object) objects and a total. -Fluxer honours exactly one selector, in this precedence order: `user_id`, `resolve`, `email`, `last_active_ip`, then the indexed `q` search. A lower-precedence parameter sent alongside a higher-precedence one is ignored, and the request still succeeds. A request sending no selector at all runs the indexed search with an empty query. +Fluxer honours exactly one selector, in this precedence order: `user_id`, `resolve`, `email`, `last_active_ip`, then the indexed `q` search. A lower-precedence parameter sent alongside a higher-precedence one is ignored, and the request still succeeds. A request with no selector runs the indexed search with an empty query. ### Query parameters @@ -479,7 +479,7 @@ Fluxer honours exactly one selector, in this precedence order: `user_id`, `resol 6 Honoured by the `last_active_ip` and `q` selectors. The `user_id`, `resolve`, and `email` selectors ignore both -Fluxer chooses the `resolve` lookup from the shape of the value. A value matching `username#discriminator` resolves the tag, and a value that is entirely digits resolves the account ID. A value containing `@` resolves the email address. Every other value resolves a Stripe subscription ID. +Fluxer chooses the `resolve` lookup from the shape of the value. A `username#discriminator` value resolves the tag, a value that is entirely digits resolves the account ID, and a value containing `@` resolves the email address. Every other value resolves a Stripe subscription ID. A `q` value that is entirely digits also resolves that exact account ID and places it first, even when the search index did not match it, provided `offset` is zero. That direct hit raises `total` by one when the index did not already return it. @@ -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 already 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 regardless of what the acting credential holds. `acls` reports the ACL set stored on the account, which for an Admin API key credential can be wider than what the key itself can exercise. +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. ### Response body @@ -571,7 +571,7 @@ An account with a pending or completed deletion is still returned, with its life Replaces the username, allocates or claims a discriminator, and returns the resulting account. Requires `user:update:username`. -A target account may hold a custom discriminator on every self-hosted instance. On any other instance it may hold one when the `feature_custom_discriminator` limit admits it. +A target account may hold a custom discriminator on every self-hosted instance, and on any other instance only when the `feature_custom_discriminator` limit admits it. ### Path parameters @@ -604,7 +604,7 @@ A target account may hold a custom discriminator on every self-hosted instance. | 400 | [error response](/admin-api/#error-response) | Path or body validation fails, or the requested tag is taken and the request returns `TAG_ALREADY_TAKEN` | | 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist | -`TAG_ALREADY_TAKEN` also covers a submitted username with no free discriminator left, and an allocation lock the operation cannot acquire within its 10 second wait. +`TAG_ALREADY_TAKEN` also covers a submitted username with no free discriminator left, and an allocation lock the operation cannot get within its 10 second wait. ### Side effects @@ -638,7 +638,7 @@ The replacement address is stored unverified. Use [Verify user email](#verify-us | --- | --- | --- | | email1 | string | Replacement email address (1-254 characters) | -1 Normalised and validated as an email address. The operation performs no uniqueness check of its own +1 Normalised and validated as an email address, but the operation itself does no uniqueness check ### Response body @@ -720,7 +720,7 @@ An already verified account with no email reverification [suspicious activity fl Nothing is sent when the instance email transport is disabled or when the address is marked hard bounced. Neither case changes the status code, and the audit entry is recorded either way. ::: -A per-address control, independent of the Admin buckets, permits three verification emails for each address in fifteen minutes. Fluxer charges it before creating the token, so a request that exhausts it returns 429 and stores no token. +A per-address control, independent of the Admin buckets, permits three verification emails for each address in fifteen minutes. Fluxer charges it before creating the token, so a request that uses it up returns 429 and stores no token. ### Path parameters @@ -775,7 +775,7 @@ Delivery is silently dropped when the instance email transport is disabled and w 1 A missing address returns `INVALID_FORM_BODY` with the validation code `USER_DOES_NOT_HAVE_AN_EMAIL_ADDRESS` -Unlike [Resend verification email](#resend-verification-email), this operation is subject to no per-address control and does not refuse a bot account. +Unlike [Resend verification email](#resend-verification-email), this operation has no per-address control and does not refuse a bot account. ### Side effects @@ -836,7 +836,7 @@ The change is not written to the contact change log. The operation records one [ Clears the named profile fields and returns the resulting account. Requires `user:update:profile`. -Clearing is the only profile mutation on this resource. No route sets a bio, a display name, an avatar, or a banner to a new value. +Clearing is the only profile change on this resource. No route sets a biography, display name, avatar, or banner to a new value. ### Path parameters @@ -1015,7 +1015,7 @@ The grant bound is evaluated after the account is resolved, so an unknown ID fai ### Side effects -The stored ACL set is replaced and appears in the next [Admin user object](#admin-user-object). Whether the account can reach the Admin API follows from whether the new set holds `admin:authenticate` or `*`. Narrowing the set narrows every existing session and Admin API key of the account without rotating any credential. +The stored ACL set is replaced and appears in the next [Admin user object](#admin-user-object). The account can reach the Admin API exactly when the new set holds `admin:authenticate` or `*`. Narrowing the set narrows every existing session and Admin API key of the account without rotating any credential. [User Update](/gateway/events/#user-update) is emitted to the account's own sessions. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `set_acls`, target type `user`, and a metadata key `acls` with the submitted values joined by commas. @@ -1122,7 +1122,7 @@ The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-obje Adds and removes [premium flags](#premium-flags) and returns the resulting account. Requires `user:update:flags`, the same ACL as [Update user flags](#update-user-flags). -Premium flags control badge presentation, the premium override, the purchase block, and perk sanitisation. They do not grant or revoke a subscription. +Premium flags control badge display, the premium override, the purchase block, and perk sanitisation. They do not grant or revoke a subscription. ### Path parameters @@ -1154,7 +1154,7 @@ Premium flags control badge presentation, the premium override, the purchase blo ### Side effects -The stored premium flag bitfield is replaced with the computed value, and premium badge presentation changes for the account. [User Update](/gateway/events/#user-update) is emitted to the account's own sessions. +The stored premium flag bitfield is replaced with the computed value, and premium badge display changes for the account. [User Update](/gateway/events/#user-update) is emitted to the account's own sessions. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `update_premium_flags`, target type `user`, and the metadata keys `add_flags`, `remove_flags`, and `new_flags`. @@ -1182,7 +1182,7 @@ The user-facing phone verification marker is otherwise irreversible, and this is | --- | --- | --- | | has_verified_phone1 | boolean | Whether the account counts as phone verified | -1 Required. Setting it to true additionally clears the `REQUIRE_VERIFIED_PHONE` and `REQUIRE_INBOUND_PHONE_VERIFICATION` [suspicious activity flags](#suspicious-activity-flags) together with the deferral bit `1 << 16`. Setting it to false clears no flag +1 Required. Setting it to true also clears the `REQUIRE_VERIFIED_PHONE` and `REQUIRE_INBOUND_PHONE_VERIFICATION` [suspicious activity flags](#suspicious-activity-flags) together with the deferral bit `1 << 16`, and setting it to false clears no flag ### Response body @@ -1201,7 +1201,7 @@ The user-facing phone verification marker is otherwise irreversible, and this is ### Side effects -[User Update](/gateway/events/#user-update) is emitted to the account's own sessions. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `update_has_verified_phone`, target type `user`, and a metadata key `has_verified_phone`. When suspicious activity flags were also cleared, the entry additionally has `suspicious_activity_flags_before` and `suspicious_activity_flags_after`. +[User Update](/gateway/events/#user-update) is emitted to the account's own sessions. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `update_has_verified_phone`, target type `user`, and a metadata key `has_verified_phone`. When suspicious activity flags were also cleared, the entry also has `suspicious_activity_flags_before` and `suspicious_activity_flags_after`. ### Rate limit @@ -1213,7 +1213,7 @@ The user-facing phone verification marker is otherwise irreversible, and this is Replaces the account's [suspicious activity flags](#suspicious-activity-flags) and returns the resulting account. Requires `user:update:suspicious_activity`. -The operation imposes verification requirements without disabling the account. [Disable user for suspicious activity](#disable-user-for-suspicious-activity) also locks the account out. +The operation sets verification requirements without disabling the account. [Disable user for suspicious activity](#disable-user-for-suspicious-activity) also locks the account out. ### Path parameters @@ -1227,7 +1227,7 @@ The operation imposes verification requirements without disabling the account. [ | --- | --- | --- | | flags1 | integer | Replacement [suspicious activity flags](#suspicious-activity-flags) | -1 Required. The value replaces the complete stored bitfield, so an omitted bit is cleared and a value of zero imposes no requirement at all +1 Required. The value replaces the complete stored bitfield, so an omitted bit is cleared and a value of zero sets no requirement at all ### Response body @@ -1299,7 +1299,7 @@ Fluxer adds the `DISABLED_SUSPICIOUS_ACTIVITY` [account flag](#account-flags), s The account is marked with `DISABLED_SUSPICIOUS_ACTIVITY`, its suspicious activity flags are replaced, and its password hash is set to null. Every authentication session is then deleted, so the account is signed out on every device. -Fluxer records a `disabled_suspicious` risk outcome, together with a `challenged` outcome when the submitted `flags` is non-zero. The account holder is emailed when the account has an email address. +Fluxer records a `disabled_suspicious` risk outcome, together with a `challenged` outcome when the submitted `flags` is non-zero. Fluxer emails the account holder when the account has an email address. [User Update](/gateway/events/#user-update) is emitted after the sessions have already been deleted, so no connection of the account remains to receive it. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `disable_suspicious_activity`, target type `user`, and a metadata key `flags`. @@ -1332,7 +1332,7 @@ Both modes add the `DISABLED` [account flag](#account-flags) and set `temp_banne 1 Required. The expiry is computed from the moment the ban is applied, so no absolute-expiry field is accepted. The 8760 ceiling is one year -2 Included in the temporary ban email and in the audit entry. It is not stored on the account, so no read operation returns it +2 Included in the temporary ban email and in the audit entry, but not stored on the account, so no read operation returns it ### Response body @@ -1355,11 +1355,11 @@ Every authentication session of the account is deleted. [Unban user](#unban-user `DISABLED` is added to the account flags and `temp_banned_until` is set to the resolved expiry. Every authentication session is then deleted, so the account is signed out on every device. -An authentication attempt while the ban stands fails with 403 `ACCOUNT_SUSPENDED_TEMPORARILY`. An attempt after the expiry has passed clears the disabled state and `temp_banned_until` in the same request, so a temporary ban lifts itself without an Admin operation. +An authentication attempt while the ban stands fails with 403 `ACCOUNT_SUSPENDED_TEMPORARILY`. An attempt after the expiry has passed clears the disabled state and `temp_banned_until` in the same request, so a temporary ban ends without an Admin operation. When the account has an email address and `duration_hours` is greater than zero, the account holder is emailed the duration, the expiry, and the supplied `reason`. A permanent ban sends no email. -[User Update](/gateway/events/#user-update) is emitted after the sessions have already been deleted, so no connection of the account remains to receive it. `DISABLED` is not a public account flag, and no other account observes the change. +[User Update](/gateway/events/#user-update) is emitted after the sessions are deleted, so no connection of the account remains to receive it. `DISABLED` is not a public account flag, and no other account observes the change. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `temp_ban`, target type `user`, and the metadata keys `duration_hours`, `reason`, and `banned_until`. @@ -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 additional 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`, 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. 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. @@ -1508,7 +1508,7 @@ Clearing the deadline permits the account to authenticate again, but it does not `DELETED` and `SELF_DELETED` are both removed from the account flags, and `pending_deletion_at`, `deletion_reason_code`, `deletion_public_reason`, and the private deletion audit reason are cleared. The queued final deletion is withdrawn. -The account holder is emailed when the account has an email address. The email quotes the `X-Audit-Log-Reason` value verbatim and falls back to the literal text `deletion canceled` when the header is absent or resolves to null. +Fluxer emails the account holder when the account has an email address. The email quotes the `X-Audit-Log-Reason` value verbatim and falls back to the literal text `deletion canceled` when the header is absent or resolves to null. [User Update](/gateway/events/#user-update) is emitted to the account's own sessions. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `cancel_deletion`, target type `user`, and no metadata. @@ -1778,7 +1778,7 @@ The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-obje -Lists the authentication sessions of the account, including the tombstones of terminated ones. Requires `user:list:sessions`. IP, reverse DNS, and location fields additionally require `user:view:ip`. +Lists the authentication sessions of the account, including the tombstones of terminated ones. Requires `user:list:sessions`. IP, reverse DNS, and location fields also require `user:view:ip`. :::caution[Listing sessions writes an audit entry] The `X-Audit-Log-Reason` header value is stored on the recorded entry. @@ -1812,7 +1812,7 @@ Without `user:view:ip` the two derived fields are null and `client_ip` is the li ### Side effects -This read does not mutate session state and emits no Gateway Dispatch. It performs outbound reverse DNS and geolocation lookups for each session address when the caller holds `user:view:ip`. +This read does not mutate session state and emits no Gateway Dispatch. It makes outbound reverse DNS and geolocation lookups for each session address when the caller holds `user:view:ip`. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `list_user_sessions`, target type `user`, and a metadata key `session_count` counting active and terminated sessions together. @@ -1884,7 +1884,7 @@ Like [List user sessions](#list-user-sessions), this read records an audit entry | 2001 | array[[WebAuthn credential](/http-api/users/mfa/#webauthn-credential-object) object] | The credentials were returned | | 404 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist | -1 The body is a bare array rather than an object. An account with no credential receives an empty array +1 An account with no credential receives an empty array The complete credential set is returned in one response, bounded by the ten credentials an account may register. No public key, attestation object, or signature counter is exposed. @@ -1916,7 +1916,7 @@ Deletes one passkey or security key from the account. Requires `user:update:mfa` | 204 | empty | The credential was deleted | | 4041 | [error response](/admin-api/#error-response) | `UNKNOWN_USER`, because the account does not exist | -1 A credential that does not exist on the account also answers `UNKNOWN_USER` rather than `UNKNOWN_WEBAUTHN_CREDENTIAL`, so a 404 does not distinguish an unknown account from an unknown credential +1 A credential that does not exist on the account also answers `UNKNOWN_USER`, never `UNKNOWN_WEBAUTHN_CREDENTIAL`, so a 404 does not distinguish an unknown account from an unknown credential :::caution[A deleted passkey cannot be restored] The passkey or security key stops authenticating immediately and cannot be restored from its deleted record. The account holder must register it again to use it later. @@ -1924,7 +1924,7 @@ The passkey or security key stops authenticating immediately and cannot be resto ### Side effects -The credential record is deleted. When it was the account's final WebAuthn credential, Fluxer removes the `WEBAUTHN` [authenticator type](/http-api/users/#authenticator-types) from the account, emits [User Update](/gateway/events/#user-update) to the account's own sessions, and resynchronises the authenticator mirror of every bot the account owns. +The credential record is deleted. When it was the account's final WebAuthn credential, Fluxer removes the `WEBAUTHN` [authenticator type](/http-api/users/#authenticator-types) from the account, emits [User Update](/gateway/events/#user-update) to the account's own sessions, and resyncs the authenticator mirror of every bot the account owns. [WebAuthn Credentials Update](/gateway/events/#webauthn-credentials-update) is emitted to the target account with its remaining credentials. @@ -1959,7 +1959,7 @@ Clearing the authenticator type set removes `WEBAUTHN` from the advertised authe ### Side effects -The account's TOTP secret, authenticator type set, and every multi-factor backup code are deleted. Fluxer resynchronises the authenticator mirror of every bot the account owns. Sessions and credentials are not revoked. +The account's TOTP secret, authenticator type set, and every multi-factor backup code are deleted. Fluxer resyncs the authenticator mirror of every bot the account owns. Sessions and credentials are not revoked. [User Update](/gateway/events/#user-update) is emitted to the account's own sessions. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `disable_mfa`, target type `user`, and no metadata. @@ -2019,9 +2019,9 @@ The queued job is an ordinary shred job, so its progress, totals, skips, and fai ### Side effects -A dry run reads and counts only. A deletion run additionally queues one shred job with every matched message identity, and the request returns before any message is processed. +A dry run reads and counts only. A deletion run also queues one shred job with every matched message identity, and the request returns before any message is processed. -The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with target type `message_deletion`, the target account ID as its target ID, and the metadata keys `user_id`, `channel_count`, `message_count`, and `dry_run`. The action is `delete_all_user_messages_dry_run` for a dry run and `delete_all_user_messages` otherwise. A deletion run additionally records the `queue_message_shred` entry described under [Shred user messages](#shred-user-messages). +The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with target type `message_deletion`, the target account ID as its target ID, and the metadata keys `user_id`, `channel_count`, `message_count`, and `dry_run`. The action is `delete_all_user_messages_dry_run` for a dry run and `delete_all_user_messages` otherwise. A deletion run also records the `queue_message_shred` entry described under [Shred user messages](#shred-user-messages). ### Rate limit @@ -2153,7 +2153,7 @@ Archive progress, download, and expiry are documented under [Archives](/admin-ap ### Side effects -Fluxer creates the archive record and queues a build job, so the response returns before any data is collected. Generating the archive reads the account's data without mutating it. +Fluxer creates the archive record and queues a build job, so the response returns before any data is collected. Generating the archive reads the account's data without changing it. No Gateway Dispatch is emitted. @@ -2165,7 +2165,7 @@ No Gateway Dispatch is emitted. -Lists the identity and contact field changes recorded for the account, newest first. Requires `user:view:contact_log`. Email values additionally require `user:view:email`. +Lists the identity and contact field changes recorded for the account, newest first. Requires `user:view:contact_log`. Email values also require `user:view:email`. ### Path parameters @@ -2201,7 +2201,7 @@ Lists the identity and contact field changes recorded for the account, newest fi 1 This operation does not resolve the account, so it never answers `UNKNOWN_USER`. An ID with no matching account returns an empty page -Without `user:view:email`, an `email` entry is still returned, with both value fields replaced by the literal string `[redacted]`, so the fact and the time of the change remain visible while the addresses do not. +Without `user:view:email`, an `email` entry is still returned with both value fields replaced by the literal string `[redacted]`, so the fact and the time of the change stay visible while the addresses do not. ### Rate limit diff --git a/fluxer_docs/src/content/docs/admin-api/voice.mdx b/fluxer_docs/src/content/docs/admin-api/voice.mdx index cc13a3f3b..7ff7390a4 100644 --- a/fluxer_docs/src/content/docs/admin-api/voice.mdx +++ b/fluxer_docs/src/content/docs/admin-api/voice.mdx @@ -15,7 +15,7 @@ Every write takes effect once each API node reloads its topology. No write moves ::: :::caution[Each write runs in three separate steps] -A write stores the record, notifies every node to reload, then records the audit entry. A failure at the second or third step answers 500 with the record already stored, so read the record back before retrying. +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. ::: ## Media transport @@ -24,7 +24,7 @@ A write stores the record, notifies every node to reload, then records the audit `endpoint` is that deployment's signalling URL, a `ws://` or `wss://` address. Fluxer hands it to the placed session verbatim as the `endpoint` of [Voice Server Update](/gateway/events/#voice-server-update), and the client opens its media connection there. The API rewrites the same value to `http://` or `https://` for its own room service calls, and it keeps any path prefix. -`api_key` and `api_secret` mint the access token the placed session presents. The token is valid for 600 seconds, grants room admission to exactly one room, and has the track sources the member's permissions allow. +`api_key` and `api_secret` mint the 600-second grant the placed session presents, as [Voice](/voice/#media-transport) describes. The grant admits the session to exactly one room and has the track sources the member's permissions allow. A room is one voice channel. A guild channel uses the room name `guild_{guild_id}_channel_{channel_id}` and a private call uses `dm_channel_{channel_id}`. @@ -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 and then a server inside it, so a caller admitted to a region whose servers all refuse it has no access to that region. +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. `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. -`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, so a restricted region or server is never selectable for a [call](/http-api/calls/#modify-call-region) or a group direct message. +`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 consulted. 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 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. ## Admin voice region object @@ -57,14 +57,14 @@ The operator supplies `id` on creation, and it is the primary key. A channel sto | longitude | number | The longitude placement measures distance from, in decimal degrees | | 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 suffices (max 100) | -| allowed_guild_ids23 | array[snowflake] | The guilds admitted without consulting the other two guild gates (max 1000) | +| 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_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 | | servers?4 | array[[Admin voice server](#admin-voice-server-object) object] | The servers registered in the region | -1 The flag is not exclusive, and setting it on a second region does not clear it on the first. Each node then treats the first flagged region its reload enumerates as the default, and a topology with the flag on no region falls back to the first region its reload enumerates +1 The flag is not exclusive, and setting it on a second region does not clear it on the first. Each node then takes the first flagged region its reload lists as the default, or the first region its reload lists when no region is flagged 2 Setting any of these three makes the region unusable for a private call @@ -97,7 +97,7 @@ A server record names one LiveKit deployment, the API key pair the instance auth The `region_id` and `server_id` pair addresses one server, and a server belongs to exactly one region. The same server identifier can exist in two regions. Moving a server between regions means deleting it and recreating it. Both identifiers are operator-chosen strings of 1 to 64 characters, and no operation renames either one. -A server can have its own coordinate. When it does, placement measures distance from that coordinate to pick the closest server for an automatically placed session. When it does not, the server takes no part in distance comparison. +A server can have its own coordinate. Placement then measures distance from that coordinate to pick the closest server for an automatically placed session. A server without one takes no part in distance comparison. ### Structure @@ -110,13 +110,13 @@ A server can have its own coordinate. When it does, placement measures distance | longitude2 | ?number | The longitude replacing this server's region coordinate, in decimal degrees, or null when the region coordinate is used | | is_active3 | boolean | Whether the server is in rotation for new placement | | 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 suffices (max 100) | -| allowed_guild_ids4 | array[snowflake] | The guilds admitted without consulting the other two guild gates (max 1000) | +| 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_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 | -1 An instance can configure an internal URL that the API uses for its own room service calls to one designated server, and that URL changes neither the stored value nor the value the session receives +1 An instance can configure an internal URL that the API uses for its own room service calls to one designated server. That URL changes neither the stored value nor the value the session receives 2 The two coordinates are set and cleared together, and a server with only one of them cannot be stored @@ -211,7 +211,7 @@ Returns one [Admin voice region](#admin-voice-region-object) object. Requires `v | Status | Body | Condition | | --- | --- | --- | -| 200 | response body | A lookup was performed, whether or not it resolved | +| 200 | response body | A lookup was done, whether or not it resolved | ### Rate limit @@ -235,7 +235,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 consulting the other two guild gates (max 1000, default empty) | +| allowed_guild_ids? | array[snowflake] | The guilds admitted without checking the other two 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 @@ -258,7 +258,7 @@ Stores a region and returns it. Requires `voice:region:create`. ### Side effects -Each node reloads its topology after the write. One [Admin audit entry](/admin-api/#admin-audit-entry-object) with the action `create_voice_region` and the target type `voice_region` records the region identifier and its name in metadata, together with the audit reason. +Each node reloads its topology after the write. One [Admin audit entry](/admin-api/#admin-audit-entry-object) with the action `create_voice_region` and the target type `voice_region` records the region identifier and its name in metadata. ### Rate limit @@ -278,7 +278,7 @@ Applies a partial update to a region and returns the updated record. Requires `v ### JSON body -Every field is optional and an omitted field is left unchanged. An absent, empty, or whitespace-only body is read as an empty object, which validates and updates nothing but `updated_at`. A body that does not parse as JSON returns 400 `INVALID_FORM_BODY` with one element at path `body` and code `INVALID_FORMAT`. +Every field is optional and an omitted field is left unchanged. An absent, empty, or whitespace-only body is read as an empty object, which validates and updates nothing but `updated_at`. A body that does not parse as JSON returns 400 `INVALID_FORM_BODY` with one element at path `body` and code `INVALID_FORMAT`, as [Errors](/http-api/errors/#validation-failure-codes) describes. | Field | Type | Description | | --- | --- | --- | @@ -289,12 +289,12 @@ 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 consulting the other two guild gates (max 1000) | +| allowed_guild_ids?1 | array[snowflake] | The guilds admitted without checking the other two 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 -The body has no identifier. An `id` member in the body is overwritten from the path and never renames the region. +An `id` member in the body is overwritten from the path and never renames the region. ### Response body @@ -313,7 +313,7 @@ The body has no identifier. An `id` member in the body is overwritten from the p ### Side effects -The write sets `updated_at` to the time it ran, and each node reloads its topology. One [Admin audit entry](/admin-api/#admin-audit-entry-object) with the action `update_voice_region` and the target type `voice_region` records the region identifier in metadata, together with the audit reason. The entry has no field diff. +The write sets `updated_at` to the time it ran, and each node reloads its topology. One [Admin audit entry](/admin-api/#admin-audit-entry-object) with the action `update_voice_region` and the target type `voice_region` records the region identifier in metadata. The entry has no field diff. ### Rate limit @@ -332,7 +332,7 @@ Deletes a region and every server registered in it. Requires `voice:region:delet | region_id | string | The ID of the region (1-64 characters) | :::danger[Deleting a region deletes every server in it] -The region record and every server record stored under it are removed in one batch. There is no confirmation step and no way to recover the server records, including their stored credentials, so read the servers back first if they have to be recreated. +The region record and every server record stored under it are removed in one batch. There is no confirmation step. The server records and their stored credentials cannot be recovered, so read the servers back first if they have to be recreated. ::: ### Response body @@ -354,7 +354,7 @@ The region record and every server record stored under it are removed in one bat Each node reloads its topology once for the whole batch, and an operation that has to reach a deleted server afterwards fails. Sessions already placed in the region are not disconnected by the deletion itself. -One [Admin audit entry](/admin-api/#admin-audit-entry-object) with the action `delete_voice_region` and the target type `voice_region` records the region identifier and its name in metadata, together with the audit reason. No `delete_voice_server` entry is written for the servers deleted with the region. +One [Admin audit entry](/admin-api/#admin-audit-entry-object) with the action `delete_voice_region` and the target type `voice_region` records the region identifier and its name in metadata. No `delete_voice_server` entry is written for the servers deleted with the region. ### Rate limit @@ -415,7 +415,7 @@ Returns one [Admin voice server](#admin-voice-server-object) object. Requires `v | Status | Body | Condition | | --- | --- | --- | -| 200 | response body | A lookup was performed, whether or not it resolved | +| 200 | response body | A lookup was done, whether or not it resolved | ### Rate limit @@ -446,12 +446,12 @@ Registers a voice server in a region and returns it. Requires `voice:server:crea | is_active? | boolean | Whether the server is in rotation for new placement (default true) | | 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 consulting the other two guild gates (max 1000, default empty) | +| allowed_guild_ids? | array[snowflake] | The guilds admitted without checking the other two 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 -2 The value has to parse as a URL. Validation does not constrain the scheme, and the client receives the value unchanged +2 The value has to parse as a URL. Validation does not limit the scheme, and the client receives the value unchanged 3 The two coordinates are supplied together and are either both null or both a number. A mismatched pair fails body validation on the `latitude` path @@ -475,7 +475,7 @@ The body has no `region_id`. A `region_id` member in the body is overwritten fro ### Side effects -Each node reloads its topology after the write. One [Admin audit entry](/admin-api/#admin-audit-entry-object) with the action `create_voice_server` and the target type `voice_server` records the region identifier, the server identifier, and the endpoint in metadata, together with the audit reason. Neither credential is recorded. +Each node reloads its topology after the write. One [Admin audit entry](/admin-api/#admin-audit-entry-object) with the action `create_voice_server` and the target type `voice_server` records the region identifier, the server identifier, and the endpoint in metadata. Neither credential is recorded. ### Rate limit @@ -496,7 +496,7 @@ Applies a partial update to a voice server and returns the updated record. Requi ### JSON body -Every field is optional and an omitted field is left unchanged. An absent, empty, or whitespace-only body is read as an empty object, which validates and updates nothing but `updated_at`. A body that does not parse as JSON returns 400 `INVALID_FORM_BODY` with one element at path `body` and code `INVALID_FORMAT`. +Every field is optional and an omitted field is left unchanged. An absent, empty, or whitespace-only body is read as an empty object, which validates and updates nothing but `updated_at`. A body that does not parse as JSON returns 400 `INVALID_FORM_BODY` with one element at path `body` and code `INVALID_FORMAT`, as [Errors](/http-api/errors/#validation-failure-codes) describes. | Field | Type | Description | | --- | --- | --- | @@ -508,7 +508,7 @@ Every field is optional and an omitted field is left unchanged. An absent, empty | is_active? | boolean | Whether the server is in rotation for new placement | | 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 consulting the other two guild gates (max 1000) | +| allowed_guild_ids?3 | array[snowflake] | The guilds admitted without checking the other two 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 @@ -517,7 +517,7 @@ Every field is optional and an omitted field is left unchanged. An absent, empty 3 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 -The body has no identifiers. A `region_id` or `server_id` member in the body is overwritten from the path and cannot move the server to another region. +A `region_id` or `server_id` member in the body is overwritten from the path and cannot move the server to another region. ### Response body @@ -536,7 +536,7 @@ The body has no identifiers. A `region_id` or `server_id` member in the body is The write sets `updated_at` to the time it ran, and each node reloads its topology and uses the stored credentials for later calls to the server. Setting `is_active` to false stops new placement and does not move sessions already on the server. -One [Admin audit entry](/admin-api/#admin-audit-entry-object) with the action `update_voice_server` and the target type `voice_server` records the region identifier and the server identifier in metadata, together with the audit reason. The entry records neither credential and no field diff, so a credential rotation is indistinguishable in the audit log from any other update. +One [Admin audit entry](/admin-api/#admin-audit-entry-object) with the action `update_voice_server` and the target type `voice_server` records the region identifier and the server identifier in metadata. The entry records neither credential and no field diff, so a credential rotation is indistinguishable in the audit log from any other update. ### Rate limit @@ -572,9 +572,9 @@ Deletes a voice server. Requires `voice:server:delete`. ### Side effects -Each node reloads its topology after the removal, and server-side moderation of a session still on that server fails afterwards. Sessions already placed on the server are not disconnected by the deletion itself. The stored credentials are removed with the record and are not recoverable. +Each node reloads its topology after the removal, and server-side moderation of a session still on that server then fails. Sessions already placed on the server are not disconnected by the deletion itself. The stored credentials are removed with the record and are not recoverable. -One [Admin audit entry](/admin-api/#admin-audit-entry-object) with the action `delete_voice_server` and the target type `voice_server` records the region identifier, the server identifier, and the endpoint the server held in metadata, together with the audit reason. +One [Admin audit entry](/admin-api/#admin-audit-entry-object) with the action `delete_voice_server` and the target type `voice_server` records the region identifier, the server identifier, and the endpoint the server held in metadata. ### Rate limit diff --git a/fluxer_docs/src/content/docs/authentication.md b/fluxer_docs/src/content/docs/authentication.md index a1b742830..b186a2e36 100644 --- a/fluxer_docs/src/content/docs/authentication.md +++ b/fluxer_docs/src/content/docs/authentication.md @@ -20,7 +20,7 @@ An `Authorization` credential MUST NOT be copied into a URL. Webhook tokens, sig ## Authorization header -The header value must have no leading or trailing whitespace, and a padded value never authenticates. A value beginning with `Bot `, `Bearer `, or `Admin ` selects that scheme, and the remainder must be non-empty and must have no surrounding whitespace either. The prefixes match exactly, so any other spelling is not recognised as a scheme. +The header value must have no leading or trailing whitespace, and a padded value never authenticates. A value beginning with `Bot `, `Bearer `, or `Admin ` selects that scheme, and the rest must be non-empty and must have no surrounding whitespace either. The prefixes match exactly, so any other spelling is not recognised as a scheme. A value containing no space is parsed as a bare user session token. A value containing a space without a recognised scheme prefix is invalid. @@ -76,7 +76,7 @@ Authorization: Admin fa_1508923117441703936_KaqkNax1BF3YSWHGkEPjDRKeO48jGb9F 1 The token is opaque and has no client-readable claims -2 The identifier before the full stop selects which application record to check, and the secret after it is the only part that authorises the request +2 The identifier before the full stop selects which application record to check, and only the secret after it authorises the request 3 A key whose identifier segment is not a decimal integer is invalid @@ -87,12 +87,12 @@ A bot token, an Admin API key, and a client secret cannot be read back after the ::: :::note[Rotation invalidates the previous value immediately] -Rotation applies to a bot token and a client secret, and rotating a bot token also terminates every Gateway session the bot holds. An Admin API key is not rotated. It is revoked and replaced. +Rotation applies to a bot token and a client secret, and rotating a bot token also ends every Gateway session the bot holds. An Admin API key is not rotated. It is revoked and replaced. ::: ## User session tokens -A user session token authenticates an ordinary user account. It is issued by the login, registration, and session exchange operations documented in [Authentication](/http-api/authentication/). A token that does not identify a live session leaves the request unauthenticated. The Gateway accepts a user session token in [Identify](/gateway/commands/#identify). +A user session token authenticates an ordinary user account. The login, registration, and session exchange operations in [Authentication](/http-api/authentication/) issue it. A token that does not identify a live session leaves the request unauthenticated. The Gateway accepts a user session token in [Identify](/gateway/commands/#identify). The `Authorization` header holds a single credential. A [sudo mode](#sudo-mode) proof travels separately, in the `X-Fluxer-Sudo-Mode-JWT` header, and it proves that the already resolved account recently re-verified. @@ -170,7 +170,7 @@ While enforcement is active, an operation that uses a locally held credential re The single sign-on callback returns the same code when the provider claims match no existing account and the instance does not auto-provision. -Enforcement applies at those operations only. It does not gate password change or multi-factor management on an already authenticated account, and enabling it leaves an already issued session token, bot token, OAuth2 access token, or Admin API key valid. +Enforcement applies at those operations only, and does not gate password change or multi-factor management on an already authenticated account. Enabling it leaves an already issued session token, bot token, OAuth2 access token, or Admin API key valid. ## Account state gates @@ -209,7 +209,7 @@ Sudo mode is a short-lived proof that the account holder recently re-verified a A sudo proof is an HS256 JSON Web Token with the account ID as its subject, the fixed claim `type` set to `sudo`, an issue time, and an expiry five minutes after issue. A client presents it in the `X-Fluxer-Sudo-Mode-JWT` request header. An invalid, expired, or account-mismatched token produces the same response as a missing one. -Fluxer mints a token only for an account holding a multi-factor authenticator, so a password-only account re-verifies for each operation that requires sudo mode. [Create WebAuthn registration options](/http-api/users/mfa/#create-webauthn-registration-options) and [Disable current account](/http-api/users/current-user/#disable-current-account) mint no token and return no header even for a multi-factor account. A bot account satisfies sudo mode immediately. So does an account that has neither a password nor a multi-factor authenticator. +Fluxer issues a token only for an account holding a multi-factor authenticator, so a password-only account re-verifies for each operation that requires sudo mode. [Create WebAuthn registration options](/http-api/users/mfa/#create-webauthn-registration-options) and [Disable current account](/http-api/users/current-user/#disable-current-account) issue no token and return no header even for a multi-factor account. A bot account satisfies sudo mode immediately. So does an account that has neither a password nor a multi-factor authenticator. :::note[A sudo proof covers every account session] The check covers only the signature, the `type` claim, the subject, and the expiry. Revoking the session that obtained a proof leaves that proof valid. diff --git a/fluxer_docs/src/content/docs/conventions.md b/fluxer_docs/src/content/docs/conventions.md index 32d9efd8f..d5f81b916 100644 --- a/fluxer_docs/src/content/docs/conventions.md +++ b/fluxer_docs/src/content/docs/conventions.md @@ -4,7 +4,7 @@ title: API conventions description: Normative keywords, protocol subjects, wire table notation, and endpoint entry structure. --- -This page defines the notation every reference page uses: the wire tables, the type names, the footnote markers, the endpoint entry layout, and what an omitted field means. An operation that states a different rule and names the difference overrides anything here. A code example illustrates the contract and never overrides prose, a wire table, a registry, or a state-transition table. +This page defines the notation every reference page uses. An operation that states a different rule and names the difference overrides anything here. A code example shows the contract and never overrides prose, a wire table, a registry, or a state-transition table. ## Normative language @@ -32,7 +32,7 @@ Each subject below has the same meaning on every reference page. A Dispatch is one Gateway server-to-client event, and a command is one client-to-server message. The reference writes ordinary user or non-bot user where a rule excludes bots. -Several of these words have an unrelated second sense. An authentication session is the stored login record defined by [Authentication](/authentication/), a voice server is the registered media machine defined by [Admin Voice](/admin-api/voice/), and a guild administrator is a member holding guild permissions. +Several of these words have an unrelated second sense. An authentication session is the stored login record defined by [Authentication](/authentication/). A voice server is the registered media machine defined by [Admin Voice](/admin-api/voice/). A guild administrator is a member holding guild permissions. ## Wire table notation @@ -51,15 +51,15 @@ The `Type` column uses this notation. | `integer string`, `base64 string` | That representation in a JSON string | | `binary`, `file` | A multipart file part | -The description of a duration field names its unit. +A duration field's description names its unit. The type of a union lists its alternatives separated by a vertical bar, written as `type | type` in a table cell. A field that accepts a small fixed set of literal values lists those exact wire values in the same form, as in `emoji | sticker`. -A superscript marker such as 1 refers to the numbered footnote written beneath its table or paragraph. Numbering restarts in every table. A footnote records a presence condition, gate, bound, or computed value that does not fit in a description cell. +A superscript marker such as 1 refers to the numbered footnote written below its table or paragraph. Numbering restarts in every table. A footnote records a presence condition, gate, bound, or computed value that does not fit in a description cell. Bitfield tables and enumeration tables with symbolic names share the `Value`, `Name`, and `Description` columns. A table is a bitfield when every non-zero value cell holds a shift expression of the form `1 << n`. An enumeration whose values have no symbolic name uses `Value` and `Description` alone. -A registry is closed when its page states that it is complete, gives an exact count, or states that a value outside it is rejected. A value absent from a closed registry is unsupported even when its wire type could represent it. +A registry is closed when its page states it is complete, gives an exact count, or states that a value outside it is rejected. A value absent from a closed registry is unsupported even when its wire type could represent it. A state-transition table uses `Event and condition`, `Action`, and `Next state` columns. Its first cell names an event one state accepts and then the condition that selects this outcome. A state accepts an event only when it appears in that state's table or in a table the section declares for every open state. @@ -78,7 +78,7 @@ Where an operation departs from either default, it states the departure in the f } ``` -On a response, the page that owns a field states what an absent field means. That is commonly that the operation did not populate it, that the object variant does not own it, or that its value has been cleared. Each of those is distinct from a present field whose value is `null`. +On a response, the page that owns a field states what an absent field means. That is commonly that the operation did not fill it, that the object variant does not own it, or that its value has been cleared. An absent field is not the same as a present field whose value is `null`. ## Endpoint entries @@ -109,7 +109,7 @@ Prose then states the contract. The subsections that apply follow it in this ord 10. `Side effects`. 11. `Rate limit`, which states the bucket the operation draws on. -A subsection that does not apply is omitted. An object that a page defines has its own field table under `Structure`. +Subsections that do not apply are omitted. An object that a page defines has its own field table under `Structure`. A response table uses `Status`, `Body`, and `Condition` columns. A `Body` cell uses the same type notation as a wire table, names `empty` where the response has no body, and names `response body` where the preceding subsection defines it. A response table has no header column. The shared contract is defined once under [standard response headers](/http-api/#standard-response-headers), and a header an operation sets for itself is stated in prose under the operation. @@ -132,16 +132,16 @@ An array bound counts elements. A string bound names its unit, written as charac ## Examples and notes -A `json` example shows one valid or representative wire value, and a `text` example shows an expression or a string form. Every identifier, token, hash, and host in an example is fabricated, and an example host uses the reserved `example.com` domain. +A `json` example shows one valid or representative wire value, and a `text` example shows an expression or a string form. Every identifier, token, hash, and host in an example is made up, and an example host uses the reserved `example.com` domain. -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 elides content the example does not need to show. Where inline code quotes an exact response body, the brackets are part of the literal value. +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, which 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 three are binding. ## Independent protocol surfaces -The HTTP API and the Gateway each own an error registry, and the Media Proxy API owns none because its failures have no code. The Gateway alone defines opcodes and close codes. +The HTTP API and the Gateway each own an error registry. The Media Proxy API owns none because its failures have no code. The Gateway alone defines opcodes and close codes. The Admin API is a privileged namespace inside the HTTP API. It shares the `/v1` prefix, the request and response framing, the error envelope, and the standard response headers. It adds its own credential policy, ACL registry, audit contract, and rate limit registry. -An identifier other than a [snowflake](/snowflakes/) reaches a second surface only where that surface names it, as the Media Proxy API does for the upload capability and the asset hash the HTTP API issues. +An identifier other than a [snowflake](/snowflakes/) reaches a second surface only where that surface names it. The Media Proxy API does that for the upload capability and the asset hash the HTTP API issues. diff --git a/fluxer_docs/src/content/docs/gateway/commands.md b/fluxer_docs/src/content/docs/gateway/commands.md index 60ab694d3..f6d9c517a 100644 --- a/fluxer_docs/src/content/docs/gateway/commands.md +++ b/fluxer_docs/src/content/docs/gateway/commands.md @@ -26,17 +26,19 @@ Except for [Heartbeat](#heartbeat), [Identify](#identify), and [Resume](#resume) A frame that fails the size, decompression, or decoding checks closes the connection without consuming any budget. A frame that decodes to something other than an object closes with `4002` and reason `Decode failed`, and an object with no `op` closes with `4002` and reason `Invalid payload`. -Fluxer charges every command that gets past those checks against the [source IP, session, and connection payload budgets](/gateway/limits-and-rate-limits/#connection-and-command-rate-limits) before it handles the opcode. Each session gets its own session budget, so two sessions of one account never share one, and Fluxer skips that budget while the connection is unauthenticated. +Fluxer charges every command that gets past those checks against the [source IP, session, and connection payload budgets](/gateway/limits-and-rate-limits/#connection-and-command-rate-limits) before it handles the opcode. Each session has its own session budget, so two sessions of one account never share one, and Fluxer skips that budget while the connection is unauthenticated. An opcode outside the registry closes with `4001` and reason `Unknown opcode` once a session is attached, and with `4003` while the connection is unauthenticated. A payload that has `op` but no `d` also closes with `4001`, except for Identify, which closes with `4005`. +A Boolean command field is set only by `true`, and by the string `"true"` where noted. Every other value resolves to false, except in the Lazy Request [guild subscription object](#guild-subscription-object), which states its own rule. + :::note[Most command payloads are permissive] Identify, Resume, and Presence Update reject a malformed payload with a close. The [bounded query commands](#bounded-requests) coerce or discard whatever they cannot parse, so a wrong field type usually produces an empty result. ::: ## Heartbeat -Opcode `1` has the last Dispatch sequence the client processed. Use `null` before the first Dispatch. +Opcode `1` has the last Dispatch sequence the client processed, or `null` before the first Dispatch. | Field | Type | Description | | --- | --- | --- | @@ -106,8 +108,8 @@ Opcode `2` authenticates and creates a new session. The following faults close with `4002` and reason `Invalid identify payload`: - A missing `token` or `properties`. -- A `properties` value that is not an object. -- A `properties` object whose `os`, `browser`, or `device` is missing or is not a string. +- `properties` that is not an object. +- `properties` whose `os`, `browser`, or `device` is missing or is not a string. - An `ignored_events` value that is not an array of strings, or one that holds more than 256 entries. - A `flags` value that is not a non-negative integer. @@ -121,11 +123,11 @@ A token the backend rejects closes with `4004` and reason `Invalid token`. A non | Value | Name | Description | | --- | --- | --- | -| 1 << 1 | DEBOUNCE_MESSAGE_REACTIONS | Coalesce runs of reaction additions into [Message Reaction Add Many](/gateway/events/#message-reaction-add-many) | +| 1 << 1 | DEBOUNCE_MESSAGE_REACTIONS | Merge runs of reaction additions into [Message Reaction Add Many](/gateway/events/#message-reaction-add-many) | Bit `0` and every bit above `1` are undefined. An undefined bit is accepted and ignored without closing the connection. -`DEBOUNCE_MESSAGE_REACTIONS` applies to a reaction in a direct message or group direct message. A reaction in a guild channel is never coalesced and arrives as its own [Message Reaction Add](/gateway/events/#message-reaction-add). +`DEBOUNCE_MESSAGE_REACTIONS` applies to a reaction in a direct message or group direct message. A reaction in a guild channel is never merged and arrives as its own [Message Reaction Add](/gateway/events/#message-reaction-add). ### Identify properties object @@ -141,7 +143,7 @@ Bit `0` and every bit above `1` are undefined. An undefined bit is accepted and | latitude?3 | string | The client latitude as a decimal string of 1 through 32 characters | | longitude?3 | string | The client longitude as a decimal string of 1 through 32 characters | -1 Only the exact value `true` sets it. A session without it is refused from an end-to-end encrypted voice channel with `VOICE_E2EE_REQUIRED` +1 A session without it is refused from an end-to-end encrypted voice channel with `VOICE_E2EE_REQUIRED` 2 Read only when `presence` is absent or null, in which case it decides the session's mobile flag. Otherwise the [initial presence object](#initial-presence-object) decides it @@ -149,7 +151,7 @@ Bit `0` and every bit above `1` are undefined. An undefined bit is accepted and `os`, `browser`, and `device` are required strings. The remaining fields are optional hints. Fluxer accepts and ignores unrecognised properties. -`latitude` and `longitude` are accepted here only as strings. A number fails validation and the whole session start fails, so send `"52.52"`. Both must be sent together to have any effect, and a string that does not parse as a number counts as absent. Omit them when the client has no location, and Ready orders `rtc_regions` by region ID instead. +`latitude` and `longitude` are accepted here only as strings. A number fails validation and the whole session start fails, so send `"52.52"`. Both must be sent together to have any effect, and a string that does not parse as a number counts as absent. A client with no location omits them, and Ready orders `rtc_regions` by region ID instead. ### Initial presence object @@ -158,15 +160,13 @@ Bit `0` and every bit above `1` are undefined. An undefined bit is accepted and | Field | Type | Description | | --- | --- | --- | | status?1 | string | The initial status, accepting `online`, `idle`, `dnd`, `invisible`, or `offline` | -| afk?2 | boolean | Whether the session is away (default false) | -| mobile?2 | boolean | Whether the presence is mobile (default false) | -| custom_status?3 | ?[custom status](#custom-status-object) object | The custom status this session publishes | +| afk? | boolean | Whether the session is away (default false) | +| mobile? | boolean | Whether the presence is mobile (default false) | +| custom_status?2 | ?[custom status](#custom-status-object) object | The custom status this session publishes | -1 The account's saved status wins when `status` is absent, when it is null or any other non-string, when it is the string `unknown`, and when it is `online` while the saved status is not `online`. The empty string resolves to `online`, and the remaining accepted values are used as sent +1 The account's saved status is used when `status` is absent, when it is null or any other non-string, when it is the string `unknown`, and when it is `online` while the saved status is not `online`. The empty string resolves to `online`, and the remaining accepted values are used as sent -2 Only the exact value `true` sets the flag. Every other value resolves to false - -3 Read only when the account has no saved custom status, and stored as sent with no validation +2 Read only when the account has no saved custom status, and stored as sent with no validation Identify accepts `offline` as a distinct initial status, and [Presence Update](#presence-update) normalises `offline` to `invisible`. A session whose resolved status is `offline` or `invisible` publishes `status: "offline"` and a null `custom_status` to other users. @@ -187,7 +187,7 @@ Identify accepts `offline` as a distinct initial status, and [Presence Update](# 3 A single Unicode emoji. Fluxer strips the field before validation when `emoji_id` is supplied -Only these fields are read. [Presence Update](#presence-update) validates the object against the account before publishing it. [Identify](#identify) does not validate it, and reads it only when the account has no saved custom status. +Only these fields are read. [Presence Update](#presence-update) validates the object against the account before publishing it. The published presence adds `emoji_animated` to this object. @@ -207,7 +207,7 @@ An unknown or expired session produces Opcode `9` with `d: false` and leaves the A successful Resume replays every retained Dispatch strictly above `seq` in order and finishes with [Resumed](/gateway/events/#resumed). It also replaces the session's socket, and the displaced socket receives Opcode `7` followed by a close. -Fluxer processes Resume in any authentication state. A socket that already has a session attached still processes one, and the named session takes the attached session's place. Send Resume only on a fresh socket. +Fluxer processes Resume in any authentication state. A socket that already has a session attached still processes a Resume, and the named session takes the attached session's place. Send Resume only on a fresh socket. ## Presence Update @@ -216,13 +216,11 @@ Opcode `3` replaces the current session presence. | Field | Type | Description | | --- | --- | --- | | status | string | The status to publish, accepting `online`, `idle`, `dnd`, `invisible`, or `offline` | -| afk?1 | boolean | Whether the session is away (default false) | -| mobile?1 | boolean | Whether the session is mobile (default false) | -| custom_status?2 | ?[custom status](#custom-status-object) object | The custom status that replaces the current one | +| afk? | boolean | Whether the session is away (default false) | +| mobile? | boolean | Whether the session is mobile (default false) | +| custom_status?1 | ?[custom status](#custom-status-object) object | The custom status that replaces the current one | -1 Only the exact value `true` sets the flag. Every other value resolves to false - -2 An object identical to the current one in `text`, `expires_at`, `emoji_id`, and `emoji_name` is reused without revalidation +1 An object identical to the current one in `text`, `expires_at`, `emoji_id`, and `emoji_name` is reused without revalidation ```json { @@ -240,7 +238,7 @@ Opcode `3` replaces the current session presence. `status` is required. A payload that is not an object, an object with no `status` key, and a `status` string outside the accepted set all close with `4002` and reason `Invalid presence payload`. The empty string resolves to `online`. -Fluxer accepts a `status` that is not a string. Null and a Boolean publish the session as offline, and every other non-string value resolves to `online`. +Fluxer accepts a non-string `status`. Null and a Boolean publish the session as offline, and every other non-string value resolves to `online`. `offline` is normalised to `invisible`, so a Presence Update cannot publish a session as offline while it is connected. @@ -248,7 +246,7 @@ Fluxer accepts a `status` that is not a string. Null and a Boolean publish the s The published presence has a [custom status](#custom-status-object) object and no activities. -Presence Update has a dedicated limit of five accepted commands per 20 seconds on one WebSocket. A further update inside that window is discarded without closing the connection, after it has consumed the shared payload budgets. +Presence Update has its own limit of five accepted commands per 20 seconds on one WebSocket. A further update inside that window is discarded without closing the connection, after it has consumed the [shared payload budgets](/gateway/limits-and-rate-limits/#connection-and-command-rate-limits). ## Voice State Update @@ -279,7 +277,7 @@ Opcode `4` joins, moves, updates, or leaves the voice membership associated with 4 A non-negative integer. Fluxer treats every other value as absent, which disables the staleness check -5 Only `true` and the string `"true"` set it, and Fluxer publishes `false` when the member lacks `STREAM` in the channel. The screenshare track rides this same connection, so setting the flag mints no grant and sends no [Voice Server Update](/gateway/events/#voice-server-update) +5 Only `true` and the string `"true"` set it, and Fluxer publishes `false` when the member lacks `STREAM` in the channel. The screenshare track uses this same connection, so setting the flag issues no grant and sends no [Voice Server Update](/gateway/events/#voice-server-update) ```json { @@ -311,7 +309,7 @@ When a guild update has `mutation_id`, Fluxer also reports the outcome to the re `base_version` is a staleness check for an update that names an existing guild connection. An update whose `base_version` is more than one behind that connection's current voice state version is rejected with `stale_base_version`. The check runs after the member, channel, and connection lookups and before the permission checks. It does not apply to opening a new connection, to leaving a channel, or to the DM and group DM call context. -The first two updates in a rolling one-second window are processed immediately. Later updates enter a per-session queue that holds at most 64 commands and drains one command every 500 ms. A newer update replaces an older queued update for the same `guild_id` and `connection_id` pair, and a full queue discards its oldest entry before accepting the new one. +The first two updates in a rolling one-second window are processed immediately. Later updates enter a [per-session queue](/gateway/limits-and-rate-limits/#connection-and-command-rate-limits) that holds at most 64 commands and drains one command every 500 ms. A newer update replaces an older queued update for the same `guild_id` and `connection_id` pair, and a full queue discards its oldest entry before accepting the new one. ## Request Guild Members @@ -327,7 +325,7 @@ Opcode `8` requests bounded member chunks. | presences?5 | boolean | Whether results include presences (default false) | | nonce? | string | A value of at most 32 bytes echoed in each chunk | -1 A non-empty `guild_ids` array wins. `guild_id` is read only when `guild_ids` is absent or empty +1 A non-empty `guild_ids` array is used. `guild_id` is read only when `guild_ids` is absent or empty 2 Matched case-insensitively as a prefix of the member's display name, which is the guild nickname, then the global name, then the username @@ -335,7 +333,7 @@ Opcode `8` requests bounded member chunks. 4 A non-empty `user_ids` array selects those members directly and ignores `query` and `limit` -5 Only the exact value `true` sets it. Presences whose status is `offline` or `invisible` are omitted from the result +5 Presences whose status is `offline` or `invisible` are omitted from the result ```json { @@ -356,12 +354,12 @@ The command never closes the connection. Invalid input is coerced or discarded: - Duplicate guild IDs are collapsed. - A `user_ids` array longer than 100 entries abandons the whole request. Individual entries that are not positive Snowflakes are dropped. - A `limit` that is not a non-negative integer becomes `0`, and a larger value is clamped to `100`. -- A `query` that is not a string becomes the empty string. +- Any non-string `query` becomes the empty string. - A `nonce` that is not a string of at most 32 bytes becomes null. The nonce is echoed only when the request named exactly one guild. -Fluxer skips a guild the session is not currently connected to. A request that resolves to no connected guild produces no chunk. +Fluxer skips a guild this session is not connected to. A request that resolves to no connected guild produces no chunk. A bot requests one guild at a time, and a bot request naming two or more guilds is abandoned. @@ -414,7 +412,7 @@ Opcode `14` sets the per-guild subscriptions that decide member list, typing, an | member_list_channels?2 | map[snowflake, array[array[integer]]] | The member list windows to subscribe to, keyed by channel ID | | members? | array[snowflake] | The explicit member IDs to subscribe to, at most 1,000 | -1 Both are Booleans when present. Any other value abandons the rest of the command silently, without a close and without a result +1 Both are Booleans when present. Any other value drops the rest of the command silently, without a close and without a result 2 A coalesced subscription waits 100 ms before Fluxer applies it, and ranges arriving inside that window merge into the ranges already buffered for the same channel. A request that has no ranges for a channel discards the ranges already buffered for it @@ -426,7 +424,7 @@ Marking a guild active changes how much traffic it produces, and [Event filterin Fluxer applies a subscription at once when the guild has no coalescing window open, its buffer is empty, and the channel's member list is already built. That request opens the window. Fluxer buffers it as applied and does not apply it a second time when the window closes. Every other subscription waits out the window, including one for a channel whose member list is not built yet and one arriving while the window is open. -`VIEW_CHANNEL` and `VIEW_CHANNEL_MEMBERS` together govern the member list subscription, and both are evaluated for each channel separately. A channel the session cannot view, or can view without holding `VIEW_CHANNEL_MEMBERS` there, receives no [Guild Member List Update](/gateway/events/#guild-member-list-update) while its siblings subscribe normally. +`VIEW_CHANNEL` and `VIEW_CHANNEL_MEMBERS` together control the member list subscription, and both are evaluated for each channel separately. A channel the session cannot view, or can view without holding `VIEW_CHANNEL_MEMBERS` there, receives no [Guild Member List Update](/gateway/events/#guild-member-list-update) while its siblings subscribe normally. Subscribing a channel to at least one range drops the session's other member list subscriptions in that guild, so one session holds at most one member list per guild. @@ -487,6 +485,6 @@ Results arrive in one [Channel Member Counts Update](/gateway/events/#channel-me ## Bounded requests -One WebSocket processes at most four bounded requests at once across Request Guild Members, Lazy Request, Request Guild Counts, and Request Channel Member Counts. A command that arrives when all four slots are taken is dropped, without a close and without a result. Each request has a 10,000 ms deadline, after which it produces no further events. +One WebSocket processes at most four bounded requests at once across [Request Guild Members](#request-guild-members), [Lazy Request](#lazy-request), [Request Guild Counts](#request-guild-counts), and [Request Channel Member Counts](#request-channel-member-counts). A command that arrives when all four slots are taken is dropped, without a close and without a result. Each request has a 10,000 ms deadline, after which it produces no further events. -Request Guild Members also keeps one replaceable pending request while another member request is active, whether or not a slot is free. +Request Guild Members also keeps one replaceable pending request while another member request is active, whether or not a slot is free. See [Bounded commands](/gateway/limits-and-rate-limits/#bounded-commands). diff --git a/fluxer_docs/src/content/docs/gateway/event-filtering.md b/fluxer_docs/src/content/docs/gateway/event-filtering.md index 32a5a7df8..35df09113 100644 --- a/fluxer_docs/src/content/docs/gateway/event-filtering.md +++ b/fluxer_docs/src/content/docs/gateway/event-filtering.md @@ -39,7 +39,7 @@ The guild resolves each event to one of these recipient sets. 2 The channel is read from the payload's `channel_id`, and from a nested `channel.id` when that field is absent. An invite payload with neither field reaches no session -Every one of those sets also excludes a session whose connection to the guild is still in flight, so a session receives none of the events above until the guild has given it that initial state. +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. @@ -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. -The session holds a presence back in two cases. Every presence that arrives before [Ready](/gateway/events/#ready) is held. Fluxer 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 buffers 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 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 bot session holds no friend or group direct message presence subscriptions, so a bot receives a presence through this guild path alone. @@ -137,7 +137,7 @@ A session that identified with a `shard` pair whose `shard_id` is not 0 drops ev Account-level traffic, direct message traffic, relationship changes, and calls therefore never reach a session on a shard other than 0. -A session on shard 0, and a session that identified without a `shard` pair, filter nothing at this gate. Fluxer still applies guild ownership at Identify, as [Sharding](/gateway/overview/#sharding) describes, so a shard 0 session is only ever connected to the guilds its shard owns. +Sessions on shard 0, and sessions that identified without a `shard` pair, filter nothing at this gate. Fluxer still applies guild ownership at Identify, as [Sharding](/gateway/overview/#sharding) describes, so a shard 0 session is only ever connected to the guilds its shard owns. ## What a bot should send diff --git a/fluxer_docs/src/content/docs/gateway/events.md b/fluxer_docs/src/content/docs/gateway/events.md index e86496868..e33c553d9 100644 --- a/fluxer_docs/src/content/docs/gateway/events.md +++ b/fluxer_docs/src/content/docs/gateway/events.md @@ -4,7 +4,7 @@ title: Gateway events description: Every main Gateway Dispatch event, its payload, its delivery scope, and its replay behaviour. --- -A Dispatch is a message from the [Gateway](/gateway/overview/). It tells a client that something happened, such as a new message arriving or a member joining a guild. Every Dispatch has [Opcode](/gateway/opcodes-and-close-codes/#opcodes) 0, the name of the event, and the event's data. [Event filtering](/gateway/event-filtering/) defines the gates each one passes on its way to a socket. +A Dispatch is a message from the [Gateway](/gateway/overview/) that tells a client something happened, such as a new message arriving or a member joining a guild. Every Dispatch has [Opcode](/gateway/opcodes-and-close-codes/#opcodes) 0, the name of the event, and the event's data. [Event filtering](/gateway/event-filtering/) defines the gates each one passes on its way to a socket. ## Dispatch envelope @@ -84,7 +84,7 @@ A Dispatch is buffered for [Resume](/gateway/commands/#resume) replay unless it | [Guild Member Update](#guild-member-update) | A member's guild state or public user representation changes | Guild connection | | [Guild Member Remove](#guild-member-remove) | A user stops being a member of a connected guild | Guild connection | | [Guild Members Chunk](#guild-members-chunk) | A bounded member result answers Request Guild Members | Command response | -| [Guild Member List Update](#guild-member-list-update) | A subscribed member list resynchronises the subscriber's ranges | Member list subscription | +| [Guild Member List Update](#guild-member-list-update) | A subscribed member list resyncs the subscriber's ranges | Member list subscription | | [Guild Audit Log Entry Create](#guild-audit-log-entry-create) | An audit log entry is written in a guild | Holders of `VIEW_AUDIT_LOG` | | [Guild Ban Add](#guild-ban-add) | A guild ban is created | Guild connection | | [Guild Ban Remove](#guild-ban-remove) | A guild ban is removed | Guild connection | @@ -108,7 +108,7 @@ A Dispatch is buffered for [Resume](/gateway/commands/#resume) replay unless it | [Voice State Ack](#voice-state-ack) | The session's own voice mutation is applied or rejected | Current session | | [Voice Server Update](#voice-server-update) | The session receives or replaces its own voice grant | Current session | | [Entrance Sound Play](#entrance-sound-play) | A participant's entrance sound plays in a voice channel | Voice channel | -| [Call Create](#call-create) | A DM or group DM call begins or becomes visible | Call recipient | +| [Call Create](#call-create) | A private channel call begins or becomes visible | Call recipient | | [Call Update](#call-update) | The ringing set, participant roster, or region of a call changes | Call recipient | | [Call Delete](#call-delete) | A call ends or becomes unavailable | Call recipient | | [Guild Counts Update](#guild-counts-update) | Member and online counts are returned for connected guilds | Command response | @@ -234,7 +234,7 @@ The payload is otherwise empty. Resumed has the session's current sequence in `s ### SESSIONS_REPLACE -The account's set of live sessions changed. The payload is a bare JSON array of [session presence objects](#session-presence-object) and replaces the client's copy in full. [Ready](#ready) sends the initial set as `sessions`. +The account's set of live sessions changed. The payload, a bare JSON array of [session presence objects](#session-presence-object), replaces the client's copy in full. [Ready](#ready) sends the initial set as `sessions`. ### AUTH_SESSION_CHANGE @@ -244,7 +244,7 @@ The account's authentication session was rotated, for example by a password chan | --- | --- | --- | | old_auth_session_id_hash | string | Base64url hash of the authentication session that was replaced | | new_auth_session_id_hash | string | Base64url hash of the replacement authentication session | -| new_token | string | The token that replaces the one the client currently holds | +| new_token | string | Replacement for the token the client holds | Every session of the account receives the event, including the one that caused the rotation. A client MUST use `new_token` for every later HTTP request and for any later [Resume](/gateway/commands/#resume) or [Identify](/gateway/commands/#identify). A client whose own `auth_session_id_hash` from [Ready](#ready) equals `old_auth_session_id_hash` MUST replace it with `new_auth_session_id_hash`. @@ -260,7 +260,7 @@ A [Request Guild Members](/gateway/commands/#request-guild-members) command was `meta` has `guild_id` and, when the request named exactly one guild and supplied a valid nonce, `nonce`. -That budget admits one unfiltered member request per bot account and guild every 30,000 ms, and `retry_after` is the remainder of that window expressed in seconds. Every other command refusal is silent. +That budget admits one unfiltered member request per bot account and guild every 30,000 ms, and `retry_after` is the remainder of that window in seconds. Every other command refusal is silent. ### USER_UPDATE @@ -276,7 +276,7 @@ Fluxer republishes the account's presence on every settings update, whether or n ### USER_GUILD_SETTINGS_UPDATE -One guild's notification settings changed. The payload is the complete user guild settings object for that guild. +One guild's notification settings changed. The payload is that guild's complete user guild settings object. ### USER_NOTE_UPDATE @@ -387,7 +387,7 @@ A user session receives Guild Create when a guild becomes available after Ready, A session that asked for a sync through [Lazy Request](/gateway/commands/#lazy-request) receives a replacement snapshot of the guild. The payload is a [guild ready object](#guild-ready-object) and has the same replacement semantics as [Guild Create](#guild-create). -Fluxer sends a sync when the subscription flips the guild between active and passive, and when `sync: true` names a guild the session has not already synced. A second `sync: true` for an already-synced guild sends nothing. +Fluxer sends a sync when the subscription switches the guild between active and passive, and when `sync: true` names a guild the session has not already synced. A second `sync: true` for an already-synced guild sends nothing. ### GUILD_UPDATE @@ -584,7 +584,7 @@ This event is delivered live and is never retained for [Resume](/gateway/command ### GUILD_MEMBER_LIST_UPDATE -A member list the session subscribed to through [Lazy Request](/gateway/commands/#lazy-request) resynchronises the subscriber's ranges. +A member list the session subscribed to through [Lazy Request](/gateway/commands/#lazy-request) resyncs the subscriber's ranges. | Field | Type | Description | | --- | --- | --- | @@ -605,7 +605,7 @@ A member list the session subscribed to through [Lazy Request](/gateway/commands 1 Hoisted role groups come first in role order, then `online`, then `offline` -A group whose count is `0` is omitted. The `offline` group is also omitted once it holds more than 1,000 members, and in that case the offline members are omitted from `items` as well. `member_count` can then exceed the number of items a client can ever read back. +A group whose count is `0` is omitted. The `offline` group is also omitted once it holds more than 1,000 members, and in that case `items` omits the offline members too. `member_count` can then exceed the number of items a client can ever read back. #### Member list operation object @@ -626,7 +626,7 @@ Each item has exactly one of the two fields. | group? | [member list group object](#member-list-group-object) | A group header occupying one list position | | member?1 | [guild member](/http-api/guild-members/#guild-member-object) object | A member of the list | -1 Extended with a `presence` field that always exists. It is the guild's [presence object](#presence-object) for that member when the member is visibly online to the guild, and otherwise the placeholder `{"status": "offline", "mobile": false, "afk": false}` +1 Extended with a `presence` field that always exists. The value is the guild's [presence object](#presence-object) for that member when the member is visibly online to the guild, and otherwise the placeholder `{"status": "offline", "mobile": false, "afk": false}` ### GUILD_AUDIT_LOG_ENTRY_CREATE @@ -634,7 +634,7 @@ An audit log entry was written. The payload has the shape of a [guild audit log The `ip` change key is stripped from `changes`, so an entry whose only change was `ip` has no `changes` at all. A client MUST treat an absent `options` or `changes` as an empty set. -Recipients are every session in the guild that holds `VIEW_AUDIT_LOG`, including the session that performed the action. +Recipients are every session in the guild that holds `VIEW_AUDIT_LOG`, including the acting session. ### GUILD_BAN_ADD @@ -679,11 +679,11 @@ After the burst a Dispatch is held again when the session cannot place it. The s | custom_status | ?[custom status object](#custom-status-object) | The user's custom status | | guild_id? | snowflake | Guild context, present when the presence arrived through a guild | -An account's published `status` is the highest-precedence status across its live sessions, resolved in the order `dnd`, `online`, `idle`, `invisible`, and finally `offline` when no session is live. A session that selected `invisible`, and an account with no live session, both publish `status: "offline"`. A session that lost its transport is published as `offline` 5,000 ms later, even though it stays resumable for the rest of its 60,000 ms retention window, and a successful [Resume](/gateway/commands/#resume) republishes the status it last selected. +An account's published `status` is the highest-precedence status across its live sessions, resolved in the order `dnd`, `online`, `idle`, `invisible`, and finally `offline` when no session is live. A session that selected `invisible`, and an account with no live session, both publish `status: "offline"`. A session that lost its transport is published as `offline` 5,000 ms later, even though it stays resumable for the rest of its 60,000 ms retention window. A successful [Resume](/gateway/commands/#resume) republishes the status it last selected. -`mobile` is true only when the account's resolved status is `online` and at least one online session declared itself mobile. `afk` is false whenever `mobile` is true, and otherwise true only when every live session is away. +`mobile` is true only when the account's resolved status is `online` and at least one online session declared itself mobile. `afk` is false whenever `mobile` is true. Otherwise it is true only when every live session is away. -`custom_status` is suppressed to `null` whenever the published status is `offline`, so an invisible account never leaks one. +`custom_status` is suppressed to `null` whenever the published status is `offline`, so an invisible account never reveals one. #### Custom status object @@ -738,11 +738,11 @@ A visible message was created. The payload is the complete [message object](/htt 1 The `user` field is removed from it, and the account is in the message's `author` -Message Create alone overrides both the passive filter and the `ignored_events` list, and the two use different tests. The passive filter is defeated by a direct mention, a mention of one of the user's roles, an everyone mention, or a here mention. The `ignored_events` list is defeated by a direct, everyone, or here mention alone. +Message Create alone overrides both the passive filter and the `ignored_events` list, and the two use different tests. A direct mention, a mention of one of the user's roles, an everyone mention, or a here mention overrides the passive filter. A direct, everyone, or here mention alone overrides the `ignored_events` list. ### MESSAGE_UPDATE -A visible message changed. The payload is the complete current [message object](/http-api/messages/#message-object). In a guild channel it is extended with `guild_id` and with `member`, the author's guild member object with its `user` field removed. It has no `channel_type`, `nicks`, or `mention_here`. +A visible message changed. The payload is the complete current [message object](/http-api/messages/#message-object), with no `channel_type`, `nicks`, or `mention_here`. In a guild channel it is extended with `guild_id` and with `member`, the author's guild member object with its `user` field removed. Recipients must hold `READ_MESSAGE_HISTORY` on the channel, or the message must be newer than the guild's message history cutoff. @@ -759,7 +759,7 @@ One visible message was deleted. | guild_id? | snowflake | Guild the channel belongs to | | member?2 | [guild member](/http-api/guild-members/#guild-member-object) object | The author's guild member object, present in a guild channel | -1 Both fields are omitted when the deletion came from moderation tooling, and `author_id` is also omitted for a message with no author +1 Both fields are omitted when the deletion came from moderation tools, and `author_id` is also omitted for a message with no author 2 The `user` field is removed from it, and the whole field is absent when `author_id` is absent or the author is no longer a member @@ -798,7 +798,7 @@ A user added a reaction to a message. | guild_id? | snowflake | Guild the channel belongs to | | member? | [guild member](/http-api/guild-members/#guild-member-object) object | The reacting user's guild member object, present in a guild channel | -In a guild channel the session named by the request's `session_id` is excluded and that field is removed from the payload. In a direct message or group direct message the field is delivered as `session_id` and excludes nobody, so a client MUST tolerate receiving its own reaction back. +In a guild channel the session named by the request's `session_id` is excluded and that field is removed from the payload. In a private channel the field is delivered as `session_id` and excludes nobody, so a client MUST tolerate receiving its own reaction back. #### Reaction emoji object @@ -814,16 +814,16 @@ Neither `id` nor `animated` is ever null. A Unicode reaction omits both, so a cl ### MESSAGE_REACTION_ADD_MANY -A session that set the `DEBOUNCE_MESSAGE_REACTIONS` [session flag](/gateway/commands/#session-flags) coalesces a run of reaction additions in a direct message or group direct message into one Dispatch. A reaction in a guild channel is never coalesced and arrives as its own [Message Reaction Add](#message-reaction-add). The session opens a 650 ms window on the first addition and sends the coalesced Dispatch when the window closes. The window holds at most 512 additions and drops the oldest beyond that. +A session that set the `DEBOUNCE_MESSAGE_REACTIONS` [session flag](/gateway/commands/#session-flags) merges a run of reaction additions in a private channel into one Dispatch. A reaction in a guild channel is never merged and arrives as its own [Message Reaction Add](#message-reaction-add). The session opens a 650 ms window on the first addition and sends the merged Dispatch when the window closes. The window holds at most 512 additions and drops the oldest beyond that. | Field | Type | Description | | --- | --- | --- | | channel_id1 | snowflake | Channel the message is in | | message_id1 | snowflake | Message that was reacted to | | guild_id?1 | snowflake | Guild the channel belongs to | -| reactions | array[[reaction addition object](#reaction-addition-object)] | The coalesced additions, in arrival order | +| reactions | array[[reaction addition object](#reaction-addition-object)] | The merged additions, in arrival order | -1 Taken from the first addition of the group. The window is per session, and the session groups the additions by guild, channel, and message when the window closes. Each group is one Dispatch, so every addition in `reactions` is on the message these fields name +1 Taken from the first addition of the group. The window is per session and groups its additions by guild, channel, and message when it closes, so each group is one Dispatch and every addition in `reactions` is on the message these fields name When the window closes holding exactly one addition, the session sends [Message Reaction Add](#message-reaction-add) instead. A session without the flag receives one Message Reaction Add per addition. @@ -1047,14 +1047,14 @@ Recipients are every other account with a voice state in that channel, one Dispa ### CALL_CREATE -A direct message or group direct message call began, or became visible in the session's initial state. +A private channel call began, or became visible in the session's initial state. | Field | Type | Description | | --- | --- | --- | | channel_id | snowflake | Channel the call is in | | message_id | snowflake | Call message that opened the call | | region | ?string | Voice region serving the call, null until one is chosen | -| ringing | array[snowflake] | Recipients currently being rung | +| ringing | array[snowflake] | Recipients being rung | | voice_states1 | array[[voice state object](#voice-state-object)] | Participants, ordered by participant ID | | recipients?2 | array[snowflake] | Every recipient of the channel | | created_at?2 | integer | Unix milliseconds when the call was opened | @@ -1107,7 +1107,7 @@ A guild that is not connected, or that missed its deadline, has no entry in `cou | member_count | integer | Total members | | online_count1 | integer | Online members visible to the requesting account | -1 Counts only the online members that share at least one channel the requesting account can view. An account holding `ADMINISTRATOR` receives the guild's whole online count instead, and an account that can view no channel at all receives `1` when it is itself online and `0` when it is not +1 Counts only the online members that share at least one channel the requesting account can view. An account holding `ADMINISTRATOR` receives the guild's whole online count instead, and an account that can view no channel receives `1` when it is itself online and `0` when it is not ### CHANNEL_MEMBER_COUNTS_UPDATE @@ -1133,6 +1133,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 hoists 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. +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. 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 57e2fbc52..6b80a0084 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 @@ -29,7 +29,7 @@ One Gateway node admits 512 concurrent session starts by default, which an opera `session_rollout_mode` decides which share. The default `modulo` hashes the account ID, so one account is admitted or refused consistently at a given percentage. The alternative `random` draws once per admission attempt, and the percentage is a share of session starts. One account can be admitted on one attempt and refused on the next. :::note[A refused session start is held and retried] -The Gateway refuses a session start for draining, capacity, paused starts, the rollout percentage, or a failed backend RPC. It keeps the Identify payload and tries again after a jittered 1,000 ms to 1,999 ms delay until it succeeds. A rollout config change retries a held payload immediately. +The Gateway refuses a session start for draining, capacity, paused starts, the rollout percentage, or a failed backend RPC. It keeps the Identify payload and retries after a jittered 1,000 ms to 1,999 ms delay until it succeeds. A rollout config change retries it immediately. ::: ## Session start limit @@ -40,15 +40,15 @@ The Gateway refuses a session start for draining, capacity, paused starts, the r A Gateway node running with `FLUXER_DISABLE_RATE_LIMITS` set to `1`, `true`, or `TRUE` disables nine budgets together: -- The connection payload budget -- The session payload budget -- The source IP payload budget -- The source IP connection ceiling -- The Presence Update budget -- The Voice State Update queue -- The source IP Identify budget -- The per-user session count -- The 30-second complete member list budget +- Connection payload budget +- Session payload budget +- Source IP payload budget +- Source IP connection ceiling +- Presence Update budget +- Voice State Update queue +- Source IP Identify budget +- Per-user session count +- 30-second complete member list budget The figures below are the enforced defaults. @@ -82,19 +82,19 @@ Resume history retains at most 4,096 Dispatch events and 16,777,216 bytes of ret One Dispatch larger than 2,097,152 bytes, the 2 MiB single-event ceiling, is delivered to the socket and never retained. The connection is not closed for it, so a client that resumes across such an event does not receive it again. -Both byte bounds measure the in-memory size of the event inside the session process. That size differs from the JSON payload on the wire, so both figures are approximate. +Both byte bounds measure in-memory size, not wire bytes, so both figures are approximate. [Guild Members Chunk](/gateway/events/#guild-members-chunk) is delivered live and is never retained for Resume, whatever its size. -[Guild Sync](/gateway/events/#guild-sync) and [Guild Member List Update](/gateway/events/#guild-member-list-update) are delivered with a sequence and never retained. Those two and Guild Members Chunk are the only events excluded by name. Every other Dispatch is retained, including the pre-encoded guild fan-out that broadcasts one event to every eligible session. A replay is therefore a subset of the sequence range it covers, which is why replay frames have sequence gaps. +[Guild Sync](/gateway/events/#guild-sync) and [Guild Member List Update](/gateway/events/#guild-member-list-update) are delivered with a sequence and never retained. Those two and Guild Members Chunk are the only events excluded by name. Every other Dispatch is retained, including the pre-encoded guild fan-out that broadcasts one event to every eligible session. A replay is therefore a subset of the sequence range it covers, so replay frames have sequence gaps. -A heartbeat with a sequence discards every retained Dispatch at or below that sequence and records it as the acknowledged sequence, so a client MUST acknowledge only a sequence whose events it has finished processing. A heartbeat with `null`, and a heartbeat with a sequence below the acknowledged sequence, change nothing. Resume neither acknowledges nor evicts. A client that never heartbeats with a sequence keeps its full window until the count or byte bound evicts from the front. +A heartbeat with a sequence discards every retained Dispatch at or below that sequence and records it as the acknowledged sequence. A client MUST acknowledge only a sequence whose events it has finished processing. A heartbeat with `null`, and a heartbeat with a sequence below the acknowledged sequence, change nothing. Resume neither acknowledges nor evicts. A client that never heartbeats with a sequence keeps its full window until the count or byte bound evicts from the front. Resume closes with `4007` on a `seq` below the acknowledged sequence, and on a `seq` above the session's current sequence. An eviction from the count or byte bound raises a replay floor to the highest sequence it dropped. A Resume with a `seq` below that floor produces Opcode `9` with `d: false` and no close, so a client that reconnects long after it fell behind Identifies again. -The session holds every Presence Update Dispatch in a pending queue until it dispatches [Ready](/gateway/events/#ready). After Ready, a Dispatch is held only when it names a guild the session is not connected to, or a user who is neither a friend nor a group DM recipient. The queue holds at most 2,048 entries and discards its oldest entry when full. The session releases the queue right after Ready and discards every held entry for a user the `presences` array already covers. A 10,000 ms timer releases it when Ready has not been dispatched by then. A held entry is also released for one user when a relationship or a channel brings that user into scope. +Each session holds at most 2,048 entries in its presence hold queue and discards the oldest when full. [Presence Update](/gateway/events/#presence-update) states when the queue is held and released. Fluxer drops a presence cast to a session process whose mailbox already holds more than 5,000 messages. A session that cannot keep up sheds presence casts and stays connected. @@ -112,6 +112,6 @@ Identify accepts at most 256 `ignored_events` entries. A longer array closes wit Voice admission follows the enclosing guild, DM, group DM, channel, and permission rules. A refusal is reported as [Voice State Ack](/gateway/events/#voice-state-ack) with an `error_code` and closes nothing. A Voice State Update that has no `mutation_id` produces no event when it is refused. -A guild voice channel admits at most `user_limit` users, where `0` means unlimited. A channel in which any participant has a camera enabled additionally admits at most 25 users in total, whatever its `user_limit`. A channel that already holds 25 users with cameras enabled refuses a further camera with `VOICE_CAMERA_USER_LIMIT`. +A guild voice channel admits at most `user_limit` users, where `0` means unlimited. A channel in which any participant has a camera enabled also admits at most 25 users in total, whatever its `user_limit`. A channel that already holds 25 users with cameras enabled refuses a further camera with `VOICE_CAMERA_USER_LIMIT`. [Capacity](/voice/#capacity) states these bounds in full. One user holds at most `voice_connection_limit` simultaneous voice connections in one guild voice channel. The field is part of the [channel object](/http-api/channels/#channel-object), defaults to 5, and is accepted from 1 through 100. Pending connections that have not yet expired count against it. 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 f62e3ef4e..cbd7fbd2c 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 @@ -73,7 +73,7 @@ Fluxer resolves an inbound payload in this order. 5. With a session attached, Presence Update, Voice State Update, Request Guild Members, Lazy Request, Request Guild Counts, and Request Channel Member Counts are handled. Every other opcode, including a server opcode and an undefined value, closes with `4001`. :::note[Unknown server opcodes are forward compatible] -A client that receives an unknown opcode SHOULD log it and ignore the frame. It MUST NOT close or reconnect solely because the server used an opcode newer than this registry. +A client SHOULD log an unknown opcode and ignore the frame, and MUST NOT close or reconnect solely because the server used an opcode newer than this registry. ::: ## Close codes @@ -188,4 +188,4 @@ A held or discarded Identify, an over-budget Presence Update, a dropped bounded ## Ordinary WebSocket closes -A transport can end with no Fluxer application close code, as happens on a network failure, an intermediary reset, and an ordinary `1000` or `1001` close. An established session remains available for 60,000 ms after the transport ends. A later transport end starts a new 60,000 ms window, and neither the window length nor the bounded replay history grows. +A transport can end with no Fluxer application close code, as happens on a network failure, an intermediary reset, and an ordinary `1000` or `1001` close. An established session remains available for 60,000 ms after the transport ends, and a later transport end starts a new 60,000 ms window. Neither the window length nor the bounded replay history grows. diff --git a/fluxer_docs/src/content/docs/gateway/overview.md b/fluxer_docs/src/content/docs/gateway/overview.md index 67ca080a1..fc6b2917d 100644 --- a/fluxer_docs/src/content/docs/gateway/overview.md +++ b/fluxer_docs/src/content/docs/gateway/overview.md @@ -34,7 +34,7 @@ A client opens the socket, waits for Hello, sends Identify, and then heartbeats } ``` -`token` and `properties` are the only required fields. The token is the raw account or bot token. It has no HTTP authentication prefix, so a bot sends the token without the `Bot ` prefix the HTTP API requires. [Client commands](/gateway/commands/#identify) defines the rest. Everything the server sends after Ready is a [Dispatch](/gateway/events/#dispatch-delivery), which is one event payload with its name in `t` and its data in `d`. +`token` and `properties` are the only required fields. The token is the raw account or bot token, with no HTTP authentication prefix, so a bot sends it without the `Bot ` prefix the HTTP API requires. [Client commands](/gateway/commands/#identify) defines the rest. Everything the server sends after Ready is a [Dispatch](/gateway/events/#dispatch-delivery), which is one event payload with its name in `t` and its data in `d`. ## Protocol version @@ -104,7 +104,7 @@ Snowflakes are decimal strings. See [Snowflakes](/snowflakes/) for the identifie `zstd-stream` is a continuous stream in both directions. A client MUST feed every server frame to the same decompressor in arrival order and produce every client frame from the same compressor. -The server compresses at level 3, and one WebSocket message has exactly one Gateway payload. +The server compresses at level 3. One WebSocket message has exactly one Gateway payload. Hello is already compressed on a connection that negotiated `zstd-stream`, so the first frame such a connection receives is a binary frame. @@ -179,7 +179,7 @@ A connection moves through five states: Opening, Unauthenticated, Starting, Repl | Heartbeat. No session is attached, or the payload is `null`, or the attached session accepts the sequence | Send Heartbeat ACK | Same state | | Heartbeat deadline. The connection is awaiting an acknowledgement and more than 45,000 ms have passed since the last one | Close with `4009` and reason `Heartbeat timeout` | Closed | | Invalid frame or payload. Size, decompression, or decoding validation fails | Close with the applicable close code | Closed | -| Transport terminates. A session exists | Retain the session for 60,000 ms | Closed | +| Transport ends. A session exists | Retain the session for 60,000 ms | Closed | An opcode outside the registry, and a server opcode sent by a client, close with `4001` once a session is attached and with `4003` while the connection is unauthenticated. @@ -196,7 +196,7 @@ The server sends Opcode 10 Hello while accepting the WebSocket. } ``` -The advertised interval is in milliseconds and is authoritative for the connection. +The interval is in milliseconds and is authoritative for the connection. Opcode 2 Identify creates a session. A successful Identify sends [Ready](/gateway/events/#ready), whose `session_id` identifies the retained session. Fluxer publishes no separate resume URL, so a Resume reconnects to the same Gateway endpoint the client discovered. @@ -227,9 +227,9 @@ The Gateway also runs its own timer, which ticks every 13,750 ms. On the first t A client MUST answer the server's Opcode 1 with its own Opcode 1. -The Gateway resets the elapsed time and clears the awaiting state when it accepts a client Heartbeat. Its own Opcode 1 does neither, so the deadline keeps running from the last client Heartbeat. +An accepted client Heartbeat resets the elapsed time and clears the awaiting state. The server's own Opcode 1 does neither, so the deadline keeps running from the last client Heartbeat. -A heartbeat with a sequence permanently trims every retained Dispatch at or below that sequence from the replay buffer and records it as the acknowledged sequence. A client MUST send the sequence it has actually processed, because a later Resume from a lower sequence closes with `4007`. +A heartbeat with a sequence permanently trims every retained Dispatch at or below that sequence from the replay buffer and records it as the acknowledged sequence. A client MUST send the sequence it has processed, because a later Resume from a lower sequence closes with `4007`. ## Resuming a session @@ -297,7 +297,7 @@ A guild belongs to `((guild_id >> 22) % shard_count)`, computed on the integer v The pair selects the session's guild membership. At Identify, Fluxer filters the account's guild list to the guilds the shard owns, and the session connects only to those. -For a user session, the filtered set is also the [Ready](/gateway/events/#ready) `guilds` array. For a bot session, it is the guild burst of [Guild Create](/gateway/events/#guild-create) and [Guild Delete](/gateway/events/#guild-delete) Dispatches that follows Ready. Ready echoes the accepted pair back as `shard`. +For a user session, the filtered set is also the [Ready](/gateway/events/#ready) `guilds` array. For a bot session, it is the guild burst of [Guild Create](/gateway/events/#guild-create) and [Guild Delete](/gateway/events/#guild-delete) Dispatches after Ready. Ready echoes the accepted pair back as `shard`. Fluxer checks only a bot session against the guild ceiling. A bot whose shard owns more than 2,500 guilds closes with `4011` and reason `Sharding required`. A bot that supplies no pair is checked against its whole guild list. A user session is bounded by the 100-session-per-user limit alone, whatever its guild count. diff --git a/fluxer_docs/src/content/docs/http-api/applications.mdx b/fluxer_docs/src/content/docs/http-api/applications.mdx index d8c101c87..d54147445 100644 --- a/fluxer_docs/src/content/docs/http-api/applications.mdx +++ b/fluxer_docs/src/content/docs/http-api/applications.mdx @@ -15,7 +15,7 @@ Every route except [Get public application](#get-public-application) requires a 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. :::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 only by [Create application](#create-application) and [Reset bot token](#reset-bot-token). Both are stored as one-way hashes. +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. ::: ## Rate limits @@ -79,7 +79,7 @@ The bot account an application owns. [Create application](#create-application) c 2 An animated hash retains its `a_` prefix, which is the animation indicator for this object. A bot account always holds the animated avatar and animated banner entitlements, so neither hash is ever stripped or suppressed -3 Present only in the response to [Create application](#create-application). [Reset bot token](#reset-bot-token) returns the new token in the enclosing [bot token reset](#bot-token-reset-object) object instead +3 Present only in the response to [Create application](#create-application). [Reset bot token](#reset-bot-token) returns the new token in the parent [bot token reset](#bot-token-reset-object) object instead 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 @@ -89,7 +89,7 @@ The bot account an application owns. [Create application](#create-application) c A bot token is the application ID, a full stop, and an opaque secret of 32 random bytes encoded as base64url. ::: -The API identifies the application from the token itself and rejects an invalid secret without revealing whether the application exists. The built-in `Fluxer Admin` application owns no bot account, so a token bearing its ID is always rejected. +Fluxer identifies the application from the token itself and rejects an invalid secret without saying whether the application exists. The built-in `Fluxer Admin` application owns no bot account, so a token bearing its ID is always rejected. ## Public application object @@ -115,7 +115,7 @@ The application as any caller sees it, including a caller that presents no crede 3 Reported as true for the application owner even when the bot is not public -4 Null when the application has no bot account. Its `token`, `mfa_enabled`, and `authenticator_types` are never populated here +4 Null when the application has no bot account. Its `token`, `mfa_enabled`, and `authenticator_types` are never set here 5 Declared optional by the schema and always emitted, and null when the request has no credential that resolves to an account @@ -140,11 +140,11 @@ The application as the bot token that application issued sees it. 1 Read from the bot account, so it changes with [Update bot profile](#update-bot-profile), and it is null when the application has no bot account -2 Fluxer persists no application signing key +2 Fluxer stores no application signing key 3 The request fails with 401 `INVALID_TOKEN` when the owning account no longer exists -4 Absent when the application has no bot account, and its `token` is never populated here +4 Absent when the application has no bot account, and its `token` is never set here 5 Declared optional by the schema and always emitted, and an empty array when the application has registered no redirect URI @@ -201,7 +201,7 @@ Returns every [application](#application-object) object the current user owns. | 200 | array[[application](#application-object) object] | The applications were returned | | 403 | [error response](/http-api/#error-response) | The credential is a bot or bearer token (`ACCESS_DENIED`) | -The listing is unpaginated because one user can own at most 25 applications. No entry has a client secret or bot token. The system account owns the built-in `Fluxer Admin` application, which never appears in this listing. +The listing is unpaginated because one user can own at most 25 applications. No entry has a client secret or bot token. The built-in `Fluxer Admin` application belongs to the system account and never appears here. ### Rate limit @@ -257,7 +257,7 @@ Every rejection reports the same `INVALID_TOKEN`, so a caller cannot distinguish -Returns the [public application](#public-application-object) object for any application. Authentication is not required. A credential that resolves to an account also populates `current_user`, and one that resolves to the application owner reports `bot_public` as true even when the bot is not public. +Returns the [public application](#public-application-object) object for any application. Authentication is not required. A credential that resolves to an account also sets `current_user`, and one that resolves to the application owner reports `bot_public` as true even when the bot is not public. ### Path parameters @@ -273,7 +273,7 @@ Returns the [public application](#public-application-object) object for any appl | 400 | [error response](/http-api/#error-response) | The application ID is not a valid snowflake | | 404 | [error response](/http-api/#error-response) | The application does not exist (`UNKNOWN_APPLICATION`) | -This representation exposes the application's complete registered redirect URI list and its bot profile to anonymous callers. It has no client secret, no bot token, no owner identity, and nothing about the owner's MFA enrolment. An application owner should not register a redirect URI whose hostname is itself sensitive. +This representation exposes the full registered redirect URI list and the bot profile to anonymous callers. It has no client secret, no bot token, no owner identity, and nothing about the owner's MFA enrolment. An application owner should not register a redirect URI whose hostname is itself sensitive. ### Rate limit @@ -311,7 +311,7 @@ An unclaimed account cannot create an application. An account is unclaimed while 2 Null registers no redirect URI. Each entry is normalised and then bounded at 1 to 256 characters -3 Each URI must parse as an absolute URL with a host and must use the HTTPS scheme. HTTP is accepted only when the host is `localhost`, a `.localhost` subdomain, an IPv4 literal, or a bracketed IPv6 literal. A URI that content moderation blocks is rejected with 403 `CONTENT_BLOCKED` +3 Each URI must parse as an absolute URL with a host and must use HTTPS. HTTP is accepted only when the host is `localhost`, a `.localhost` subdomain, an IPv4 literal, or a bracketed IPv6 literal. A URI that content moderation blocks is rejected with 403 `CONTENT_BLOCKED` ### Response @@ -342,7 +342,7 @@ Fluxer creates the application and the bot account together. The bot starts with -Returns one owned [application](#application-object) object. The caller must own the application. +Returns one [application](#application-object) object. Only the owner can read it. ### Path parameters @@ -359,7 +359,7 @@ Returns one owned [application](#application-object) object. The caller must own | 403 | [error response](/http-api/#error-response) | The credential is a bot or bearer token, or the caller does not own the application (`ACCESS_DENIED`) | | 404 | [error response](/http-api/#error-response) | The application does not exist (`UNKNOWN_APPLICATION`) | -Fluxer reports existence before ownership, so an application the caller does not own returns 403 `ACCESS_DENIED` rather than 404. +Fluxer reports existence before ownership, so an application the caller does not own returns 403 `ACCESS_DENIED` and never 404. ### Rate limit @@ -369,7 +369,7 @@ Fluxer reports existence before ownership, so an application the caller does not -Updates one owned application and returns the resulting [application](#application-object) object. The caller must own the application. +Updates one application and returns the resulting [application](#application-object) object. Only the owner can update it. ### Path parameters @@ -405,7 +405,7 @@ Removing a redirect URI takes effect immediately for new authorisation requests. ### Side effects -The submitted fields replace the corresponding application configuration. Neither credential is rotated, and no Gateway Dispatch is emitted. +The submitted fields replace the matching application configuration. Neither credential is rotated, and no Gateway Dispatch is emitted. ### Rate limit @@ -415,7 +415,7 @@ The submitted fields replace the corresponding application configuration. Neithe -Updates the bot account owned by an application and returns the resulting [bot profile](#bot-profile-object) object. The caller must own the application. +Updates the bot account an application owns and returns the resulting [bot profile](#bot-profile-object) object. The caller must own the application. ### Path parameters @@ -460,7 +460,7 @@ Fluxer checks the decoded bytes of `avatar` and `banner` against the instance's | 403 | [error response](/http-api/#error-response) | The username or biography is blocked (`CONTENT_BLOCKED`) | | 404 | [error response](/http-api/#error-response) | The application does not exist (`UNKNOWN_APPLICATION`), or it has no bot account (`BOT_USER_NOT_FOUND`) | -This operation always clears the bot's global display name, so a bot is always presented by its username. The write consumes the tag change allowance only when it changes the username or the discriminator. Fluxer reports an exhausted allowance as a body validation entry on `username` with the code `USERNAME_CHANGED_TOO_MANY_TIMES` rather than as 429. +This operation always clears the bot's global display name, so a bot is always presented by its username. The write consumes the tag change allowance only when it changes the username or the discriminator. Fluxer reports an exhausted allowance as a body validation entry on `username` with the code `USERNAME_CHANGED_TOO_MANY_TIMES`, never as 429. :::note[Only null clears a stored value] An omitted `avatar`, `banner`, or `bio` leaves the stored value untouched. Sending `{"bio": null}` clears the biography, and sending `{}` changes nothing except the global display name. @@ -468,11 +468,11 @@ An omitted `avatar`, `banner`, or `bio` leaves the stored value untouched. Sendi ### Side effects -Fluxer applies the supplied profile fields to the bot account. Replaced avatar and banner assets stop appearing after the update, and a username change that alters more than letter case also replaces the discriminator with a random free one for the new username. The operation emits a [User Update](/gateway/events/#user-update) Gateway Dispatch to the bot account's own sessions. +Fluxer applies the supplied profile fields to the bot account. Replaced avatar and banner assets stop appearing after the update. A username change that alters more than letter case also replaces the discriminator with a random free one for the new username. The operation emits a [User Update](/gateway/events/#user-update) Gateway Dispatch to the bot account's own sessions. ### Rate limit -30 requests per minute for each authenticated user, on the `oauth_dev:clients:update::client_id` bucket, shared with [Update application](#update-application), and a change to the resulting tag is additionally limited to 5 changes per 3 hours for each bot account. +30 requests per minute for each authenticated user, on the `oauth_dev:clients:update::client_id` bucket, shared with [Update application](#update-application), and a change to the resulting tag is also limited to 5 changes per 3 hours for each bot account. ## Reset bot token @@ -501,14 +501,14 @@ The body is a [sudo verification](/http-api/users/mfa/#sudo-verification-object) | 404 | [error response](/http-api/#error-response) | The application does not exist (`UNKNOWN_APPLICATION`), or it has no bot account (`BOT_USER_NOT_FOUND`) | :::caution[Rotation disconnects the bot] -The previous token stops authenticating the moment the new one is stored, and every live main Gateway session the bot holds is terminated. +The moment the new token is stored, the previous one stops authenticating and every live main Gateway session the bot holds ends. ::: A running bot reconnects only after its operator installs the returned token. ### Side effects -Fluxer stores the new token and terminates every active main Gateway session belonging to the bot account. The bot's guild memberships, group direct message memberships, and OAuth2 grants are unchanged, and no resource Dispatch is emitted. +Fluxer stores the new token and ends every live main Gateway session the bot account holds. The bot's guild memberships, group direct message memberships, and OAuth2 grants are unchanged, and no resource Dispatch is emitted. ### Rate limit @@ -544,7 +544,7 @@ The returned application object has the new `client_secret` and no `bot` member. ### Side effects -The previous client secret stops authenticating token exchange, introspection, and revocation immediately. Existing access and refresh tokens remain valid until they expire or are revoked, and no Gateway Dispatch is emitted. +Token exchange, introspection, and revocation stop accepting the previous client secret immediately. Existing access and refresh tokens remain valid until they expire or are revoked, and no Gateway Dispatch is emitted. ### Rate limit @@ -586,9 +586,9 @@ A 204 response means the removal has finished. Repeating the request afterwards ### Side effects -Fluxer reattributes every message the bot authored to a freshly created placeholder account with the deleted-account representation, and it creates that placeholder only when the bot authored at least one message. The bot's tag is released for reuse. +Fluxer reassigns every message the bot authored to a newly created placeholder account with the deleted-account representation. It creates that placeholder only when the bot authored at least one message. The bot's tag is released for reuse. -The bot is removed from every guild it is a member of, and the bot account record is retained and rewritten to the deleted-account representation. That representation has the username `DeletedUser`, the global name `Deleted User`, and the discriminator `0000`, with no email, no password, no authenticators, no avatar, banner, biography, pronouns, accent colour, timezone, or date of birth, and only the deleted flag set. Fluxer then deletes the application record. +The bot is removed from every guild it is a member of. The bot account record is retained and rewritten to the deleted-account representation. That representation has the username `DeletedUser`, the global name `Deleted User`, and the discriminator `0000`. It has no email, no password, no authenticators, no avatar, banner, biography, pronouns, accent colour, timezone, or date of birth, and only the deleted flag is set. Fluxer then deletes the application record. The bot token and the client secret stop authenticating as soon as the application record is gone. No further token exchange, refresh, introspection, or revocation succeeds for the application. Access and refresh tokens already issued to it are not deleted, and an outstanding access token keeps authenticating a scope-gated route until it expires. [Get current OAuth2 authorisation](/http-api/oauth2/#get-current-oauth2-authorisation) and [Get OAuth2 user information](/http-api/oauth2/#get-oauth2-user-information) resolve the application as well, so both return 401 `INVALID_TOKEN` for that token. diff --git a/fluxer_docs/src/content/docs/http-api/authentication.mdx b/fluxer_docs/src/content/docs/http-api/authentication.mdx index 1c3ca1c81..f592033f8 100644 --- a/fluxer_docs/src/content/docs/http-api/authentication.mdx +++ b/fluxer_docs/src/content/docs/http-api/authentication.mdx @@ -20,15 +20,15 @@ Most operations need no `Authorization` credential, because the request has a on Every route has a route bucket and is also subject to the [global HTTP limit](/topics/rate-limits/). A route bucket is keyed by the authenticated user when the request has a resolvable credential and by the client IP address otherwise. Bucket or global denial returns 429 `RATE_LIMITED`. -An invalid JSON shape returns 400 `INVALID_FORM_BODY` with [validation error object](/http-api/#validation-error-object) entries in `errors`. An unexpected failure returns 500 `INTERNAL_SERVER_ERROR`. Every enumerated code on this page is registered in the [error registry](/http-api/errors/), and every snowflake field is the decimal string form defined by [Snowflakes](/snowflakes/). +An invalid JSON shape returns 400 `INVALID_FORM_BODY` with [validation error object](/http-api/#validation-error-object) entries in `errors`. An unexpected failure returns 500 `INTERNAL_SERVER_ERROR`. Every enumerated code on this page is registered in [Errors](/http-api/errors/), and every snowflake field is the decimal string form defined by [Snowflakes](/snowflakes/). -When SSO is both enabled and enforced, every local authentication operation returns 403 `SSO_REQUIRED`. The SSO status route, SSO start and completion, logout, both session routes, and every handoff route stay available under enforcement. +When SSO is both enabled and enforced, every local authentication operation returns 403 `SSO_REQUIRED`. The SSO status route, SSO start and completion, logout, both session routes, and every handoff route stay available under enforcement. Every other route on this page is a local authentication operation. :::caution[SSO enforcement precedes every other check] The enforcement check runs before CAPTCHA verification, before the route rate limit, before authentication, and before body validation. An enforced instance therefore answers a malformed or unauthenticated local authentication request with 403 `SSO_REQUIRED`. ::: -A successful sign-in creates one authentication session and issues its token. Fluxer sets no ceiling on live sessions and evicts none when a further session starts, so an account accumulates one session per sign-in until it revokes them through [terminate authentication sessions](#terminate-authentication-sessions). +A successful sign-in creates one authentication session and issues its token. Fluxer sets no ceiling on live sessions and evicts none when a further session starts, so an account holds one session per sign-in until it revokes them through [terminate authentication sessions](#terminate-authentication-sessions). Revoking a session, whether the account revoked it or an administrator terminated it, stops its token authenticating requests. Its main Gateway connection receives [Invalid Session](/gateway/overview/#invalid-session) with `d: false` and stays open, unauthenticated. No Gateway Dispatch is emitted for the revocation itself, so a client learns of it from that frame or from the next request that fails to authenticate. @@ -64,7 +64,7 @@ The public single sign-on state. The same object is embedded by the [instance di | Field | Type | Description | | --- | --- | --- | -| enabled1 | boolean | Whether SSO can currently be started on this instance | +| enabled1 | boolean | Whether SSO can be started on this instance | | enforced2 | boolean | Whether SSO is required for every user | | display_name | ?string | The configured provider display name, or null when none is set | | redirect_uri | string | The default OAuth2 redirect URI used for the provider callback | @@ -105,7 +105,7 @@ SSO completion always issues a session. It extends the [authentication token res 1 The value is the empty string when [start SSO](#start-sso) received no `redirect_to` or when the supplied value did not survive sanitisation :::note[SSO completion never issues an MFA challenge] -Completion does not consult the account's second factor and cannot return an [MFA challenge response](#mfa-challenge-response). A client MUST NOT branch on an `mfa` member here. +Completion does not check the account's second factor and cannot return an [MFA challenge response](#mfa-challenge-response). A client MUST NOT branch on an `mfa` member here. ::: ## Registration pending approval response object @@ -153,7 +153,7 @@ There is no backup code completion route and `backup_code` never appears in `all Both WebAuthn option operations return a PublicKeyCredential request options object that a browser passes straight to its credential request. The [WebAuthn authentication options object](/http-api/users/mfa/#webauthn-authentication-options-object) on the Multi-factor authentication resource defines its fields, together with its [WebAuthn credential descriptor](/http-api/users/mfa/#webauthn-credential-descriptor-object) and [WebAuthn client extension inputs](/http-api/users/mfa/#webauthn-client-extension-inputs-object) objects. -A challenge issued for MFA completion is additionally bound to its ticket and account, so it cannot be replayed against discoverable login. The MFA route lists every registered credential for the resolved account in `allowCredentials`, while the discoverable route omits the member because discovery happens at the authenticator. +A challenge issued for MFA completion is also bound to its ticket and account, so it cannot be replayed against discoverable login. The MFA route lists every registered credential for the resolved account in `allowCredentials`. The discoverable route omits the member, because discovery happens at the authenticator. @@ -195,7 +195,7 @@ The parsed device metadata recorded for a session or a pending handoff. | device | string | The device class, either `mobile` or `desktop` | | location? | ?[client location](#client-location-object) object | The approximate geolocation derived from the recorded IP address | -1 A native or Electron client reports null, and a session created by an unparseable request reports null. A handoff omits the member entirely +1 A native or Electron client reports null, as does a session created by an unparseable request. A handoff omits the member entirely Fluxer recognises a native Fluxer client from a `User-Agent` beginning `Fluxer Android`, `Fluxer iOS`, `Fluxer Linux`, `Fluxer Desktop`, or `Fluxer Client`. @@ -227,7 +227,7 @@ The result of checking a password reset token without consuming it. | Field | Type | Description | | --- | --- | --- | -| valid1 | boolean | Whether the token is currently valid and unexpired | +| valid1 | boolean | Whether the token is valid and unexpired | 1 A password reset token expires one hour after it is issued @@ -248,7 +248,7 @@ The state of one IP authorisation ticket, as observed by the device whose sign-i ## Username suggestions object -The username candidates derived from a display name. +Username candidates derived from a display name. ### Structure @@ -329,7 +329,7 @@ The operation stays available while SSO is enforced. It shares the `auth:sso:sta Creates one SSO state with PKCE and nonce material. Authentication is not required. Returns an [SSO start](#sso-start-object) object. -The returned `authorization_url` is the configured provider authorisation endpoint with `response_type` set to `code`, the configured `client_id` and `scope`, the resolved `redirect_uri`, the returned `state`, and a freshly generated `nonce`. It also has a `code_challenge` computed as the base64url SHA-256 digest of the state's code verifier, with `code_challenge_method` set to `S256`. +The returned `authorization_url` is the configured provider authorisation endpoint with `response_type` set to `code`, the configured `client_id` and `scope`, the resolved `redirect_uri`, the returned `state`, and a newly generated `nonce`. It also has a `code_challenge` computed as the base64url SHA-256 digest of the state's code verifier, with `code_challenge_method` set to `S256`. ### JSON body @@ -338,7 +338,7 @@ The returned `authorization_url` is the configured provider authorisation endpoi | redirect_to?1 | ?string | The post-authentication redirect to bind to the state | | redirect_uri?2 | ?string | The provider callback URI to use instead of the configured default | -1 Fluxer sanitises the value before binding it to the state and discards a value that does not survive, which the eventual [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 +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 @@ -355,7 +355,7 @@ The returned `authorization_url` is the configured provider authorisation endpoi ### Side effects -The returned state can be completed once and lives for ten minutes. It binds the PKCE verifier, nonce, redirect, and callback URI to the flow. No account state changes and no Gateway Dispatch is emitted. +The returned state can be completed once and lives for ten minutes. It binds the PKCE verifier, nonce, redirect, and callback URI to the flow. No account state changes. No Gateway Dispatch is emitted. ### Rate limit @@ -377,11 +377,11 @@ Consumes the SSO state, exchanges the authorisation code, verifies the identity 1 The state is consumed on the first call that resolves it, so a repeated request with the same state returns the field code `INVALID_OR_EXPIRED_SSO_STATE` :::note[Identity binding is one-to-one] -A provider subject is bound to exactly one Fluxer account, and an account accepts exactly one provider subject. Presenting a subject that is already linked to a different account, or presenting a new subject for an account that already holds a different linked subject, returns the field code `SSO_IDENTITY_MISMATCH`. +A provider subject is bound to exactly one Fluxer account, and an account accepts exactly one provider subject. A subject already linked to a different account, or a new subject for an account that already holds a different linked subject, returns the field code `SSO_IDENTITY_MISMATCH`. ::: :::caution[A verified provider email adopts a local account] -When the subject is not yet linked, Fluxer resolves the account by the verified email address in the provider claim. It adopts any existing account holding that address and binds the subject to it, whether or not that account has a password or an enrolled authenticator. An operator therefore MUST enable SSO only for a provider that controls the addresses it asserts, because a provider asserting an arbitrary verified address takes over the matching local account. +When the subject is not yet linked, Fluxer adopts any existing account holding the verified email address in the provider claim and binds the subject to it, whether or not that account has a password or an authenticator. A provider that asserts an arbitrary verified address therefore takes over the matching local account. An operator MUST enable SSO only for a provider that controls the addresses it asserts. ::: Fluxer refuses the adoption only when the account already holds a different provider subject. @@ -419,7 +419,7 @@ Approval mode registration instead returns 403 `REGISTRATION_PENDING_APPROVAL` a Creates an ordinary account. Returns an [authentication token response](#authentication-token-response) when the instance admits the account immediately and a [registration pending approval response](#registration-pending-approval-response-object) when it does not. Emits a [Guild Member Add](/gateway/events/#guild-member-add) Gateway event when an invite or instance community admission takes effect. -This is a local authentication operation and it verifies [CAPTCHA](/topics/captcha/) when CAPTCHA is enabled. Registration permits 3 attempts per hour for each client IP address and 15 per hour for each client subnet, which is the IPv4 /24 or IPv6 /48 network. A supplied email address permits 3 attempts per 15 minutes of its own. Those allowances are separate from the route bucket, and only a deployment that relaxes registration rate limits disables them. +Registration verifies [CAPTCHA](/topics/captcha/) when CAPTCHA is enabled. It permits 3 attempts per hour for each client IP address and 15 per hour for each client subnet, which is the IPv4 /24 or IPv6 /48 network. A supplied email address permits 3 attempts per 15 minutes of its own. Those allowances are separate from the route bucket, and only a deployment that relaxes registration rate limits disables them. ### Request headers @@ -455,11 +455,11 @@ This is a local authentication operation and it verifies [CAPTCHA](/topics/captc 3 The password is checked against the public breached-password corpus described under [reset a password](#reset-a-password), and a match returns the field code `PASSWORD_IS_TOO_COMMON` -4 The field is required when the instance [collects date of birth](/http-api/instance/#public-registration-fields-object). An absent or blank value, and a value in `YYYY-MM-DD` shape that is not a real calendar date, return the field code `INVALID_DATE_OF_BIRTH_FORMAT`. A value that is not ten characters in `YYYY-MM-DD` shape fails schema validation with the field code `STRING_LENGTH_EXACT` or `INVALID_FORMAT` +4 The field is required when the instance [collects date of birth](/http-api/instance/#public-registration-fields-object). An absent or blank value, or a value in `YYYY-MM-DD` shape that is not a real calendar date, returns the field code `INVALID_DATE_OF_BIRTH_FORMAT`. A value that is not ten characters in `YYYY-MM-DD` shape fails schema validation with the field code `STRING_LENGTH_EXACT` or `INVALID_FORMAT` 5 A false or absent value returns the field code `MUST_AGREE_TO_TOS_AND_PRIVACY_POLICY` on the official instance and on any instance that publishes a terms or privacy document -6 A code supplied on an instance with administrator registration URLs disabled, and a code that does not resolve, both return 400 `REGISTRATION_URL_INVALID`. A valid code overrides the closed registration mode and supplies its own approval setting in place of the instance approval mode +6 A code supplied on an instance with administrator registration URLs disabled, and a code that does not resolve, both return 400 `REGISTRATION_URL_INVALID`. A valid code overrides the closed registration mode and replaces the instance approval mode with its own approval setting Fluxer resolves the region from the client IP address, and an age below the minimum for that region returns the field code `MUST_BE_MINIMUM_AGE`. That minimum is 13 years unless account policy sets a different minimum for the region, and the applied minimum appears only in the localised message. @@ -502,7 +502,7 @@ Approval mode registration creates no guild membership and no authentication ses Validates local email and password credentials. Authentication is not required. Returns an [authentication token response](#authentication-token-response) when no second factor and no IP approval are outstanding, and an [MFA challenge response](#mfa-challenge-response) when the account has a second factor. -This is a local authentication operation and it verifies CAPTCHA when CAPTCHA is enabled. Login additionally permits 10 attempts per client IP address in 30 minutes, keyed by the exact IPv4 address or the IPv6 /64 network, and 5 attempts per email address in 15 minutes. Every admitted attempt consumes both allowances, whether or not the credentials turn out to be correct, and a successful login clears neither. +Login verifies CAPTCHA when CAPTCHA is enabled. It also permits 10 attempts per client IP address in 30 minutes, keyed by the exact IPv4 address or the IPv6 /64 network, and 5 attempts per email address in 15 minutes. Every admitted attempt consumes both allowances, whether or not the credentials turn out to be correct, and a successful login clears neither. ### Request headers @@ -535,15 +535,15 @@ An unknown email address and an incorrect password both return the same paired f 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. -When the instance has disabled new-IP authorisation or sends no email, Fluxer authorises the client IP address silently and the login proceeds. An account that already has a second factor never enters IP authorisation. +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. Account policy can return 403 `REGISTRATION_PENDING_APPROVAL`, 403 `REGISTRATION_REJECTED`, 403 `ACCOUNT_SUSPENDED_TEMPORARILY`, or 403 `ACCOUNT_SUSPENDED_PERMANENTLY`. A bot account returns 403 `BOT_USER_AUTH_ENDPOINT_ACCESS_DENIED`. :::note[A verified password reactivates the account] -A correct password on an account the user had disabled clears the disabled state. A correct password on an account with a pending self-scheduled deletion cancels that deletion. Both happen during the login, before any second factor is requested, with no confirmation step and no reactivation ticket. +On an account the user had disabled, a correct password clears the disabled state. On an account with a pending self-scheduled deletion, a correct password cancels that deletion. Both happen during the login, before any second factor is requested, with no confirmation step and no reactivation ticket. ::: -The login clears an expired temporary suspension the same way. It does not clear a live temporary or permanent administrator suspension, and returns that suspension's 403 instead. +The login clears an expired temporary suspension the same way, but returns the 403 of a live temporary or permanent administrator suspension without clearing it. ### Response @@ -572,7 +572,7 @@ A successful login with no outstanding MFA or IP approval creates one authentica Consumes an MFA ticket and validates a time-based one-time password or an unconsumed backup code, then creates the session. Authentication is not required. Returns an [authentication token response](#authentication-token-response). -This is a local authentication operation. MFA verification additionally permits 10 failed attempts per account in 15 minutes and destroys the ticket after 5 failed attempts. +MFA verification also permits 10 failed attempts per account in 15 minutes and destroys the ticket after 5 failed attempts. ### JSON body @@ -611,8 +611,6 @@ Success consumes the ticket, clears the per-account and per-ticket failed-attemp Resolves the account from an MFA ticket and creates a challenge restricted to that account's registered credentials. Authentication is not required. Returns a [WebAuthn authentication options](#webauthn-authentication-options) object. -This is a local authentication operation. - ### JSON body | Field | Type | Description | @@ -636,7 +634,7 @@ A client MUST still complete the returned challenge through [complete login with ### Side effects -The operation issues a one-use WebAuthn challenge valid for five minutes and bound to this MFA flow, account, and ticket. It changes no account state and emits no Gateway Dispatch. +The operation issues a one-use WebAuthn challenge valid for five minutes and bound to this MFA flow, account, and ticket. It changes no account state. No Gateway Dispatch is emitted. ### Rate limit @@ -648,8 +646,6 @@ The operation issues a one-use WebAuthn challenge valid for five minutes and bou Consumes the MFA ticket, verifies the WebAuthn assertion against the challenge, and creates the session. Authentication is not required. Returns an [authentication token response](#authentication-token-response). -This is a local authentication operation. - ### JSON body | Field | Type | Description | @@ -691,8 +687,6 @@ Success consumes the challenge and the ticket, records the credential's new sign Creates a challenge for passwordless authentication with a discoverable credential. Authentication is not required. Returns a [WebAuthn authentication options](#webauthn-authentication-options) object. -This is a local authentication operation. - The request has no body. The response omits `allowCredentials` and requests `required` user verification. ### Response @@ -707,7 +701,7 @@ The request has no body. The response omits `allowCredentials` and requests `req ### Side effects -The operation issues a one-use discoverable WebAuthn challenge valid for five minutes and bound to the discoverable context alone. It changes no account state and emits no Gateway Dispatch. +The operation issues a one-use discoverable WebAuthn challenge valid for five minutes and bound to the discoverable context alone. It changes no account state. No Gateway Dispatch is emitted. ### Rate limit @@ -719,8 +713,6 @@ The operation issues a one-use discoverable WebAuthn challenge valid for five mi Resolves the account from the presented credential ID, verifies the assertion with user verification required, applies account suspension policy, and creates the session. Authentication is not required. Returns an [authentication token response](#authentication-token-response). -This is a local authentication operation. - ### JSON body | Field | Type | Description | @@ -760,7 +752,7 @@ Success consumes the challenge, records the credential's new signature counter a Revokes the session identified by the user session token in the `Authorization` header. Returns 204 with no body. -The request has no body. The operation requires a credential that resolves to an account, and it admits one flagged for suspicious activity. An absent, malformed, or unknown token returns 401 `UNAUTHORIZED`. An OAuth2 bearer token returns 403 `ACCESS_DENIED`. A bot token is accepted and revokes nothing, because a bot holds no session. +The request has no body. The operation requires a credential that resolves to an account, and it admits one flagged for suspicious activity. An absent, malformed, or unknown token returns 401 `UNAUTHORIZED`. An OAuth2 bearer token returns 403 `ACCESS_DENIED`. A bot holds no session, so a bot token is accepted and revokes nothing. :::note[Logging out revokes exactly the calling session] The account's other sessions, its OAuth2 grants, and any outstanding MFA ticket, IP authorisation ticket, or desktop handoff all survive. Use [terminate authentication sessions](#terminate-authentication-sessions) to revoke other sessions. @@ -779,7 +771,7 @@ The account's other sessions, its OAuth2 grants, and any outstanding MFA ticket, ### Side effects -Revoking a live session ends its Gateway session. The connection receives Invalid Session with `d: false` and stays open, unauthenticated. No Gateway Dispatch is emitted for the revocation. +Revoking this session ends its Gateway session as [shared behaviour](#shared-behaviour) states. No Gateway Dispatch is emitted for the revocation. ### Rate limit @@ -791,8 +783,6 @@ Revoking a live session ends its Gateway session. The connection receives Invali Consumes an email verification token and marks the account's current address verified. Authentication is not required. Returns 204 with no body. -This is a local authentication operation. - ### JSON body | Field | Type | Description | @@ -830,8 +820,6 @@ The operation marks the current address verified, clears its bounced state, and Issues and sends a new email verification token for the authenticated account. Requires a user session token for an ordinary user. Returns 204 with no body. -An account locked by suspicious activity is still permitted. This is a local authentication operation. - The request has no body. When the current address is already verified and no reverification suspicious activity flag is set, the operation returns 204 and sends nothing. ### Response @@ -847,7 +835,7 @@ The request has no body. When the current address is already verified and no rev ### Side effects -The operation creates one 64-character verification token bound to the account and its current address, valid for 24 hours, and sends the verification message. Previously issued verification tokens remain valid. It emits no Gateway Dispatch. +The operation creates one 64-character verification token bound to the account and its current address, valid for 24 hours, and sends the verification message. Previously issued verification tokens remain valid. No Gateway Dispatch is emitted. ### Rate limit @@ -859,7 +847,7 @@ The operation creates one 64-character verification token bound to the account a Accepts a password recovery request for an email address. Authentication is not required. Returns 204 with no body. -This is a local authentication operation and it verifies CAPTCHA when CAPTCHA is enabled. Fluxer consumes both the client IP address and email address allowances before it validates the address, and exhausting either returns 429. +Password recovery verifies CAPTCHA when CAPTCHA is enabled. Fluxer consumes both the client IP address and email address allowances before it validates the address, and exhausting either returns 429. ### Request headers @@ -897,7 +885,7 @@ An address that resolves to no account produces the same 204 response as an addr ### Side effects -Fluxer sends one 64-character reset token by email to an address that resolves to an account. The token is bound to that account and its current address, and it is valid for one hour. Nothing else changes and no Gateway Dispatch is emitted. +Fluxer sends one 64-character reset token by email to an address that resolves to an account. The token is bound to that account and its current address, and it is valid for one hour. Nothing else changes. No Gateway Dispatch is emitted. ### Rate limit @@ -909,8 +897,6 @@ Fluxer sends one 64-character reset token by email to an address that resolves t Checks a reset token without consuming it. Authentication is not required. Returns a [password reset validity](#password-reset-validity-object) object. -This is a local authentication operation. - ### Path parameters | Field | Type | Description | @@ -920,7 +906,7 @@ This is a local authentication operation. A well-formed token that is unknown, already consumed, or bound to a deleted account returns the same object with `valid` set to false. :::caution[Validity does not promise the reset succeeds] -The check resolves the token and confirms that its account exists and is not deleted. A token this operation reports as valid is still refused there for a bot account, for an account under a live temporary suspension, or for a replacement password in the breached-password corpus. +The check resolves the token and confirms that its account exists and is not deleted. The reset itself still refuses a valid token for a bot account, for an account under a live temporary suspension, or for a replacement password in the breached-password corpus. ::: ### Response @@ -936,7 +922,7 @@ The check resolves the token and confirms that its account exists and is not del ### Side effects -This read does not consume the token, mutate account state, or emit a Gateway Dispatch. +This read does not consume the token or change account state. No Gateway Dispatch is emitted. :::caution[The token travels in the path] A token in the path reaches proxy logs, browser history, and referrer headers. A client SHOULD read the token from the recovery link and send it in this one request, and SHOULD NOT route the user's browser to the API path directly. @@ -952,8 +938,6 @@ A token in the path reaches proxy logs, browser history, and referrer headers. A Consumes a valid reset token and replaces the account password, then issues a new session or an MFA challenge. Authentication is not required. Returns an [authentication token response](#authentication-token-response) when the account has no second factor, and an [MFA challenge response](#mfa-challenge-response) when it has one. -This is a local authentication operation. - ### JSON body | Field | Type | Description | @@ -966,13 +950,13 @@ This is a local authentication operation. An unknown or already consumed token, and a token bound to a deleted account, return the field code `INVALID_OR_EXPIRED_RESET_TOKEN`. A live temporary suspension returns 403 `ACCOUNT_SUSPENDED_TEMPORARILY`. A permanent suspension returns the field code `INVALID_OR_EXPIRED_RESET_TOKEN` and never 403 `ACCOUNT_SUSPENDED_PERMANENTLY`. A bot account returns 403 `BOT_USER_AUTH_ENDPOINT_ACCESS_DENIED`. :::note[The breached-password check is an outbound request] -The corpus is the public Pwned Passwords range service at `https://api.pwnedpasswords.com`, which is not configurable. Fluxer sends only the first five hexadecimal characters of the candidate password's SHA-1 digest to that service. The check fails open. +The corpus is the public Pwned Passwords range service at `https://api.pwnedpasswords.com`, which is not configurable. Fluxer sends only the first five hexadecimal characters of the candidate password's SHA-1 digest to that service. ::: Fluxer treats the password as unbreached when the service returns a non-success status, returns a malformed body, or does not answer inside five seconds. The same check runs during [registration](#register-an-account) and [email reversion](#revert-an-email-change). :::caution[Resetting a password revokes every existing session] -A successful reset terminates every authentication session on the account, including the one that any other device is holding. The reset then issues one fresh session of its own, unless the account has a second factor, in which case it issues an MFA ticket and the session follows the factor. +A successful reset terminates every authentication session on the account, including the one any other device is holding. It then issues one fresh session. An account with a second factor instead receives an MFA ticket, and the session follows the factor. ::: ### Response @@ -988,7 +972,7 @@ A successful reset terminates every authentication session on the account, inclu ### Side effects -The operation replaces the password, records the change time, clears an expired temporary suspension, terminates every authentication session on the account, and consumes the presented reset token. Every terminated session's Gateway connection receives Invalid Session with `d: false` and stays open, unauthenticated. Other outstanding reset tokens are not invalidated, so a second recovery link issued earlier still works. +The operation replaces the password, records the change time, clears an expired temporary suspension, terminates every authentication session on the account, and consumes the presented reset token. Every terminated session loses its Gateway session as [shared behaviour](#shared-behaviour) states. Other outstanding reset tokens are not invalidated, so a second recovery link issued earlier still works. An account with no second factor then receives one new session and its token. An account with a second factor receives a five-minute MFA ticket instead, and the MFA completion creates the session. @@ -1002,7 +986,7 @@ An account with no second factor then receives one new session and its token. An Consumes the token delivered to the previous email address, restores that address, replaces the password, and rebuilds the account's credential state. Returns an [authentication token response](#authentication-token-response). Emits a [User Update](/gateway/events/#user-update) Gateway event. -The token is valid for 24 hours after the address change that issued it. This is a local authentication operation. +The token is valid for 24 hours after the address change that issued it. ### JSON body @@ -1014,7 +998,7 @@ The token is valid for 24 hours after the address change that issued it. This is 1 The value becomes the account's new password, and Fluxer checks it against the public breached-password corpus, returning the field code `PASSWORD_IS_TOO_COMMON` on a match :::danger[Reversion destroys every other credential] -Completing a reversion terminates every authentication session, clears the account's entire second-factor enrolment, and replaces the authorised IP address set with the single IP address that made the request. Any other device, passkey, or backup code held by whoever performed the original email change stops working immediately, and none of it can be restored. +Completing a reversion terminates every authentication session and clears the account's entire second-factor enrolment. The authorised IP address set becomes the requesting IP address alone. Any other device, passkey, or backup code held by whoever made the original email change stops working immediately, and none of it can be restored. ::: An unknown or already consumed token, and a token bound to an account that no longer exists, return the field code `INVALID_OR_EXPIRED_REVERT_TOKEN`. Account suspension returns 403 `ACCOUNT_SUSPENDED_TEMPORARILY` or 403 `ACCOUNT_SUSPENDED_PERMANENTLY`, and a bot account returns 403 `BOT_USER_AUTH_ENDPOINT_ACCESS_DENIED`. @@ -1034,9 +1018,9 @@ Fluxer checks the replacement password against the same breached-password corpus ### Side effects -The operation restores the previous email address and marks it verified, sets the replacement password, clears the TOTP secret and every authenticator type, deletes every MFA backup code and every registered WebAuthn credential, and terminates every authentication session. It clears the authorised IP address set and then authorises the requesting client IP address alone. +The operation restores the previous email address and marks it verified, sets the replacement password, and terminates every authentication session. Fluxer also clears the TOTP secret and every authenticator type, and deletes every MFA backup code and every registered WebAuthn credential. It then clears the authorised IP address set and authorises the requesting client IP address alone. -Every terminated session's Gateway connection receives Invalid Session with `d: false` and stays open, unauthenticated. [User Update](/gateway/events/#user-update) is emitted for the account, and Fluxer records the contact change. +Every terminated session loses its Gateway session as [shared behaviour](#shared-behaviour) states. [User Update](/gateway/events/#user-update) is emitted for the account, and Fluxer records the contact change. The operation then creates one new authentication session and returns it. @@ -1050,8 +1034,6 @@ The operation then creates one new authentication session and returns it. Lists every live authentication session belonging to the authenticated account, newest activity first. Requires a user session token for an ordinary user. Returns an array of [authentication session](#authentication-session-object) objects. -An account locked by suspicious activity is still permitted. - The request has no body and takes no parameters. ### Response @@ -1067,7 +1049,7 @@ The request has no body and takes no parameters. ### Side effects -This read does not mutate session state and emits no Gateway Dispatch. +This read does not change session state. No Gateway Dispatch is emitted. ### Rate limit @@ -1079,7 +1061,7 @@ This read does not mutate session state and emits no Gateway Dispatch. Deletes the named authentication sessions. Requires a user session token for an ordinary user, and [sudo mode](/http-api/users/mfa/#sudo-mode). Returns 204 with no body. -An account locked by suspicious activity is still permitted. The caller MUST send a valid sudo token in the `X-Fluxer-Sudo-Mode-JWT` header, or supply a password or MFA proof in the body. +The caller MUST send a valid sudo token in the `X-Fluxer-Sudo-Mode-JWT` header, or supply a password or MFA proof in the body. ### JSON body @@ -1101,7 +1083,7 @@ An account locked by suspicious activity is still permitted. The caller MUST sen A `totp` method reads `mfa_code` as an authenticator code and accepts an unconsumed backup code in its place. A `webauthn` method reads `webauthn_response` and `webauthn_challenge` together, and it accepts only a challenge that was issued for the sudo context. :::caution[The caller can delete its own session] -The operation deletes exactly the sessions identified by the supplied `id_hash` values. Because `current` is false on every entry, a client that wants to keep its own session MUST compare the base64url SHA-256 digest of its own token against `id_hash` and omit that identifier. Revoking the calling credential still returns 204. +The operation deletes exactly the sessions identified by the supplied `id_hash` values. `current` is false on every entry, so a client that wants to keep its own session MUST omit its own `id_hash`. Revoking the calling credential still returns 204. ::: Missing or unusable proof returns 403 `SUDO_MODE_REQUIRED`, whose error body has top-level `has_mfa` and `methods` members, and `methods` reports whether `totp` and `webauthn` are available. @@ -1124,7 +1106,7 @@ An account that has neither a password nor a second factor satisfies sudo mode w ### Side effects -Each named session is deleted, and its Gateway connection receives Invalid Session with `d: false` and stays open, unauthenticated. A fresh MFA proof mints a new sudo token, while an accepted incoming token is echoed unchanged. Fluxer sets whichever token results in the `X-Fluxer-Sudo-Mode-JWT` response header. A password proof on an account with no second factor mints no token, so the header is not set unless the request already had one. No Gateway Dispatch is emitted. +Each named session is deleted and loses its Gateway session as [shared behaviour](#shared-behaviour) states. A fresh MFA proof issues a new sudo token, while an accepted incoming token is echoed unchanged. Fluxer sets whichever token results in the `X-Fluxer-Sudo-Mode-JWT` response header. A password proof on an account with no second factor issues no token, so the header is not set unless the request already had one. No Gateway Dispatch is emitted. ### Rate limit @@ -1136,15 +1118,13 @@ Each named session is deleted, and its Gateway connection receives Invalid Sessi Consumes the authorisation token delivered by email, authorises the pending client IP address, and completes the waiting login. Authentication is not required. Returns 204 with no body. -This is a local authentication operation. - ### JSON body | Field | Type | Description | | --- | --- | --- | | token | string | The authorisation token delivered by email | -An unknown, expired, already consumed, or account-mismatched token returns the field code `INVALID_OR_EXPIRED_AUTHORIZATION_TOKEN`. A token that resolves to an account that no longer exists returns 404 `UNKNOWN_USER`, and one that resolves to a bot account returns 403 `BOT_USER_AUTH_ENDPOINT_ACCESS_DENIED`. Session creation additionally rejects an account that has not been admitted or is suspended, with the codes listed under [log in with a password](#log-in-with-a-password). +An unknown, expired, already consumed, or account-mismatched token returns the field code `INVALID_OR_EXPIRED_AUTHORIZATION_TOKEN`. A token that resolves to an account that no longer exists returns 404 `UNKNOWN_USER`, and one that resolves to a bot account returns 403 `BOT_USER_AUTH_ENDPOINT_ACCESS_DENIED`. Session creation also rejects an account that has not been admitted or is suspended, with the codes listed under [log in with a password](#log-in-with-a-password). ### Response @@ -1160,7 +1140,7 @@ An unknown, expired, already consumed, or account-mismatched token returns the f ### Side effects -The operation adds the pending client IP address to the account's authorised set and creates one authentication session using the IP address, user agent, and reported operating system captured when the login was attempted. It consumes the authorisation token and the ticket for the pending login. +The operation adds the pending client IP address to the account's authorised set and creates one authentication session. That session takes the IP address, user agent, and reported operating system captured when the login was attempted. It consumes the authorisation token and the ticket for the pending login. The session token is then published against the ticket and remains readable by [poll IP authorisation](#poll-ip-authorisation) for 60 seconds. The caller of this operation receives 204 and no token of its own. No Gateway Dispatch is emitted. @@ -1174,8 +1154,6 @@ The session token is then published against the ticket and remains readable by [ Sends the authorisation message for an outstanding IP authorisation ticket again. Authentication is not required. Returns 204 with no body. -This is a local authentication operation. - ### JSON body | Field | Type | Description | @@ -1203,7 +1181,7 @@ There is no retry key. A client that loses the response cannot tell whether the ### Side effects -Fluxer sends the authorisation message to the address associated with the login attempt and marks the ticket's single resend used whether or not delivery succeeds. No account state changes and no Gateway Dispatch is emitted. +Fluxer sends the authorisation message to the address associated with the login attempt and marks the ticket's single resend used whether or not delivery succeeds. No account state changes. No Gateway Dispatch is emitted. ### Rate limit @@ -1215,8 +1193,6 @@ Fluxer sends the authorisation message to the address associated with the login Reads the login result associated with an IP authorisation ticket. Authentication is not required. Returns an [IP authorisation poll](#ip-authorisation-poll-object) object. -This is a local authentication operation. - ### Query parameters | Field | Type | Description | @@ -1251,7 +1227,7 @@ Repeated polling returns the same token, so anyone holding the ticket within the Derives username candidates from a display name. Authentication is not required. Returns a [username suggestions](#username-suggestions-object) object. -This is a local authentication operation. It shares the `auth:register` bucket, which permits 10 requests per 10 seconds. +The route shares the `auth:register` bucket, which permits 10 requests per 10 seconds. ### JSON body @@ -1260,7 +1236,7 @@ This is a local authentication operation. It shares the `auth:register` bucket, | global_name | string | The display name after normalisation (1-32 characters) | :::note[A returned candidate can be taken before use] -The operation performs no availability check and holds nothing. Registration allocates its own discriminator regardless of the suggestion. +The operation does no availability check and holds nothing. Registration allocates its own discriminator regardless of the suggestion. ::: ### Response @@ -1276,7 +1252,7 @@ The operation performs no availability check and holds nothing. Registration all ### Side effects -This read does not reserve a username, mutate account state, or emit a Gateway Dispatch. +This read reserves no username and changes no account state. No Gateway Dispatch is emitted. ## Initiate desktop handoff @@ -1304,7 +1280,7 @@ The request has no body. Fluxer derives the device metadata shown to the approvi ### Side effects -The handoff remains pending for five minutes and records the initiating device's client IP address, user agent, and reported operating system. Initiation creates no session and emits no Gateway Dispatch. +The handoff remains pending for five minutes and records the initiating device's client IP address, user agent, and reported operating system. Initiation creates no session. No Gateway Dispatch is emitted. ### Rate limit @@ -1324,10 +1300,10 @@ Fluxer counts failed code attempts against the client IP address and blocks it a | --- | --- | --- | | code1 | string | The handoff code | -1 Fluxer normalises the value by removing hyphens and whitespace and upper-casing the remainder, and the normalised result is exactly 12 characters from the handoff alphabet +1 Fluxer normalises the value by removing hyphens and whitespace and upper-casing the rest, and the normalised result is exactly 12 characters from the handoff alphabet :::caution[A successful lookup makes the code completable] -Reading the handoff information marks the code as inspected but does not approve the transfer. [Complete desktop handoff](#complete-desktop-handoff) rejects a code that has never been inspected. An approving client MUST show the initiating device information and obtain explicit user confirmation before it calls the completion operation. +Reading the handoff information marks the code as inspected but does not approve the transfer. [Complete desktop handoff](#complete-desktop-handoff) rejects a code that has never been inspected. An approving client MUST show the initiating device information and obtain explicit user confirmation before completion. ::: Anyone who can reach the API and knows the code can mark it inspected. Declining the request in a client discards only what that client is showing, and the inspected state remains until the handoff is completed or the code expires. @@ -1346,7 +1322,7 @@ A code that is not exactly 12 characters from the handoff alphabet after hyphens ### Side effects -A successful lookup consumes one of the code's three lookups and records the code as inspected, which is the state [complete desktop handoff](#complete-desktop-handoff) requires. That record lasts for the remaining life of the code, and only completion and cancellation remove it. An expired result records one failed attempt against the client IP address. No account state changes and no Gateway Dispatch is emitted. +A successful lookup consumes one of the code's three lookups and records the code as inspected, which is the state [complete desktop handoff](#complete-desktop-handoff) requires. That record lasts for the remaining life of the code, and only completion and cancellation remove it. An expired result records one failed attempt against the client IP address. No account state changes. No Gateway Dispatch is emitted. ### Rate limit @@ -1378,9 +1354,9 @@ Fluxer reads that token from the `Authorization` header, or from the body `token Sending the token in `token` puts a live credential into a JSON payload. A client SHOULD use the `Authorization` header and omit `token`. ::: -A token that resolves to no live session returns 401 `INVALID_TOKEN`. A code that is not exactly 12 characters from the handoff alphabet after hyphens and whitespace are removed returns 400 `INVALID_HANDOFF_CODE`. A code that has never been inspected through [get desktop handoff information](#get-desktop-handoff-information), an unknown or already completed code, and a request from a client IP address that has exhausted its failed-attempt allowance all return the same error code. A code whose five minutes have elapsed is discarded with its handoff and returns `INVALID_HANDOFF_CODE`. `HANDOFF_CODE_EXPIRED` is returned only when the stored handoff is still present after its five minutes. +A token that resolves to no live session returns 401 `INVALID_TOKEN`. A code that is not exactly 12 characters from the handoff alphabet after hyphens and whitespace are removed returns 400 `INVALID_HANDOFF_CODE`. A code that has never been inspected through [get desktop handoff information](#get-desktop-handoff-information) returns that same error code. So do an unknown or already completed code and a request from a client IP address that has exhausted its failed-attempt allowance. A code whose five minutes have elapsed is discarded with its handoff and returns `INVALID_HANDOFF_CODE`. `HANDOFF_CODE_EXPIRED` is returned only when the stored handoff is still present after its five minutes. -Session creation can additionally return 403 `BOT_USER_AUTH_SESSION_CREATION_DENIED` for a bot account, 403 `REGISTRATION_PENDING_APPROVAL` or 403 `REGISTRATION_REJECTED` for an account that has not been admitted, and the suspension codes listed under [log in with a password](#log-in-with-a-password) for a suspended account. +Session creation can also return 403 `BOT_USER_AUTH_SESSION_CREATION_DENIED` for a bot account, and 403 `REGISTRATION_PENDING_APPROVAL` or 403 `REGISTRATION_REJECTED` for an account that has not been admitted. A suspended account returns the suspension codes listed under [log in with a password](#log-in-with-a-password). ### Response @@ -1418,7 +1394,7 @@ Reports the state of a handoff to the device that initiated it. Authentication i | code | string | The handoff code, using the same normalisation contract as [get desktop handoff information](#get-desktop-handoff-information) | :::note[This route never returns the token] -A completed handoff reports `pending` here, because the token needs `poll_secret`. Read it with [get desktop handoff status with the poll secret](#get-desktop-handoff-status-with-the-poll-secret). +The token needs `poll_secret`, so a completed handoff reports `pending` here. Read it with [get desktop handoff status with the poll secret](#get-desktop-handoff-status-with-the-poll-secret). ::: The code is the only credential this route checks. The initiating device MUST poll for the completion itself. diff --git a/fluxer_docs/src/content/docs/http-api/billing.mdx b/fluxer_docs/src/content/docs/http-api/billing.mdx index 194412709..3d2eca9a4 100644 --- a/fluxer_docs/src/content/docs/http-api/billing.mdx +++ b/fluxer_docs/src/content/docs/http-api/billing.mdx @@ -6,9 +6,9 @@ description: Checkout, card preapproval, gift purchase, age verification, refund import RouteHeader from '@/components/RouteHeader.astro'; -These routes take payment for premium, verify an account holder's age, and refund the most recent purchase. Each of those finishes on the payment provider's own pages, so a route here creates the provider session and returns its URL for the browser to open. [Receive Stripe webhook](#receive-stripe-webhook) takes the signed events the provider sends back. The [Premium resource](/http-api/premium/) owns entitlement state, mirrored billing data and subscription self-service. +These routes take payment for premium, verify an account holder's age, and refund the most recent purchase. Each finishes on the payment provider's own pages, so a route here creates the provider session and returns its URL for the browser to open. [Receive Stripe webhook](#receive-stripe-webhook) takes the signed events the provider sends back. The [Premium resource](/http-api/premium/) owns entitlement state, mirrored billing data and subscription self-service. -Every route here is hosted-only, as [deployment availability](/http-api/deployment-availability/) describes. Every route except [receive Stripe webhook](#receive-stripe-webhook) and [continue localised card preapproval](#continue-localised-card-preapproval) is user-only. +Every route here is hosted-only, as [deployment availability](/http-api/deployment-availability/) describes. All of them are user-only except [receive Stripe webhook](#receive-stripe-webhook) and [continue localised card preapproval](#continue-localised-card-preapproval). :::note[Every response is a Fluxer object] The responses are Fluxer redirect, eligibility, refund and acknowledgement objects. The only payment provider values in them are the identifiers on a [refund](#refund-object). @@ -24,7 +24,7 @@ One absolute URL that completes a billing operation in a browser. [Create subscr | --- | --- | --- | | url1 | string | Absolute URL that completes the operation in a browser | -1 Ordinarily an externally hosted payment provider session URL. [Create subscription checkout](#create-subscription-checkout) can answer with the Fluxer premium callback URL instead +1 Usually an externally hosted payment provider session URL. [Create subscription checkout](#create-subscription-checkout) can answer with the Fluxer premium callback URL instead ### Example @@ -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, so 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 four variants the object is, and each variant defines its own members. ### Structure @@ -85,7 +85,7 @@ The state of one card preapproval flow. A localised recurring price is offered o ## Refund eligibility object -The verdict on whether the account's most recent purchase can still be refunded without operator involvement. When no refundable purchase was resolved, `eligible` is false, every invoice member is null, and `cooldown_expires_at` still reports an active cooldown unless the reason is `feature_unavailable`. A client reads `eligible` and `reason` before rendering any amount or timestamp. +Whether the account's most recent purchase can still be refunded without operator involvement. When no refundable purchase was resolved, `eligible` is false, every invoice member is null, and `cooldown_expires_at` still reports an active cooldown unless the reason is `feature_unavailable`. A client reads `eligible` and `reason` before rendering any amount or timestamp. ### Structure @@ -137,7 +137,7 @@ The verdict on whether the account's most recent purchase can still be refunded | --- | --- | | no_refundable_purchase | The account has no paid invoice with a positive amount and a resolvable payment reference among the invoices considered | | outside_refund_window | The latest refundable purchase settled more than 3 days ago | -| cooldown_active | The account performed a self-service refund less than 30 days ago | +| cooldown_active | The account made a self-service refund less than 30 days ago | | feature_unavailable | The deployment is self-hosted, or the payment provider is not configured, so self-service refunds are not offered | ## Refund object @@ -220,7 +220,7 @@ Creates a recurring premium checkout session and returns a [redirect URL](#redir Fluxer evaluates the lifetime refusal before the unclaimed account, unverified email and purchase flag refusals, so a lifetime Visionary account submitting a recurring price always receives the reason `lifetime` whatever its account state. -An account already holding an `active` or `trialing` subscription can submit a registered recurring non-gift price for the other billing cycle. Fluxer schedules a billing cycle change to the submitted cycle at the end of the current period, exactly as [change subscription billing cycle](/http-api/premium/#change-subscription-billing-cycle) with `effective_at` set to `period_end` does. The response is 200 with `url` set to the Fluxer premium callback URL. +An account already holding an `active` or `trialing` subscription can submit a registered recurring non-gift price for the other billing cycle. Fluxer then schedules a billing cycle change to the submitted cycle at the end of the current period. That is exactly what [change subscription billing cycle](/http-api/premium/#change-subscription-billing-cycle) with `effective_at` set to `period_end` does. The response is 200 with `url` set to the Fluxer premium callback URL. The conversion requires a stored billing cycle on the account that differs from the submitted price's cycle. Any other blocking subscription status, no stored billing cycle, and a matching cycle all still produce 403 `PREMIUM_PURCHASE_BLOCKED`. When scheduling the change fails, the caller receives that failure. @@ -235,12 +235,12 @@ A blocked purchase always fails on [create localised card preapproval](#create-l | price_id | string | Registered recurring price ID (1-256 characters) | | country_code?1 | string | Two-letter country used to select the regional price catalogue (2 characters) | | client_geoip_country_code?2 | string | Two-letter country the client previously observed for itself (2 characters) | -| eu_withdrawal_waiver_accepted?3 | boolean | Whether the digital content withdrawal waiver was expressly accepted | +| eu_withdrawal_waiver_accepted?3 | boolean | Whether the digital content withdrawal waiver was explicitly accepted | | pricing_mode? | string | Either `localized` or `base` (default `localized`) | | payment_method?4 | string | Either `card`, `pix` or `upi` (default `card`) | | is_business? | boolean | Whether to require a billing address for tax invoicing (default false) | -1 Required whenever the resolved price is not denominated in USD or EUR, and validated against the price catalogue for that country +1 Required whenever the resolved price is not in USD or EUR, and validated against the price catalogue for that country 2 Read to resolve the effective country for the withdrawal waiver, and recorded with the payment @@ -261,7 +261,7 @@ A blocked purchase always fails on [create localised card preapproval](#create-l ### Side effects -The operation creates a payment provider customer when the account has none, records the customer identifier on the account, and reconciles the account's stored subscription identifier with whatever the provider reports for that customer. Where a blocking subscription can still provision premium, Fluxer repairs the account's premium type, start, end, cancellation flag, billing cycle and grace deadline before it refuses the request. +The operation creates a payment provider customer when the account has none and records the customer identifier on the account. It also reconciles the account's stored subscription identifier with whatever the provider reports for that customer. Where a blocking subscription can still grant premium, Fluxer repairs the account's premium type, start, end, cancellation flag, billing cycle and grace deadline before it refuses the request. It then creates an externally hosted checkout with terms of service consent, the withdrawal waiver text attached to that consent, automatic tax, tax identifier collection and promotion codes. It records a pending payment row with the resolved countries and the waiver decision. @@ -282,7 +282,7 @@ Creates a setup mode session that captures and verifies a card before a localise ### Limitations - The claimed account, verified email and purchase flag requirements of [create subscription checkout](#create-subscription-checkout) apply unchanged, with the same codes. -- An absent `country_code`, a `pricing_mode` of `base`, and a resolved price that is not a recurring price denominated in a currency other than USD and EUR each return 400 `STRIPE_INVALID_PRODUCT_CONFIGURATION`. +- An absent `country_code`, a `pricing_mode` of `base`, and a resolved price that is not a recurring price in a currency other than USD and EUR each return 400 `STRIPE_INVALID_PRODUCT_CONFIGURATION`. - The submitted price is always recurring, so the lifetime block and the existing subscription block both apply and return 403 `PREMIUM_PURCHASE_BLOCKED` with the reason `lifetime` or `existing_subscription`. ### JSON body @@ -292,7 +292,7 @@ Creates a setup mode session that captures and verifies a card before a localise | price_id | string | Registered localised recurring price ID (1-256 characters) | | country_code1 | string | Two-letter country used to select the regional price catalogue (2 characters) | | client_geoip_country_code? | string | Two-letter country the client previously observed for itself (2 characters) | -| eu_withdrawal_waiver_accepted? | boolean | Whether the digital content withdrawal waiver was expressly accepted | +| eu_withdrawal_waiver_accepted? | boolean | Whether the digital content withdrawal waiver was explicitly accepted | | pricing_mode?2 | string | Either `localized` or `base` (default `localized`) | | is_business? | boolean | Whether to require a billing address for tax invoicing (default false) | @@ -315,9 +315,9 @@ A submitted `payment_method` is validated against the enum and then discarded. T ### Side effects -The operation creates a payment provider customer when necessary and reconciles subscription state exactly as [create subscription checkout](#create-subscription-checkout) does, then creates an externally hosted setup session restricted to cards. +The operation creates a payment provider customer when the account has none and reconciles subscription state exactly as [create subscription checkout](#create-subscription-checkout) does, then creates an externally hosted setup session restricted to cards. -It also records a preapproval flow keyed by a freshly generated continuation token, holding the price, country, business flag, waiver decision and observed countries. The flow expires one day after its most recent state change. The token is returned in the session's success callback URL. +It also records a preapproval flow keyed by a newly generated continuation token, holding the price, country, business flag, waiver decision and observed countries. The flow expires one day after its most recent state change. The token is returned in the session's success callback URL. The flow stays pending until [receive Stripe webhook](#receive-stripe-webhook) processes the matching completion event. It is approved only when the card issuing country equals the pricing country. Otherwise the flow records a [preapproval rejection reason](#preapproval-rejection-reasons), and for `country_mismatch` the detected country. No entitlement changes. @@ -341,7 +341,7 @@ An empty token, an unknown token, and a token whose one-day flow has expired are - A recorded price that no longer belongs to the recorded country catalogue returns 400 `STRIPE_INVALID_PRODUCT_CONFIGURATION`. - The claimed account, verified email, purchase flag, lifetime and existing subscription refusals all apply to the account that opened the flow. -Creating the paid session repeats the complete preparation [create subscription checkout](#create-subscription-checkout) performs, against the account, price and country recorded on the flow. +Creating the paid session repeats the complete preparation that [create subscription checkout](#create-subscription-checkout) does, against the account, price and country recorded on the flow. :::caution[Approval is resolved once] One continuation at a time can resolve an approved flow. While one call creates the paid session, another call for the same flow is reported as `pending`. @@ -394,11 +394,11 @@ Neither the lifetime block nor the existing subscription block applies here, so | price_id | string | Registered gift price ID (1-256 characters) | | country_code?1 | string | Two-letter country used to select the regional gift price catalogue (2 characters) | | client_geoip_country_code? | string | Two-letter country the client previously observed for itself (2 characters) | -| eu_withdrawal_waiver_accepted? | boolean | Whether the digital content withdrawal waiver was expressly accepted | +| eu_withdrawal_waiver_accepted? | boolean | Whether the digital content withdrawal waiver was explicitly accepted | | pricing_mode? | string | Either `localized` or `base` (default `localized`) | | is_business? | boolean | Whether to require a billing address for tax invoicing (default false) | -1 Required whenever the resolved gift price is not denominated in USD or EUR +1 Required whenever the resolved gift price is not in USD or EUR A submitted `pix` or `upi` is validated against the enum and then discarded. The session restricts no payment method type, so the provider offers whatever the deployment's provider account accepts for the resolved currency. @@ -450,7 +450,7 @@ The session requests 3-D Secure authentication on the card and takes no payment. The operation creates a payment provider customer when necessary, records the customer identifier on the account, and creates an externally hosted setup session restricted to cards. -Adult age verification is granted only after [receive Stripe webhook](#receive-stripe-webhook) processes the matching completion event and confirms a credit card. That later grant sets the age-verified adult flag and sends [User Update](/gateway/events/#user-update) to every session owned by the account. +Adult age verification is granted only after [receive Stripe webhook](#receive-stripe-webhook) processes the matching completion event and confirms a credit card. The grant sets the age-verified adult flag and sends [User Update](/gateway/events/#user-update) to every session owned by the account. ### Rate limit @@ -464,7 +464,7 @@ Returns the [refund eligibility](#refund-eligibility-object) object for the auth A deployment with no configured payment provider answers with `eligible` false and the reason `feature_unavailable`. Otherwise the operation lists the five most recent invoices for the account's payment provider customer and selects the first that is paid, has a positive amount, and resolves a payment intent or a charge. -A provider listing failure is logged and treated as no refundable purchase, and the request still succeeds. An account that has never had a payment provider customer provisioned is treated the same way. +A provider listing failure is logged and treated as no refundable purchase, and the request still succeeds. An account that has never had a payment provider customer created is treated the same way. :::note[The same object appears inside premium state] [Get premium state](/http-api/premium/#get-premium-state) returns this object as `billing.refund_eligibility`, computed from the mirrored invoices, so the two can disagree while the mirror is behind. @@ -515,7 +515,7 @@ Once the provider confirms the refund succeeded, the subscription that produced | 403 | [error response](/http-api/#error-response) | The purchase is outside the refund window, or the refund cooldown is active | | 404 | [error response](/http-api/#error-response) | The authenticated account record no longer exists | -1 The payment provider is not configured, or the payment method could not be determined or the provider rejected the refund, both of which return `STRIPE_ERROR` +1 The payment provider is not configured, or the payment method could not be determined, or the provider rejected the refund. The last two return `STRIPE_ERROR` ### Side effects @@ -533,12 +533,12 @@ A confirmed refund that cancels the account's current subscription clears the st -Authenticates a payment provider event against the exact raw request body and enqueues it for asynchronous processing, returning a [webhook received](#webhook-received-object) object. The signature header is the credential. +Authenticates a payment provider event against the exact raw request body and queues it for asynchronous processing, returning a [webhook received](#webhook-received-object) object. The signature header is the credential. ### Limitations - A missing signature header returns 400 `STRIPE_WEBHOOK_SIGNATURE_MISSING`. -- A deployment with no configured payment provider client or webhook secret then returns 400 `STRIPE_WEBHOOK_NOT_AVAILABLE`. +- A deployment with no payment provider client or webhook secret configured then returns 400 `STRIPE_WEBHOOK_NOT_AVAILABLE`. - A signature header that does not verify returns 401 `STRIPE_WEBHOOK_SIGNATURE_INVALID`. The route verifies the signature with the payment provider library's default timestamp tolerance. @@ -557,18 +557,18 @@ A proxy that re-encodes, reorders or pretty-prints the payload invalidates the s | Status | Body | Condition | | --- | --- | --- | -| 200 | [webhook received](#webhook-received-object) object | The event was authenticated and enqueued | +| 200 | [webhook received](#webhook-received-object) object | The event was authenticated and queued | | 400 | [error response](/http-api/#error-response) | The signature header is missing, or webhook processing is not configured for this deployment | | 401 | [error response](/http-api/#error-response) | The signature did not verify against the raw body | -| 500 | [error response](/http-api/#error-response) | The event could not be enqueued | +| 500 | [error response](/http-api/#error-response) | The event could not be queued | ### Side effects -The response precedes fulfilment, and a redelivered event is applied once. +The response comes before processing, and a redelivered event is applied once. -A recognised event first refreshes the local mirror of its provider object, covering customers, products, prices, checkout sessions, subscriptions, invoices, payment intents, payment methods, charges, refunds and disputes. That mirror is what [Get premium state](/http-api/premium/#get-premium-state) reads. +A recognised event first refreshes the local mirror of its provider object, covering customers, products, prices, checkout sessions, subscriptions, invoices, payment intents, payment methods, charges, refunds and disputes. [Get premium state](/http-api/premium/#get-premium-state) reads that mirror. -Later processing can create a purchased gift, grant or extend premium, settle or fail a checkout, update invoice failure state, record or clear the post-cancellation grace deadline, apply subscription changes, and resolve a card preapproval. It also completes a donation and sends its confirmation email, and completes adult age verification. A dispute or fraud warning reverses a gift and emails the redeemer whose entitlement was withdrawn, and a dispute that closes in Fluxer's favour restores the account and emails that account holder. A refund reverses entitlement and settles the self-service refund. +Later processing can create a purchased gift, grant or extend premium, settle or fail a checkout, and update invoice failure state. It can also record or clear the post-cancellation grace deadline, apply subscription changes, resolve a card preapproval, complete a donation and send its confirmation email, and complete adult age verification. A dispute or fraud warning reverses a gift and emails the redeemer whose entitlement was withdrawn. A dispute that closes in Fluxer's favour restores the account and emails that account holder. A refund reverses entitlement and settles the self-service refund. Each account-addressed message is sent only when the receiving account has an email address. The resulting account changes reach connected sessions as [User Update](/gateway/events/#user-update). diff --git a/fluxer_docs/src/content/docs/http-api/calls.mdx b/fluxer_docs/src/content/docs/http-api/calls.mdx index 8cbe3e195..3f8fecffa 100644 --- a/fluxer_docs/src/content/docs/http-api/calls.mdx +++ b/fluxer_docs/src/content/docs/http-api/calls.mdx @@ -41,7 +41,7 @@ Fluxer computes eligibility for one caller against one private channel at the mo | Field | Type | Description | | --- | --- | --- | | ringable1 | boolean | Whether the authenticated user can initiate an audible ring in this channel | -| silent2 | boolean | Whether a newly initiated call notifies the other recipient without audible ringing | +| silent2 | boolean | Whether a newly started call notifies the other recipient without audible ringing | 1 False when the caller is already connected to the channel's call, when a direct message caller has never claimed its credentials, and when the other recipient's incoming call policy excludes the caller @@ -124,7 +124,7 @@ Any other unknown or inaccessible `region` returns 400 `INVALID_FORM_BODY` with The body can be omitted. Fluxer treats a missing, empty, or whitespace-only body as an empty object, which requests no change. A body that is not valid JSON returns 400 `INVALID_FORM_BODY` with the validation code `INVALID_FORMAT` at the `body` path. -A region identifier is the `id` of an [RTC region object](/http-api/channels/#rtc-region-object). No route enumerates the regions a call accepts, and [List RTC regions](/http-api/channels/#list-rtc-regions) answers only for a guild voice channel. +A region identifier is the `id` of an [RTC region object](/http-api/channels/#rtc-region-object). No route returns the regions a call accepts, and [List RTC regions](/http-api/channels/#list-rtc-regions) answers only for a guild voice channel. A region or voice server is unselectable for a call on any of these grounds: @@ -183,7 +183,7 @@ The body can be omitted. Fluxer treats a missing, empty, or whitespace-only body An identifier that names a non-recipient or the caller itself returns 400 `INVALID_FORM_BODY` with the validation code `USER_NOT_IN_CHANNEL`. A repeated identifier is accepted and rings that recipient once. -A named recipient is only rung when the incoming call policy described by [Get call eligibility](#get-call-eligibility) admits the caller audibly, so a recipient admitted only under the silent-everyone flag is notified without being rung. In a direct message Fluxer ignores the named set when choosing whom to ring and always evaluates the policy against the other recipient. +A named recipient is only rung when the incoming call policy described by [Get call eligibility](#get-call-eligibility) admits the caller audibly. A recipient admitted only under the silent-everyone flag is notified without being rung. In a direct message Fluxer ignores the named set when choosing whom to ring and always evaluates the policy against the other recipient. ### Response @@ -201,15 +201,15 @@ A named recipient is only rung when the incoming call policy described by [Get c Fluxer first reopens the private channel for the caller and for every notified recipient, and each account for which the channel was closed receives [Channel Create](/gateway/events/#channel-create). -When no call exists, Fluxer then stores a call system message with the initial participant set and creates the call with [Call Create](/gateway/events/#call-create) that has the initial ringing set and the automatically selected region. It raises the unread mention count of every other recipient that is not a bot and has not blocked the caller. It acknowledges the message for the initiating user without a Dispatch, and only then delivers the stored message with [Message Create](/gateway/events/#message-create). Fluxer sends [Call Create](/gateway/events/#call-create) before [Message Create](/gateway/events/#message-create). +When no call exists, Fluxer then stores a call system message with the initial participant set and creates the call with [Call Create](/gateway/events/#call-create). The event has the initial ringing set and the automatically selected region. Fluxer raises the unread mention count of every other recipient that is not a bot and has not blocked the caller. It acknowledges the message for the initiating user without a Dispatch, and only then delivers the stored message with [Message Create](/gateway/events/#message-create). Fluxer sends [Call Create](/gateway/events/#call-create) before [Message Create](/gateway/events/#message-create). When a call already exists, Fluxer instead extends the ringing set and emits [Call Update](/gateway/events/#call-update) when the set grows. A recipient who is already connected to the call is never added to the ringing set. -Each rung recipient holds a ringing entry for 30 seconds. Fluxer then drops the entry and emits [Call Update](/gateway/events/#call-update) again. When that expiry leaves the call with no connected participant and no other ringing recipient, Fluxer removes the call in the same step, which emits [Call Delete](/gateway/events/#call-delete) and stamps the call system message with its ended timestamp. A call that rang at least one recipient and that nobody joined therefore ends 30 seconds after the ring. +Each rung recipient holds a ringing entry for 30 seconds. Fluxer then drops the entry and emits [Call Update](/gateway/events/#call-update) again. When that expiry leaves the call with no connected participant and no other ringing recipient, Fluxer removes the call in the same step. That emits [Call Delete](/gateway/events/#call-delete) and stamps the call system message with its ended timestamp. A call that rang at least one recipient and that nobody joined therefore ends 30 seconds after the ring. -Fluxer arms no ring timer for a call created with an empty ringing set, so that call ends on the 120 second idle timer instead. An explicit empty `recipients` array produces such a call, and so does a ring that admits no candidate audibly. +A call created with an empty ringing set has no ring timer, so it ends on the 120 second idle timer instead. An explicit empty `recipients` array produces such a call, and so does a ring that admits no candidate audibly. -Whenever Fluxer removes a call, it rewrites that call's system message with the ended timestamp and with every account that ever connected, and publishes the rewritten message to every recipient of the private channel as [Message Update](/gateway/events/#message-update). +Whenever Fluxer removes a call, it rewrites that call's system message with the ended timestamp and with every account that ever connected. It publishes the rewritten message to every recipient of the private channel as [Message Update](/gateway/events/#message-update). The call's recipient list is fixed when the call is created. Changing the recipient set of a group direct message afterwards neither ends the call nor updates that list. [Add group direct message recipient](/http-api/channels/#add-group-direct-message-recipient), [Remove group direct message recipient](/http-api/channels/#remove-group-direct-message-recipient), and [Delete or leave channel](/http-api/channels/#delete-or-leave-channel) leave a running call in place. A recipient added later receives no Dispatch for it. @@ -290,7 +290,7 @@ A participant leaves by sending [Voice State Update](/gateway/commands/#voice-st | --- | --- | --- | | 204 | empty | Request was accepted | -A call ends the moment its last connected participant leaves, whether or not a recipient is still ringing. Fluxer publishes [Call Delete](/gateway/events/#call-delete) at that moment. A call that no participant ever joined ends instead when its last ringing entry expires, and a call whose ringing set was emptied by [Stop ringing call recipients](#stop-ringing-call-recipients) ends on the Gateway's 120 second idle timer. +A call ends the moment its last connected participant leaves, whether or not a recipient is still ringing. Fluxer publishes [Call Delete](/gateway/events/#call-delete) at that moment. A call that no participant ever joined ends instead when its last ringing entry expires. A call whose ringing set was emptied by [Stop ringing call recipients](#stop-ringing-call-recipients) ends on the Gateway's 120 second idle timer. ### Rate limit diff --git a/fluxer_docs/src/content/docs/http-api/channels.mdx b/fluxer_docs/src/content/docs/http-api/channels.mdx index 4a03fe09e..c9842976a 100644 --- a/fluxer_docs/src/content/docs/http-api/channels.mdx +++ b/fluxer_docs/src/content/docs/http-api/channels.mdx @@ -189,7 +189,7 @@ The slowmode state combines the channel's configured interval with the authentic ## RTC region object -An RTC region names one voice routing target the deployment operates. Its identifier is the value a guild voice channel stores in `rtc_region`, and the name and emoji exist only for presentation. +An RTC region names one voice routing target the deployment operates. Its identifier is the value a guild voice channel stores in `rtc_region`, and the name and emoji exist only for display. ### Structure @@ -217,8 +217,7 @@ Returns the [channel object](#channel-object) visible to the authenticated user. ### Limitations -- A guild channel requires [VIEW_CHANNEL](/http-api/permissions/). -- A guild channel that resolves to an age restriction also requires a satisfied age verification. +- A guild channel requires [VIEW_CHANNEL](/http-api/permissions/), and a satisfied age verification when it resolves to an age restriction. - A private channel requires current recipient access. - The personal notes channel requires ownership. @@ -241,12 +240,12 @@ Fluxer enforces age verification only for a guild text, voice, or link channel. | 404 | [error response](/http-api/#error-response) | Channel does not exist, or the caller is not a recipient of the private channel, each returning `UNKNOWN_CHANNEL`, or the guild that owns the channel does not exist and the request returns `UNKNOWN_GUILD` | :::note[The guild resolves after the channel] -A channel whose guild is gone fails with `UNKNOWN_GUILD` rather than `UNKNOWN_CHANNEL`. +A channel whose guild is gone fails with `UNKNOWN_GUILD`, not `UNKNOWN_CHANNEL`. ::: ### Side effects -Requesting the personal notes channel of the authenticated user creates that channel when it does not already exist, and returns it directly without a [Channel Create](/gateway/events/#channel-create) Dispatch. +Requesting the personal notes channel of the authenticated user creates it when it does not already exist, and returns it with no [Channel Create](/gateway/events/#channel-create) Dispatch. ### Rate limit @@ -276,7 +275,7 @@ Returns the authenticated user's [channel slowmode state object](#channel-slowmo ### Side effects -The read does not advance the caller's slowmode countdown. Requesting the personal notes channel of the authenticated user creates that channel when it does not already exist, with no [Channel Create](/gateway/events/#channel-create) Dispatch. +The read does not advance the caller's slowmode countdown. Requesting the personal notes channel of the authenticated user creates it when it does not already exist, with no [Channel Create](/gateway/events/#channel-create) Dispatch. ### Rate limit @@ -341,7 +340,7 @@ Modifies a guild channel or a group direct message and returns the updated [chan The submitted array becomes the complete overwrite collection of the channel. ::: -[Set channel permission overwrite](#set-channel-permission-overwrite) and [Delete channel permission overwrite](#delete-channel-permission-overwrite) each mutate one overwrite and leave the rest untouched. +[Set channel permission overwrite](#set-channel-permission-overwrite) and [Delete channel permission overwrite](#delete-channel-permission-overwrite) each change one overwrite and leave the rest untouched. ### Path parameters @@ -454,7 +453,7 @@ Fluxer trims a stored nickname, and a value that is null or empty after trimming ### Side effects -A guild channel change emits [Channel Update](/gateway/events/#channel-update) to every session subscribed to the guild. Replacing a category's permission overwrites also replaces them on each child whose overwrites still exactly match the category's previous values, with one Dispatch per changed child. A child whose overwrites had diverged is left unchanged. +A guild channel change emits [Channel Update](/gateway/events/#channel-update) to every session subscribed to its guild. Replacing a category's permission overwrites also replaces them on each child whose overwrites still exactly match the category's previous values, with one Dispatch per changed child. A child whose overwrites had differed is left unchanged. Changing `rate_limit_per_user` clears the channel's current slowmode state so the new interval applies immediately. Changing `rtc_region` on a guild voice channel moves its active voice connections to the selected region. @@ -470,7 +469,7 @@ A group direct message change emits [Channel Update](/gateway/events/#channel-up -Deletes a guild channel, closes a direct message, or removes the caller from a group direct message, and returns 204 with an empty body. Emits a [Channel Delete](/gateway/events/#channel-delete) Gateway event. +Deletes a guild channel, closes a direct message, or removes the caller from a group direct message. Returns 204 with an empty body. Emits a [Channel Delete](/gateway/events/#channel-delete) Gateway event. ### Limitations @@ -569,16 +568,15 @@ Adds a user to a group direct message and returns 204 with an empty body. Emits ### Limitations -- The caller must be a current recipient of the group. -- The caller and the target must be friends, including when the caller is a bot. +- The caller must be a current recipient of the group, and must be friends with the target, including when the caller is a bot. - The target's group direct message admission policy must allow the caller. - An instance with CAPTCHA enabled also requires a CAPTCHA proof. Fluxer evaluates the target's admission policy in this order. 1. A target that has never stored settings admits every caller. -2. A nobody setting rejects every caller. An everyone setting admits every caller. -3. A friends-only setting admits only a caller the target has as a friend. +2. The nobody setting rejects every caller, and the everyone setting admits every caller. +3. The friends-only setting admits only a caller the target has as a friend. 4. Otherwise Fluxer admits the caller through an existing friendship, then through a mutual friend when the friends-of-friends setting is enabled, and then through a mutual guild when the guild-members setting is enabled. ### Path parameters @@ -610,7 +608,7 @@ Fluxer evaluates the target's admission policy in this order. | 403 | [error response](/http-api/#error-response) | Caller is not a recipient, or the target's admission policy rejects the caller, each returning `MISSING_ACCESS` | | 404 | [error response](/http-api/#error-response) | Channel does not exist and the request returns `UNKNOWN_CHANNEL` | -The `MAX_GROUP_DM_RECIPIENTS` body has `max_recipients` reporting the exact ceiling that was reached. That ceiling is the deployment's [`max_group_dm_recipients`](/http-api/instance/#limit-keys) limit resolved for the caller, and it defaults to 50. +The `MAX_GROUP_DM_RECIPIENTS` body has `max_recipients`, the exact ceiling that was reached. That ceiling is the deployment's [`max_group_dm_recipients`](/http-api/instance/#limit-keys) limit resolved for the caller, and it defaults to 50. ### Side effects @@ -629,7 +627,7 @@ Removes a user from a group direct message and returns 204 with an empty body. E ### Limitations - The caller must be a current recipient. -- A caller may remove themself regardless of ownership. +- A caller may remove themselves regardless of ownership. - Removing another recipient requires ownership of the group. - Setting `delete_messages` takes effect only when `user_id` is the caller, and it then requires sudo verification on the same terms as [Delete or leave channel](#delete-or-leave-channel). @@ -696,7 +694,7 @@ Creates or replaces one permission overwrite on a guild channel and returns 204 The operation does not require `VIEW_CHANNEL` and never returns `TWO_FACTOR_REQUIRED` or `NSFW_CONTENT_AGE_RESTRICTED`. -:::note[The identifier alone keys the overwrite] +:::note[The identifier alone selects the overwrite] `type` records whether the identifier names a role or a member, and does not change which entry is written. An overwrite can be stored for an identifier that names nothing, and [Delete channel permission overwrite](#delete-channel-permission-overwrite) removes it. ::: @@ -757,7 +755,7 @@ Deletes one permission overwrite from a guild channel and returns 204 with an em - A caller without [ADMINISTRATOR](/http-api/permissions/) must already hold every permission bit the removed overwrite denied. -The operation is idempotent. It does not require `VIEW_CHANNEL` and never returns `TWO_FACTOR_REQUIRED` or `NSFW_CONTENT_AGE_RESTRICTED`. +The operation is idempotent, does not require `VIEW_CHANNEL`, and never returns `TWO_FACTOR_REQUIRED` or `NSFW_CONTENT_AGE_RESTRICTED`. ### Path parameters @@ -766,7 +764,7 @@ The operation is idempotent. It does not require `VIEW_CHANNEL` and never return | channel_id | snowflake | The ID of the guild channel | | overwrite_id1 | snowflake | The ID of the role or member the overwrite represents | -1 The identifier alone selects the overwrite. The request has no overwrite type +1 The identifier alone selects the overwrite, and the request has no overwrite type ### Response diff --git a/fluxer_docs/src/content/docs/http-api/connections.mdx b/fluxer_docs/src/content/docs/http-api/connections.mdx index fe119ad7c..a320066e7 100644 --- a/fluxer_docs/src/content/docs/http-api/connections.mdx +++ b/fluxer_docs/src/content/docs/http-api/connections.mdx @@ -52,11 +52,11 @@ A `bsky` connection is identified by the account's decentralised identifier whil | bsky1 | BLUESKY | A Bluesky account authorised through atproto OAuth | | domain | DOMAIN | A domain the account has proved it controls | -1 [Initiate connection](#initiate-connection) rejects this type with 400 `BLUESKY_OAUTH_NOT_ENABLED` before it consults the connection ceiling. A Bluesky connection is created only by completing the flow that begins at [Start Bluesky authorisation](#start-bluesky-authorisation) +1 [Initiate connection](#initiate-connection) rejects this type with 400 `BLUESKY_OAUTH_NOT_ENABLED` before it checks the connection ceiling. A Bluesky connection is created only by completing the flow that begins at [Start Bluesky authorisation](#start-bluesky-authorisation) ## Connection visibility flags -The flags of one connection decide which viewers of the owner's profile see it. Fluxer evaluates them against a viewer with the same rule as the [profile field privacy flags](/http-api/users/#profile-field-privacy-flags). +A connection's flags decide which viewers of the owner's profile see it. Fluxer evaluates them against a viewer with the same rule as the [profile field privacy flags](/http-api/users/#profile-field-privacy-flags). | Value | Name | Description | | --- | --- | --- | @@ -100,7 +100,7 @@ Fluxer attempts a `domain` proof against the stored identifier exactly as it was 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. -When DNS does not prove ownership, 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. +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. The instance outbound URL policy applies to the initial request and to every redirect it follows. A redirect target must use the `http` or `https` scheme. Fluxer lowercases the host and strips one trailing dot before the check. @@ -108,7 +108,7 @@ A host that is an IP literal cannot sit in a loopback, private, shared, link-loc Any other host must be a fully qualified domain name of at most 253 characters with at least one dot. Every label must be 1 to 63 characters of ASCII letters, digits, and hyphens that neither begins nor ends with a hyphen. The final label cannot consist only of digits, and every address the host resolves to must be a public internet address. A host that fails any of those checks fails the proof, so a domain pointed at a loopback, link-local, or private range is never provable this way. -[Verify and create connection](#verify-and-create-connection) takes the token from the signed initiation token, and [Verify connection](#verify-connection) reuses the connection's stored verification token, so a recheck expects the same DNS record or document as the original proof. +[Verify and create connection](#verify-and-create-connection) takes the token from the signed initiation token. [Verify connection](#verify-connection) reuses the connection's stored verification token, so a recheck expects the same DNS record or document as the original proof. :::note[Only the DNS record has the prefix] The DNS record value has the `fluxer-verification=` prefix and the well-known document does not. A document that contains the prefixed form fails the HTTPS proof. @@ -134,7 +134,7 @@ Returns every [connection object](#connection-object) the authenticated account Fluxer checks the scope before it checks the account state, so a bearer without `connections` is rejected with 403 `MISSING_OAUTH_SCOPE` even when the account also has an outstanding required action. -1 The sort is stable and has no secondary key, so two connections that share a `sort_order` are returned in their stored order, which is ascending by [connection type](#connection-types) and then descending by `id` +1 The sort is stable and has no secondary key, so two connections sharing a `sort_order` are returned in stored order, ascending by [connection type](#connection-types) and then descending by `id` ### Response @@ -175,13 +175,13 @@ Fluxer checks the requested type first, then the 20-connection ceiling, and then | Status | Body | Condition | | --- | --- | --- | -| 201 | [connection verification](#connection-verification-object) object | Verification was initiated | +| 201 | [connection verification](#connection-verification-object) object | Verification was started | | 400 | [error response](/http-api/#error-response) | The requested type is bsky and the request returns `BLUESKY_OAUTH_NOT_ENABLED` | | 409 | [error response](/http-api/#error-response) | A connection of the same type already exists for that identifier, compared case-insensitively, and the request returns `CONNECTION_ALREADY_EXISTS` | ### Side effects -This operation creates no connection and emits no Gateway Dispatch. It returns a signed initiation token that expires 30 minutes after issue and a deterministic verification token that the caller MUST publish at the domain before calling [Verify and create connection](#verify-and-create-connection). +The operation creates no connection and emits no Gateway Dispatch. It returns a signed initiation token that expires 30 minutes after issue and a verification token the caller MUST publish at the domain before calling [Verify and create connection](#verify-and-create-connection). ### Rate limit @@ -193,7 +193,7 @@ This operation creates no connection and emits no Gateway Dispatch. It returns a Checks the [domain ownership proof](#domain-ownership-proof) described by a signed initiation token. Creates and returns the [connection object](#connection-object) on success. -The type, identifier, and verification token all come from the signed token, so a caller cannot verify a target it did not initiate. Fluxer re-evaluates the 20-connection ceiling and the duplicate identifier before it attempts the proof, and a target that became a duplicate since the token was issued is refused with no DNS or HTTPS lookup. +The type, identifier, and verification token all come from the signed token, so a caller cannot verify a target it did not initiate. Fluxer re-evaluates the 20-connection ceiling and the duplicate identifier before it attempts the proof. A target that became a duplicate since the token was issued is refused with no DNS or HTTPS lookup. ### JSON body @@ -256,7 +256,7 @@ Updates the visibility or display order of one existing connection and returns 2 ### Side effects -The supplied fields replace the corresponding connection values. The complete connection list is then published as a [User Connections Update](/gateway/events/#user-connections-update) Gateway Dispatch to the caller's own sessions. The Dispatch is emitted even when every supplied value already matched, and even when the body supplies no field at all. +Each supplied field replaces the stored value. The complete connection list is then published as a [User Connections Update](/gateway/events/#user-connections-update) Gateway Dispatch to the caller's own sessions. The Dispatch is emitted even when every supplied value already matched, and even when the body supplies no field at all. Assigning a `sort_order` that another connection already holds is permitted. [List connections](#list-connections) then resolves the tie by stored order. @@ -328,13 +328,13 @@ The handle a `bsky` recheck resolves proves the session is live and is then disc | 403 | [error response](/http-api/#error-response) | The proof failed and the request returns `CONNECTION_VERIFICATION_FAILED` | | 404 | [error response](/http-api/#error-response) | No connection of that type and identifier is owned by the caller and the request returns `CONNECTION_NOT_FOUND` | -1 Every other recheck failure is reported as a failed proof. The route reads the configured OAuth client alone and never consults the instance [service availability](/http-api/instance/#service-availability-object) flag `bluesky_enabled` +1 Every other recheck failure is reported as a failed proof. The route reads the configured OAuth client alone and never checks the instance [service availability](/http-api/instance/#service-availability-object) flag `bluesky_enabled` ### Side effects A successful proof sets `verified` to true, refreshes the last-verified timestamp, and sets the original verification timestamp to now when the connection had none. It then publishes the complete connection list as a [User Connections Update](/gateway/events/#user-connections-update) Gateway Dispatch to the caller's own sessions. -A failed proof against a connection that was verified sets `verified` to false, clears the original verification timestamp, and refreshes the last-verified timestamp. A failed proof against a connection that was already unverified writes nothing. Both publish the complete connection list as a [User Connections Update](/gateway/events/#user-connections-update) Gateway Dispatch, so every session of the account observes the current `verified` value. +A failed proof against a connection that was verified sets `verified` to false, clears the original verification timestamp, and refreshes the last-verified timestamp. A failed proof against an already unverified connection writes nothing. Both publish the complete connection list as a [User Connections Update](/gateway/events/#user-connections-update) Gateway Dispatch, so every session of the account observes the current `verified` value. ### Rate limit @@ -392,13 +392,13 @@ Fluxer trims surrounding whitespace, then removes a leading `https://bsky.app/pr | Status | Body | Condition | | --- | --- | --- | -| 2001 | [Bluesky authorisation](#bluesky-authorisation-object) object | Authorisation was initiated | +| 2001 | [Bluesky authorisation](#bluesky-authorisation-object) object | Authorisation was started | | 4002 | [error response](/http-api/#error-response) | Bluesky connections are unavailable on this instance and the request returns `BLUESKY_OAUTH_NOT_ENABLED`, or the flow could not be started and the request returns `BLUESKY_OAUTH_AUTHORIZATION_FAILED` | | 4093 | [error response](/http-api/#error-response) | A bsky connection whose name equals the normalised handle already exists and the request returns `CONNECTION_ALREADY_EXISTS` | -1 This operation does not consult the 20-connection ceiling, which is enforced only when the provider callback creates the connection +1 This operation does not check the 20-connection ceiling, which is enforced only when the provider callback creates the connection -2 Every authorisation failure other than an unavailable integration collapses into one code, including an unresolvable handle, a handle whose authorisation server is unreachable, and a rejected client registration, so a client MUST NOT derive a remedy from it +2 Every authorisation failure other than an unavailable integration collapses into one code, including an unresolvable handle, a handle whose authorisation server is unreachable, and a rejected client registration, so a client MUST NOT use it to choose a fix 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 @@ -416,9 +416,9 @@ The provider callback reads and deletes the state in one operation. A replay and ### Completing the flow -The returned `authorize_url` is an atproto authorisation server URL. The client sends the user there, the user approves the request, and the authorisation server calls the instance's registered redirect URI, `/connections/bluesky/callback` on the `api_public` [instance endpoint](/http-api/instance/#instance-endpoints-object). A client MUST NOT call that route directly. +A client sends the user to the returned `authorize_url`, which is an atproto authorisation server URL. The user approves the request, and the authorisation server calls the instance's registered redirect URI, `/connections/bluesky/callback` on the `api_public` [instance endpoint](/http-api/instance/#instance-endpoints-object). A client MUST NOT call that route directly. -Fluxer consumes the state, exchanges the authorisation code for an atproto session, resolves the account's decentralised identifier and current handle, writes the connection, and then redirects the browser to `/connection-callback` on the `webapp` [instance endpoint](/http-api/instance/#instance-endpoints-object). +Fluxer consumes the state and exchanges the authorisation code for an atproto session. It resolves the account's decentralised identifier and current handle, writes the connection, and then redirects the browser to `/connection-callback` on the `webapp` [instance endpoint](/http-api/instance/#instance-endpoints-object). That redirect has `status=connected` on success. On failure it has `status=error` together with one `reason` value. @@ -433,13 +433,13 @@ That redirect has `status=connected` on success. On failure it has `status=error 2 A replayed callback and a callback more than one hour after issue both land here -3 An account already holding 20 connections lands here, as does any unexpected internal failure, so a client MUST NOT derive a remedy from this value +3 An account already holding 20 connections lands here, as does any unexpected internal failure, so a client MUST NOT use this value to choose a fix -On success, an account that has no `bsky` connection for that decentralised identifier receives a new one with `verified` set to true, EVERYONE visibility, and the number of connections it already held as its order. An account that already has one keeps its identifier and `sort_order`, has its `name` rewritten to the current handle, and has `verified` set back to true. Either path publishes the complete connection list as a [User Connections Update](/gateway/events/#user-connections-update) Gateway Dispatch to that account's own sessions. +On success, an account with no `bsky` connection for that decentralised identifier receives a new one. The new connection has `verified` set to true, EVERYONE visibility, and the number of connections the account already held as its order. An account that already has one keeps its identifier and `sort_order`, has its `name` rewritten to the current handle, and has `verified` set back to true. Either path publishes the complete connection list as a [User Connections Update](/gateway/events/#user-connections-update) Gateway Dispatch to that account's own sessions. Completing the flow is the only way a `bsky` connection `name` is written, so it is the only way to reconcile an upstream handle rename. It renews the 24-hour atproto session that [Verify connection](#verify-connection) rechecks. -The callback does not consult the instance [service availability](/http-api/instance/#service-availability-object) flag `bluesky_enabled`. An authorisation started while the flag was set completes normally after an operator clears it, as long as the OAuth client is still configured. +The callback does not check the instance [service availability](/http-api/instance/#service-availability-object) flag `bluesky_enabled`. An authorisation started while the flag was set completes normally after an operator clears it, as long as the OAuth client is still configured. ### Rate limit @@ -508,7 +508,7 @@ An atproto authorisation server reads this document, named by `jwks_uri` in [Get | --- | --- | --- | | keys | array[object] | The public half of each configured signing key, in configuration order | -The array holds at least one entry on any 200. Each entry has the public members of one ES256 key. There is no `alg` and no `use`, so a verifier that demands either one rejects the set. +The array holds at least one entry on any 200. Each entry has the public members of one ES256 key. There is no `alg` and no `use`, so a verifier that requires either one rejects the set. | Field | Type | Description | | --- | --- | --- | 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 e209aee31..9a93e2312 100644 --- a/fluxer_docs/src/content/docs/http-api/deployment-availability.md +++ b/fluxer_docs/src/content/docs/http-api/deployment-availability.md @@ -12,7 +12,7 @@ The API decides registration once at process start from the deployment configura ## Deployment kind -Every deployment reports its kind in `self_hosted` on the [instance features object](/http-api/instance/#instance-features-object), which the unauthenticated [instance discovery document](/http-api/instance/#get-instance-discovery) publishes before a client holds any credential. `self_hosted` alone decides whether the API registers the routes below. +Every deployment reports its kind in `self_hosted` on the [instance features object](/http-api/instance/#instance-features-object). The unauthenticated [instance discovery document](/http-api/instance/#get-instance-discovery) publishes it before a client holds any credential. `self_hosted` alone decides whether the API registers the routes below. `stripe_enabled` on the same object reports the payment provider toggle alone. A hosted deployment that reports it false still serves every route in the table below. A deployment reporting `stripe_enabled` true with no provider secret key configured behaves exactly like one reporting it false. @@ -66,6 +66,6 @@ Without a provider client, the answer depends on the operation. An operation tha A route every deployment registers can still produce a different answer on a self-hosted instance. Each operation page documents that difference. -Premium state is the clearest case. Every deployment registers [Get premium state](/http-api/premium/#get-premium-state) and [Set premium perks disabled](/http-api/premium/#set-premium-perks-disabled). A self-hosted instance still reports premium state and still records the perks-disabled flag. The response repeats the deployment kind in `self_hosted` on the [effective premium state object](/http-api/premium/#effective-premium-state-object). That flag alone does not make `is_premium` true, because a self-hosted deployment grants premium to every account only while its instance [premium mode](/admin-api/instance/#premium-modes) is `everyone`. While that mode is in force it also overrides the perks-disabled flag, so `is_premium` stays true while `premium_perks_disabled` is true. +Premium state is the clearest case. Every deployment registers [Get premium state](/http-api/premium/#get-premium-state) and [Set premium perks disabled](/http-api/premium/#set-premium-perks-disabled). A self-hosted instance still reports premium state and still records the perks-disabled flag. The response repeats the deployment kind in `self_hosted` on the [effective premium state object](/http-api/premium/#effective-premium-state-object). That flag alone does not make `is_premium` true. A self-hosted deployment grants premium to every account only while its instance [premium mode](/admin-api/instance/#premium-modes) is `everyone`. That mode also overrides the perks-disabled flag, so `is_premium` stays true while `premium_perks_disabled` is true. The other instance feature flags published by [instance discovery](/http-api/instance/#instance-features-object) work the same way. `voice_enabled`, `presigned_attachment_uploads`, and `emails_enabled` each switch off a capability that the surrounding routes still expose, so a client reads the flag. diff --git a/fluxer_docs/src/content/docs/http-api/discovery.mdx b/fluxer_docs/src/content/docs/http-api/discovery.mdx index f9540b73f..43066d77a 100644 --- a/fluxer_docs/src/content/docs/http-api/discovery.mdx +++ b/fluxer_docs/src/content/docs/http-api/discovery.mdx @@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro'; Discovery is the public directory of guilds any account can browse and join. A guild manager applies to have a guild listed, and an operator reviews that application through the [Admin Discovery API](/admin-api/discovery/). -[Join discovery guild](#join-discovery-guild) is user-only and rejects a bot token with 403 `ACCESS_DENIED`. Every other route accepts a user session token or a bot token. +[Join discovery guild](#join-discovery-guild) is user-only and rejects a bot token with 403 `ACCESS_DENIED`. Every other route accepts a user session token or a bot token. Besides that rejection, a bearer credential is the only credential that produces `ACCESS_DENIED` on any route here. Where a response table below pairs a revoked account with `ACCESS_DENIED`, a session, bot, or bearer token that no longer resolves to an account returns 401 `UNAUTHORIZED` instead. On a guild an operator has marked unavailable, Fluxer refuses [Apply for discovery](#apply-for-discovery), [Edit discovery application](#edit-discovery-application), [Withdraw discovery application](#withdraw-discovery-application), and [Get discovery status](#get-discovery-status) with 403 `MISSING_ACCESS` before the route runs. The gate does not cover [Join discovery guild](#join-discovery-guild). @@ -155,8 +155,8 @@ The application state of one guild and its current eligibility to apply. | Field | Type | Description | | --- | --- | --- | | application | ?[discovery application](#discovery-application-object) object | The current application of the guild, or null when it has never applied or has withdrawn | -| eligible1 | boolean | Whether the guild currently satisfies the requirement to apply | -| min_member_count2 | integer | The number of members the instance currently requires | +| eligible1 | boolean | Whether the guild meets the requirement to apply | +| min_member_count2 | integer | The number of members the instance requires | 1 False whenever discovery is disabled for the instance, whatever the guild would otherwise satisfy @@ -296,12 +296,10 @@ The caller needs no permission and no relationship to the matched guilds. | --- | --- | --- | | 200 | [discovery search result](#discovery-search-result-object) object | Search completed, possibly with no match | | 400 | [error response](/http-api/#error-response) | Discovery is disabled for the instance and the request returns `DISCOVERY_DISABLED` | -| 4031 | [error response](/http-api/#error-response) | Caller presents a bearer credential or a revoked account and the request returns `ACCESS_DENIED` | +| 403 | [error response](/http-api/#error-response) | Caller presents a bearer credential or a revoked account and the request returns `ACCESS_DENIED` | | 403 | [error response](/http-api/#error-response) | The account has an outstanding required action and the request returns `ACCOUNT_SUSPICIOUS_ACTIVITY` | | 403 | [error response](/http-api/#error-response) | No search backend is configured for the instance and the request returns `FEATURE_TEMPORARILY_DISABLED` | -1 Only a bearer credential produces this code. A session, bot, or bearer token that no longer resolves to an account returns 401 `UNAUTHORIZED` - ### Rate limit 30 requests per 10 seconds for each authenticated user, on the `discovery:search` bucket, which is not partitioned by query. @@ -321,9 +319,7 @@ A client that wants a translated label supplies its own translation keyed on `id | Status | Body | Condition | | --- | --- | --- | | 200 | array[[discovery category](#discovery-category-object) object] | Categories were returned | -| 4031 | [error response](/http-api/#error-response) | Caller presents a bearer credential or a revoked account and the request returns `ACCESS_DENIED`, or the account has an outstanding required action and the request returns `ACCOUNT_SUSPICIOUS_ACTIVITY` | - -1 Only a bearer credential produces `ACCESS_DENIED`. A session, bot, or bearer token that no longer resolves to an account returns 401 `UNAUTHORIZED` +| 403 | [error response](/http-api/#error-response) | Caller presents a bearer credential or a revoked account and the request returns `ACCESS_DENIED`, or the account has an outstanding required action and the request returns `ACCOUNT_SUSPICIOUS_ACTIVITY` | ### Rate limit @@ -356,7 +352,7 @@ A pending, rejected, removed, or absent application fails with 400 `DISCOVERY_NO | 400 | [error response](/http-api/#error-response) | The guild has no approved application and the request returns `DISCOVERY_NOT_DISCOVERABLE` | | 400 | [error response](/http-api/#error-response) | The caller already holds the maximum number of guilds and the request returns `MAX_GUILDS` | | 400 | [error response](/http-api/#error-response) | The guild is full and the request returns `MAX_GUILD_MEMBERS` | -| 4032 | [error response](/http-api/#error-response) | Caller is a bot, presents a bearer credential, or holds a revoked account and the request returns `ACCESS_DENIED` | +| 403 | [error response](/http-api/#error-response) | Caller is a bot, presents a bearer credential, or holds a revoked account and the request returns `ACCESS_DENIED` | | 4031 | [error response](/http-api/#error-response) | The account has an outstanding required action and the request returns `ACCOUNT_SUSPICIOUS_ACTIVITY` | | 403 | [error response](/http-api/#error-response) | The guild has the invites-disabled feature and the request returns `INVITES_DISABLED` | | 403 | [error response](/http-api/#error-response) | The caller is banned from the guild directly or by address and the request returns `USER_BANNED_FROM_GUILD` or `USER_IP_BANNED_FROM_GUILD` | @@ -364,8 +360,6 @@ A pending, rejected, removed, or absent application fails with 400 `DISCOVERY_NO 1 An account whose phone requirement was deferred is re-evaluated against the target guild at join time, so an account that satisfies the standing check can still be refused here -2 Only a bot token or a bearer credential produces this code. A session, bot, or bearer token that no longer resolves to an account returns 401 `UNAUTHORIZED` - :::note[A guild that never existed returns 400, not 404] No approved application names it, so the request fails with `DISCOVERY_NOT_DISCOVERABLE` before any guild record is read. ::: @@ -421,8 +415,8 @@ Its submission time is also its review time, it gains the discoverable feature, | --- | --- | --- | | 200 | [discovery application](#discovery-application-object) object | Application was stored as pending, or was approved immediately | | 400 | [error response](/http-api/#error-response) | Discovery is disabled for the instance and the request returns `DISCOVERY_DISABLED`, or the guild has fewer members than the instance requires and the request returns `DISCOVERY_INSUFFICIENT_MEMBERS` | -| 400 | [error response](/http-api/#error-response) | The caller cannot exercise [MANAGE_GUILD](/http-api/permissions/) because of the guild MFA level and the request returns `TWO_FACTOR_REQUIRED` | -| 4031 | [error response](/http-api/#error-response) | Caller presents a bearer credential or a revoked account and the request returns `ACCESS_DENIED` | +| 400 | [error response](/http-api/#error-response) | The caller cannot use [MANAGE_GUILD](/http-api/permissions/) because of the guild MFA level and the request returns `TWO_FACTOR_REQUIRED` | +| 403 | [error response](/http-api/#error-response) | Caller presents a bearer credential or a revoked account and the request returns `ACCESS_DENIED` | | 403 | [error response](/http-api/#error-response) | The account has an outstanding required action and the request returns `ACCOUNT_SUSPICIOUS_ACTIVITY` | | 403 | [error response](/http-api/#error-response) | The guild is marked unavailable and the request returns `MISSING_ACCESS` | | 403 | [error response](/http-api/#error-response) | The description matches a content blocklist and the request returns `CONTENT_BLOCKED` | @@ -430,8 +424,6 @@ Its submission time is also its review time, it gains the discoverable feature, | 404 | [error response](/http-api/#error-response) | Guild does not exist and the request returns `UNKNOWN_GUILD` | | 409 | [error response](/http-api/#error-response) | The guild already holds a pending or approved application and the request returns `DISCOVERY_ALREADY_APPLIED` | -1 Only a bearer credential produces this code. A session, bot, or bearer token that no longer resolves to an account returns 401 `UNAUTHORIZED` - ### Side effects The submitted listing replaces a previous rejected or removed application. @@ -479,8 +471,8 @@ Every field is optional, and an omitted field preserves the stored value. | --- | --- | --- | | 200 | [discovery application](#discovery-application-object) object | Listing was updated, or the submitted values already matched the stored ones | | 400 | [error response](/http-api/#error-response) | Discovery is disabled for the instance and the request returns `DISCOVERY_DISABLED` | -| 400 | [error response](/http-api/#error-response) | The caller cannot exercise [MANAGE_GUILD](/http-api/permissions/) because of the guild MFA level and the request returns `TWO_FACTOR_REQUIRED` | -| 4031 | [error response](/http-api/#error-response) | Caller presents a bearer credential or a revoked account and the request returns `ACCESS_DENIED` | +| 400 | [error response](/http-api/#error-response) | The caller cannot use [MANAGE_GUILD](/http-api/permissions/) because of the guild MFA level and the request returns `TWO_FACTOR_REQUIRED` | +| 403 | [error response](/http-api/#error-response) | Caller presents a bearer credential or a revoked account and the request returns `ACCESS_DENIED` | | 403 | [error response](/http-api/#error-response) | The account has an outstanding required action and the request returns `ACCOUNT_SUSPICIOUS_ACTIVITY` | | 403 | [error response](/http-api/#error-response) | The guild is marked unavailable and the request returns `MISSING_ACCESS` | | 403 | [error response](/http-api/#error-response) | The description matches a content blocklist and the request returns `CONTENT_BLOCKED` | @@ -489,8 +481,6 @@ Every field is optional, and an omitted field preserves the stored value. | 404 | [error response](/http-api/#error-response) | The guild exists but holds no application, and the request returns `DISCOVERY_APPLICATION_NOT_FOUND` | | 409 | [error response](/http-api/#error-response) | The stored application is rejected or removed and the request returns `DISCOVERY_APPLICATION_ALREADY_REVIEWED` | -1 Only a bearer credential produces this code. A session, bot, or bearer token that no longer resolves to an account returns 401 `UNAUTHORIZED` - ### Side effects The supplied fields update the listing while preserving its status, submission time, review time, and review reason. Editing an approved listing reindexes its result for [Search discovery guilds](#search-discovery-guilds). A pending listing remains absent from search. No guild feature changes. @@ -521,16 +511,14 @@ The stored review time, review reason, removal time, and removal reason are dele | --- | --- | --- | | 204 | empty | Application was deleted | | 400 | [error response](/http-api/#error-response) | Discovery is disabled for the instance and the request returns `DISCOVERY_DISABLED` | -| 400 | [error response](/http-api/#error-response) | The caller cannot exercise [MANAGE_GUILD](/http-api/permissions/) because of the guild MFA level and the request returns `TWO_FACTOR_REQUIRED` | -| 4031 | [error response](/http-api/#error-response) | Caller presents a bearer credential or a revoked account and the request returns `ACCESS_DENIED` | +| 400 | [error response](/http-api/#error-response) | The caller cannot use [MANAGE_GUILD](/http-api/permissions/) because of the guild MFA level and the request returns `TWO_FACTOR_REQUIRED` | +| 403 | [error response](/http-api/#error-response) | Caller presents a bearer credential or a revoked account and the request returns `ACCESS_DENIED` | | 403 | [error response](/http-api/#error-response) | The account has an outstanding required action and the request returns `ACCOUNT_SUSPICIOUS_ACTIVITY` | | 403 | [error response](/http-api/#error-response) | The guild is marked unavailable and the request returns `MISSING_ACCESS` | | 403 | [error response](/http-api/#error-response) | The caller lacks [MANAGE_GUILD](/http-api/permissions/) and the request returns `MISSING_PERMISSIONS` | | 404 | [error response](/http-api/#error-response) | Guild does not exist and the request returns `UNKNOWN_GUILD` | | 404 | [error response](/http-api/#error-response) | The guild exists but holds no application, and the request returns `DISCOVERY_APPLICATION_NOT_FOUND` | -1 Only a bearer credential produces this code. A session, bot, or bearer token that no longer resolves to an account returns 401 `UNAUTHORIZED` - ### Side effects The application is permanently deleted. Withdrawing an approved listing also removes the discoverable feature, removes the listing from the discovery index, and delivers [Guild Update](/gateway/events/#guild-update) to every session that can see the guild. Withdrawing a pending, rejected, or removed application changes no guild feature and emits no Dispatch. Existing members are unaffected. @@ -558,15 +546,13 @@ The route answers even while discovery is disabled for the instance, reporting ` | Status | Body | Condition | | --- | --- | --- | | 200 | [discovery status](#discovery-status-object) object | Status was returned | -| 400 | [error response](/http-api/#error-response) | The caller cannot exercise [MANAGE_GUILD](/http-api/permissions/) because of the guild MFA level and the request returns `TWO_FACTOR_REQUIRED` | -| 4031 | [error response](/http-api/#error-response) | Caller presents a bearer credential or a revoked account and the request returns `ACCESS_DENIED` | +| 400 | [error response](/http-api/#error-response) | The caller cannot use [MANAGE_GUILD](/http-api/permissions/) because of the guild MFA level and the request returns `TWO_FACTOR_REQUIRED` | +| 403 | [error response](/http-api/#error-response) | Caller presents a bearer credential or a revoked account and the request returns `ACCESS_DENIED` | | 403 | [error response](/http-api/#error-response) | The account has an outstanding required action and the request returns `ACCOUNT_SUSPICIOUS_ACTIVITY` | | 403 | [error response](/http-api/#error-response) | The guild is marked unavailable and the request returns `MISSING_ACCESS` | | 403 | [error response](/http-api/#error-response) | The caller lacks [MANAGE_GUILD](/http-api/permissions/) and the request returns `MISSING_PERMISSIONS` | | 404 | [error response](/http-api/#error-response) | Guild does not exist and the request returns `UNKNOWN_GUILD` | -1 Only a bearer credential produces this code. A session, bot, or bearer token that no longer resolves to an account returns 401 `UNAUTHORIZED` - ### Rate limit 30 requests per 10 seconds for each authenticated user and guild ID, on the `discovery:status::guild_id` bucket. diff --git a/fluxer_docs/src/content/docs/http-api/donations.mdx b/fluxer_docs/src/content/docs/http-api/donations.mdx index 6b9b3ebb8..b831bac5c 100644 --- a/fluxer_docs/src/content/docs/http-api/donations.mdx +++ b/fluxer_docs/src/content/docs/http-api/donations.mdx @@ -33,8 +33,8 @@ Amounts are expressed in the minor unit of the selected currency. Each currency | Value | Description | | --- | --- | -| month | The donation recurs every month | -| year | The donation recurs every year | +| month | The donation repeats every month | +| year | The donation repeats every year | | null | The donation is taken once | ## Donation checkout object @@ -67,7 +67,7 @@ Accepts a request for a single-use donation management link. Returns 204 with an - The submitted address must be syntactically valid. - The address must not exceed 254 characters. -- The address domain must publish usable mail or address records. +- Its domain must publish usable mail or address records. A failure of any of those returns 400 `INVALID_FORM_BODY` with an `errors` entry on `email`. @@ -98,7 +98,7 @@ An address that resolves to no donor receives no email. Everything happens before the response. Fluxer resolves the address domain, looks up the donor, and, when a donor exists, creates a token and sends the email. A delivery failure is absorbed. An address held as hard bounced and a transport error both leave the token stored and still answer 204. -Fluxer creates a single-use token of 64 lowercase hexadecimal characters, valid for 15 minutes, only when the address resolves to a donor. Issuing a new link deletes every earlier token for the same address, and a superseded link returns 400 `DONATION_MAGIC_LINK_INVALID`. +Fluxer creates a single-use token of 64 lowercase hexadecimal characters, valid for 15 minutes, only when the address resolves to a donor. Issuing a new link deletes every earlier token for the same address, and a replaced link returns 400 `DONATION_MAGIC_LINK_INVALID`. ### Rate limit @@ -120,7 +120,7 @@ A token whose address no longer resolves to a donor holding a payment provider c | --- | --- | --- | | token1 2 | string | The management token, exactly 64 characters | -1 A token whose case was altered in transit returns 400 `DONATION_MAGIC_LINK_INVALID` +1 A token whose case was changed in transit returns 400 `DONATION_MAGIC_LINK_INVALID` 2 Consumed as soon as it is found valid, so a portal creation failure leaves the token spent @@ -170,7 +170,7 @@ A recurring donation for an address that already holds an active recurring donat | Status | Body | Condition | | --- | --- | --- | | 200 | [donation checkout](#donation-checkout-object) object | The checkout session was created, or the donor was redirected to donation management | -| 400 | [error response](/http-api/#error-response) | The amount is outside the bounds for the currency, the address domain publishes no usable records, the payment provider is not configured and returns `STRIPE_PAYMENT_NOT_AVAILABLE`, or the provider rejected the request and returns `STRIPE_ERROR` | +| 400 | [error response](/http-api/#error-response) | The amount is outside the currency's bounds, the address domain publishes no usable records, the payment provider is not configured and returns `STRIPE_PAYMENT_NOT_AVAILABLE`, or the provider rejected the request and returns `STRIPE_ERROR` | ### Side effects diff --git a/fluxer_docs/src/content/docs/http-api/entrance-sounds.mdx b/fluxer_docs/src/content/docs/http-api/entrance-sounds.mdx index d4f673c9b..3efff86d2 100644 --- a/fluxer_docs/src/content/docs/http-api/entrance-sounds.mdx +++ b/fluxer_docs/src/content/docs/http-api/entrance-sounds.mdx @@ -26,11 +26,11 @@ Every bound below is a fixed constant of the instance, and none is resolved from 2 Only [Upload entrance sound](#upload-entrance-sound) applies the lower bound -The [instance discovery](/http-api/instance/#limit-keys) document publishes a `feature_voice_entrance_sounds` limit key. That key gates the feature in the client. No route on this page consults it, so a library can be read, written, and played back regardless of its resolved value. +The [instance discovery](/http-api/instance/#limit-keys) document publishes a `feature_voice_entrance_sounds` limit key. That key gates the feature in the client. No route on this page checks it, so a library can be read, written, and played back regardless of its resolved value. ## Supported containers -The container is detected from the decoded bytes, so a filename, a `data:` media type, or any other declared type has no effect on the result. The detected container resolves to a stored extension, and every other input fails with `ENTRANCE_SOUND_INVALID_FORMAT`. +Fluxer detects the container from the decoded bytes, so a filename, a `data:` media type, or any other declared type has no effect on the result. The detected container resolves to a stored extension, and every other input fails with `ENTRANCE_SOUND_INVALID_FORMAT`. | Detected container | Stored extension | Stored content type | | --- | --- | --- | @@ -139,7 +139,7 @@ Returns the current account's complete entrance sound library together with its | Field | Type | Description | | --- | --- | --- | | sounds | array[[entrance sound](#entrance-sound-object) object] | Every clip the account owns, at most 8 | -| selections | array[[entrance sound selection](#entrance-sound-selection-object) object] | Every scope that currently has a sound assigned | +| selections | array[[entrance sound selection](#entrance-sound-selection-object) object] | Every scope with a sound assigned | Both members are always present and both arrays are empty for an account that has uploaded nothing. A selection always names a sound present in `sounds`. @@ -170,7 +170,7 @@ Decodes, validates, and stores one audio clip in the current account's library, 2 A `data:` URI prefix is accepted and discarded, and only the part after the first comma is measured and decoded -Invisible characters in `name` are not stripped and count toward the 32 character bound. +Invisible characters in `name` are not stripped and count towards the 32 character bound. ### Validation @@ -192,7 +192,7 @@ Every failure below is a 400 `INVALID_FORM_BODY`. One response can report two of 2 Measured after any `data:` prefix is discarded, so a value that is whitespace only, or that ends at its first comma, has zero payload characters and fails here -The first four rows are checked before the quota, so an account already holding 8 clips receives `BASE64_LENGTH_INVALID` or `INVALID_BASE64_FORMAT` for a malformed or over-long payload. +Fluxer checks the first four rows before the quota, so an account already holding 8 clips receives `BASE64_LENGTH_INVALID` or `INVALID_BASE64_FORMAT` for a malformed or too-long payload. The validation entry has no member naming which duration bound the clip crossed. diff --git a/fluxer_docs/src/content/docs/http-api/errors.md b/fluxer_docs/src/content/docs/http-api/errors.md index f2ca1424f..0bce8396d 100644 --- a/fluxer_docs/src/content/docs/http-api/errors.md +++ b/fluxer_docs/src/content/docs/http-api/errors.md @@ -20,7 +20,7 @@ An OAuth2 protocol failure raised by the [OAuth2 resource](/http-api/oauth2/) an ## Supplementary members -The error code determines which supplementary members a failure has, and most codes have none. A client reads only the members documented for the code it matched. `errors` is the list of field violations. `retry_after` is the delay before another attempt is admitted, `global` is `true` on a global rate limit denial and `false` on a route one, `required_scope` is the OAuth2 scope the request is missing, and `has_mfa` and `methods` are the [sudo mode](/http-api/users/mfa/#sudo-mode) proofs an account can supply. +The error code determines which supplementary members a failure has, and most codes have none. A client reads only the members documented for the code it matched. `errors` is the list of field violations. `retry_after` is the delay before another attempt is admitted. `global` is `true` on a global rate limit denial and `false` on a route one. `required_scope` is the OAuth2 scope the request is missing. `has_mfa` and `methods` are the [sudo mode](/http-api/users/mfa/#sudo-mode) proofs an account can supply. `GLOBAL_IP_BANNED` and `GLOBAL_IP_TEMPORARILY_BANNED` have their own members: @@ -65,7 +65,7 @@ An empty or whitespace-only body becomes `{}`, so the response reports the field 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. :::caution[An enumerated validation failure answers 400 alone] -A validation failure whose elements have enumerated codes answers 400 with its elements in `errors`, and 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. +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. ::: ### Default schema failure codes @@ -124,7 +124,7 @@ The window length, both thresholds, and the number of windows the score trigger ## API error code registry -These codes appear in the top-level `code` field of an error response, transmitted as the exact JSON string shown. The registry is closed. Each entry states the leading sentence of the English source message, without its final full stop. Those messages call a [guild](/http-api/guilds/) a community. +These codes appear in the top-level `code` field of an error response, sent as the exact JSON string shown. The registry is closed. Each entry states the leading sentence of the English source message, without its final full stop. Those messages call a [guild](/http-api/guilds/) a community. :::note[The rendered `message` fills in the braced values] A description containing a value in braces is an ICU MessageFormat template. `You've reached the maximum of {count, plural, one {# emoji} other {# emojis}}` renders as a complete sentence with the applicable limit. diff --git a/fluxer_docs/src/content/docs/http-api/expressions.mdx b/fluxer_docs/src/content/docs/http-api/expressions.mdx index 431b6189a..65df80a33 100644 --- a/fluxer_docs/src/content/docs/http-api/expressions.mdx +++ b/fluxer_docs/src/content/docs/http-api/expressions.mdx @@ -118,7 +118,7 @@ Returns the [emoji metadata object](#emoji-metadata-object) of any guild emoji. 1 The [error code](/http-api/errors/) is `UNKNOWN_EMOJI` for a missing emoji and `UNKNOWN_GUILD` when the emoji record survives but its guild does not -:::caution[The emoji's guild is read again at clone time] +:::caution[Cloning reads the emoji's guild again] [Clone guild emoji](/http-api/guild-emojis/#clone-guild-emoji) resolves the source emoji and the source guild again. When the source guild disables cloning between the two calls, the clone fails with 403 `MISSING_ACCESS`. ::: @@ -147,7 +147,7 @@ Returns the [sticker metadata object](#sticker-metadata-object) of any guild sti 1 The [error code](/http-api/errors/) is `UNKNOWN_STICKER` for a missing sticker and `UNKNOWN_GUILD` when the sticker record survives but its guild does not -:::caution[The sticker's guild is read again at clone time] +:::caution[Cloning reads the sticker's guild again] [Clone guild sticker](/http-api/guild-stickers/#clone-guild-sticker) resolves the source sticker and the source guild again. When the source guild disables cloning between the two calls, the clone fails with 403 `MISSING_ACCESS`. ::: diff --git a/fluxer_docs/src/content/docs/http-api/gateway.mdx b/fluxer_docs/src/content/docs/http-api/gateway.mdx index 63fc77b36..99d760c28 100644 --- a/fluxer_docs/src/content/docs/http-api/gateway.mdx +++ b/fluxer_docs/src/content/docs/http-api/gateway.mdx @@ -70,7 +70,7 @@ A session start limit reports how many new Gateway sessions a bot may open in a 3 Fluxer runs no Identify concurrency bucket, so a bot MAY identify its shards without pacing them against this value -The limits the Gateway actually enforces are in [Session lifecycle](/gateway/limits-and-rate-limits/#session-lifecycle). Fluxer budgets Identify per source address and caps a user account at a fixed number of concurrent sessions. Neither bound is reported here. +The limits the Gateway enforces are in [Session lifecycle](/gateway/limits-and-rate-limits/#session-lifecycle). Fluxer budgets Identify per source address and caps a user account at a fixed number of concurrent sessions. Neither bound is reported here. :::caution[A remaining session start does not guarantee admission] A non-zero `remaining` reserves nothing, and neither does the constant `max_concurrency`. [Session admission](/gateway/limits-and-rate-limits/#session-lifecycle) can still hold or reject a connection. diff --git a/fluxer_docs/src/content/docs/http-api/gifs.mdx b/fluxer_docs/src/content/docs/http-api/gifs.mdx index faabb34aa..a332dcc73 100644 --- a/fluxer_docs/src/content/docs/http-api/gifs.mdx +++ b/fluxer_docs/src/content/docs/http-api/gifs.mdx @@ -14,7 +14,7 @@ Every route requires a session credential. A bot token and an OAuth2 bearer cred `klipy` is the only provider name a deployment binds. [Get instance discovery](/http-api/instance/#get-instance-discovery) publishes it as the [GIF provider](/http-api/instance/#gif-provider-object) object. An instance that has bound no provider key refuses every route on this page with 403 `FEATURE_TEMPORARILY_DISABLED`. That same key drives `gif_enabled` in [service availability](/http-api/instance/#service-availability-object), a flag an operator can also set by hand, so a client that reads `gif_enabled` as true can still receive the 403. -A provider outage, a request that outlives its deadline, and an unreadable provider payload all surface as 503 `SERVICE_UNAVAILABLE`. The deadline is 12 seconds everywhere except [Register a GIF share](#register-a-gif-share), which uses 3 seconds. An operator can change both. +A provider outage, a request past its deadline, and an unreadable provider payload all return 503 `SERVICE_UNAVAILABLE`. The deadline is 12 seconds everywhere except [Register a GIF share](#register-a-gif-share), which uses 3 seconds. An operator can change both. ## Locale and country diff --git a/fluxer_docs/src/content/docs/http-api/gifts.mdx b/fluxer_docs/src/content/docs/http-api/gifts.mdx index 006a4865b..66caea391 100644 --- a/fluxer_docs/src/content/docs/http-api/gifts.mdx +++ b/fluxer_docs/src/content/docs/http-api/gifts.mdx @@ -111,7 +111,7 @@ Redeems a gift for the authenticated account and returns 204 with an empty body. - The account must not have the purchase-disabled premium flag, and the flag returns 403 `PREMIUM_PURCHASE_BLOCKED` with a top-level `reason` member set to `purchase_disabled`. - An account already holding lifetime Visionary entitlement receives 400 `CANNOT_REDEEM_PLUTONIUM_WITH_VISIONARY`. -A consumed code receives 400 `GIFT_CODE_ALREADY_REDEEMED`. A code another request is currently redeeming receives 400 `STRIPE_GIFT_REDEMPTION_IN_PROGRESS`. A code that does not exist or has been revoked receives 404 `UNKNOWN_GIFT_CODE`. +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. @@ -134,7 +134,7 @@ One redemption can be in flight for a code across the whole deployment, and a se | X-Captcha-Token?1 | string | The proof issued by the CAPTCHA provider | | X-Captcha-Type?2 | string | The CAPTCHA provider, either `hcaptcha` or `turnstile` | -1 A missing proof returns 400 `CAPTCHA_REQUIRED` and a rejected proof returns 400 `INVALID_CAPTCHA`. Verification is skipped when CAPTCHA is disabled, when the account has the exemption flag, or when the caller's contact has the exemption capability +1 A missing proof returns 400 `CAPTCHA_REQUIRED` and a rejected proof returns 400 `INVALID_CAPTCHA`. Verification is skipped when CAPTCHA is disabled, when the account has the exemption flag, or when the caller's contact has the exemption capability, as described by [CAPTCHA handling](/topics/captcha/) 2 Any other value, including an omitted header, falls back to the instance's configured provider @@ -143,7 +143,7 @@ One redemption can be in flight for a code across the whole deployment, and a se | Status | Body | Condition | | --- | --- | --- | | 204 | empty | The gift was redeemed and the entitlement was applied | -| 400 | [error response](/http-api/#error-response) | CAPTCHA failed, the code is already redeemed, a redemption is already in flight, the account is unclaimed, the account holds lifetime entitlement, the payment provider rejected the subscription work, or the Visionary guild join for a lifetime gift failed | +| 400 | [error response](/http-api/#error-response) | CAPTCHA failed, the code is redeemed or a redemption is in flight, the account is unclaimed or holds lifetime entitlement, the payment provider rejected the subscription work, or the Visionary guild join for a lifetime gift failed | | 403 | [error response](/http-api/#error-response) | The email address is unverified, or purchases are disabled for the account | | 404 | [error response](/http-api/#error-response) | No gift exists for the code, the gift was revoked, the authenticated account record no longer exists, or the configured Visionary guild does not exist | @@ -160,7 +160,7 @@ A gift with a positive quantity extends recurring premium. Fluxer stacks the dur Stacking adds the duration to the existing trial end, or to the current period end when no trial end is set, and applies the update without proration. -The stacked path and the unstacked path both set the gift extension end to the gift duration added to the latest of the current time, the current premium end and the existing gift extension end. Both also clear an active grace deadline and set the premium type when the account has none. The stacked path also records the account as having ever purchased. +The stacked and unstacked paths set the gift extension end to the gift duration added to the latest of the current time, the current premium end and the existing gift extension end. Both also clear an active grace deadline and set the premium type when the account has none. The stacked path also records the account as having ever purchased. The provider refuses a stacking attempt when the subscription is unknown, already cancelled, or has neither a trial end nor a current period end. That refusal clears the stored subscription identity, the billing cycle and any pending cancellation. The redemption then continues unstacked. Any other provider failure aborts the redemption and returns 400 `STRIPE_ERROR`, and the code stays unredeemed. 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 52e825ad6..e48a37520 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 @@ -50,7 +50,7 @@ One recorded guild change. An entry is immutable once written, except for a run | --- | --- | --- | | id | snowflake | The ID of the entry, whose [snowflake](/snowflakes/) timestamp is the time it was written | | action_type | integer | [Audit action](#audit-actions) value that identifies the recorded operation | -| user_id1 | snowflake | The ID of the user that performed the action | +| user_id1 | snowflake | The ID of the user that did the action | | target_id2 | ?string | The ID of the affected entity | | reason?3 | string | The [audit log reason](#audit-log-reason) recorded with the operation | | options?4 | [audit log options](#audit-log-options-object) object | The context fields this [audit action](#audit-actions) records | @@ -321,7 +321,7 @@ Recorded by [MEMBER_BAN_ADD](#audit-actions) and [MEMBER_BAN_REMOVE](#audit-acti | moderator_id | string | The decimal ID of the user that issued the ban | | banned_at | ISO8601 timestamp | The time the ban was issued | | expires_at | ?ISO8601 timestamp | The expiry of a temporary ban, or null for a permanent ban | -| reason | ?string | The stored [ban](/http-api/guild-moderation/#guild-ban-object) reason, which is separate from the audit reason | +| reason | ?string | The stored [ban](/http-api/guild-moderation/#guild-ban-object) reason, which is not the [audit log reason](#audit-log-reason) | #### Guild role change fields @@ -341,7 +341,7 @@ Recorded by [ROLE_CREATE](#audit-actions), [ROLE_UPDATE](#audit-actions), and [R | mentionable | boolean | Whether the role is mentionable | | permissions_diff1 | object | The permission names this change added and removed | -1 Recorded only by [Modify guild role](/http-api/permissions/#modify-guild-role), and only when the mask actually changed. The value is an object with `added` and `removed` string arrays of permission names +1 Recorded only by [Modify guild role](/http-api/permissions/#modify-guild-role), and only when the mask changed. The value is an object with `added` and `removed` string arrays of permission names `permissions_diff` arrives as `new_value` with no `old_value`, and the same change list still has the `permissions` change. The role position and hoist position operations record [ROLE_UPDATE](#audit-actions) without it. @@ -449,7 +449,7 @@ A guild ID that names no guild returns 404 `UNKNOWN_GUILD`. A non-member of an e The read is ordered from newest to oldest by entry ID. `before` selects entries below the cursor and `after` selects entries above it, both read in that same descending order. An `after` page therefore begins with the newest entry above the cursor. -Consolidation breaks that ordering. A consolidated entry keeps the array position of the first entry of the run it replaces. Its ID is freshly minted and larger than every stored entry on the page, so a client that needs a strictly descending array sorts the page by ID itself. +Consolidation breaks that ordering. A consolidated entry keeps the array position of the first entry of the run it replaces. Its ID is newly issued and larger than every stored entry on the page, so a client that needs a strictly descending array sorts the page by ID itself. A page holds fewer entries than `limit` only when the guild has no further matching entries in that direction. diff --git a/fluxer_docs/src/content/docs/http-api/guild-channels.mdx b/fluxer_docs/src/content/docs/http-api/guild-channels.mdx index e43e5d6d5..acbd68a97 100644 --- a/fluxer_docs/src/content/docs/http-api/guild-channels.mdx +++ b/fluxer_docs/src/content/docs/http-api/guild-channels.mdx @@ -102,7 +102,7 @@ Naming a category as the preceding sibling places the moved channel after that c Returns every [channel object](/http-api/channels/#channel-object) in the guild that the authenticated member can see. Membership is the only requirement. -Fluxer evaluates [channel visibility](/http-api/permissions/#permission-computation) per channel. A channel is included when the member holds [VIEW_CHANNEL](/http-api/permissions/) in it, or when the member currently holds temporary access to it through a voice transition. A category is included when at least one child grants the member `VIEW_CHANNEL`. A member with no visible channel receives an empty array. +Fluxer evaluates [channel visibility](/http-api/permissions/#permission-computation) per channel. A channel is included when the member holds [VIEW_CHANNEL](/http-api/permissions/) in it, or when the member holds temporary access to it through a voice transition. A category is included when at least one child grants the member `VIEW_CHANNEL`. A member with no visible channel receives an empty array. :::note[Every visible channel arrives in one body] The route accepts no limit and no cursor. Each entry is a complete channel object whose `permission_overwrites` array is always present and always complete. @@ -136,7 +136,7 @@ Creates a guild channel and returns its [channel object](/http-api/channels/#cha ### Limitations -- Supplying `permission_overwrites` additionally requires [MANAGE_ROLES](/http-api/permissions/) at guild level. +- Supplying `permission_overwrites` also requires [MANAGE_ROLES](/http-api/permissions/) at guild level. `MANAGE_CHANNELS` is an [elevated permission](/http-api/permissions/#elevated-permissions), so it also requires an enrolled multi-factor authenticator when the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated and the caller is not the guild owner. @@ -169,7 +169,7 @@ Creates a guild channel and returns its [channel object](/http-api/channels/#cha 1 Normalisation strips control, join, bidirectional, and tag code points, collapses whitespace runs to one space, and trims. An empty result is rejected with `NAME_EMPTY_AFTER_NORMALIZATION` -2 A text channel name is additionally lowercased, its whitespace replaced with hyphens, and ASCII punctuation other than `-`, `.`, and `_` removed, unless the guild has [TEXT_CHANNEL_FLEXIBLE_NAMES](/http-api/guilds/#guild-features). That pass rewrites the stored name and never fails the request +2 A text channel name is also lowercased, its whitespace replaced with hyphens, and ASCII punctuation other than `-`, `.`, and `_` removed, unless the guild has [TEXT_CHANNEL_FLEXIBLE_NAMES](/http-api/guilds/#guild-features). That pass rewrites the stored name and never fails the request 3 An `http` or `https` URL of at most 2048 characters with a host and no embedded credentials. It is stored for every channel type @@ -214,9 +214,9 @@ Only the allowed mask is compared, so a supplied deny is stored without any auth ### Side effects -The operation consumes one guild channel slot and, when a parent is supplied, one slot in that category. It creates a [`CHANNEL_CREATE`](/http-api/guild-audit-logs/#audit-actions) guild audit log entry with the supplied reason, plus one [`CHANNEL_OVERWRITE_CREATE`](/http-api/guild-audit-logs/#audit-actions) entry for every stored overwrite, whether that overwrite was supplied or inherited. +The operation consumes one guild channel slot and, when a parent is supplied, one slot in that category. It creates a [`CHANNEL_CREATE`](/http-api/guild-audit-logs/#audit-actions) guild audit log entry with the supplied reason, plus one [`CHANNEL_OVERWRITE_CREATE`](/http-api/guild-audit-logs/#audit-actions) entry for every stored overwrite, whether that overwrite was supplied or inherited. Reaching either channel limit fails the request without consuming a slot. -It emits [Channel Create](/gateway/events/#channel-create) with the complete channel object only to sessions that can view the new channel. Reaching either channel limit fails the request without consuming a slot. +It emits [Channel Create](/gateway/events/#channel-create) with the complete channel object only to sessions that can view the new channel. ### Rate limit @@ -230,7 +230,7 @@ Applies a guild channel hierarchy update and returns 204 with an empty body. Req ### Limitations -- Setting `lock_permissions` on an entry that changes the parent additionally requires [MANAGE_ROLES](/http-api/permissions/) in the moved channel. +- Setting `lock_permissions` on an entry that changes the parent also requires [MANAGE_ROLES](/http-api/permissions/) in the moved channel. - The operation records no guild audit log entry, so an `X-Audit-Log-Reason` header has no effect. `MANAGE_CHANNELS` is an [elevated permission](/http-api/permissions/#elevated-permissions), so it also requires an enrolled multi-factor authenticator when the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated and the caller is not the guild owner. @@ -239,7 +239,7 @@ The request takes a guild-wide lock for its duration. A request that arrives whi Fluxer resolves every channel, parent, and sibling named by any entry once before the first entry runs, so a request naming an unknown identifier anywhere applies nothing. After that check, each entry is committed as it is applied. A request that fails partway leaves the entries it already applied in place. Moving a category moves the channels inside it as one block, and that block is excluded from the sibling list an entry indexes into. -Each entry flattens the complete guild hierarchy so that each root channel appears in position order, each category is immediately followed by its children, and each category's children put text and link channels before voice channels. Fluxer lifts the moved channel out of that list, together with its children when it is a category, and reinserts it at its destination. It then renumbers the result densely from 1 through the channel count. +Each entry flattens the complete guild hierarchy into one list. In that list, each root channel appears in position order, each category is immediately followed by its children, and each category's children put text and link channels before voice channels. Fluxer lifts the moved channel out of that list, together with its children when it is a category, and reinserts it at its destination. It then renumbers the result densely from 1 through the channel count. :::note[Renumbering can rewrite a channel no entry named] An entry that changes the flattened order renumbers the guild's complete hierarchy, so a channel the request never mentioned can end up at a new position. An entry whose flattened result matches the list it started from writes nothing. @@ -269,7 +269,7 @@ The check inspects the moved channel alone, and only inside its current parent a | 40012 | [error response](/http-api/#error-response) | An entry is structurally invalid, the destination category is full, or the caller has no enrolled authenticator in an elevated-MFA guild | | 403 | [error response](/http-api/#error-response) | Caller lacks `MANAGE_CHANNELS`, is not a member of the guild, or set `lock_permissions` without sufficient authority in the moved channel, each returning `MISSING_PERMISSIONS` | | 404 | [error response](/http-api/#error-response) | Guild does not exist, returning `UNKNOWN_GUILD` | -| 423 | [error response](/http-api/#error-response) | Another hierarchy update currently owns this guild, returning `GENERAL_ERROR` | +| 423 | [error response](/http-api/#error-response) | Another hierarchy update owns this guild, returning `GENERAL_ERROR` | 1 A structural failure returns `INVALID_FORM_BODY` and names the field with `CHANNEL_NOT_FOUND`, `INVALID_CHANNEL_ID`, `INVALID_PARENT_CHANNEL`, `PARENT_MUST_BE_CATEGORY`, `CATEGORIES_CANNOT_HAVE_PARENTS`, `PRECEDING_CHANNEL_MUST_SHARE_PARENT`, `CANNOT_POSITION_CHANNEL_RELATIVE_TO_ITSELF`, or `VOICE_CHANNELS_CANNOT_BE_ABOVE_TEXT_CHANNELS`. A full destination category returns the top-level `MAX_CATEGORY_CHANNELS` diff --git a/fluxer_docs/src/content/docs/http-api/guild-emojis.mdx b/fluxer_docs/src/content/docs/http-api/guild-emojis.mdx index 29f060db7..e3ff04e05 100644 --- a/fluxer_docs/src/content/docs/http-api/guild-emojis.mdx +++ b/fluxer_docs/src/content/docs/http-api/guild-emojis.mdx @@ -16,7 +16,7 @@ The instance phrase and URL blocklists screen a submitted emoji `name` before th ## Guild emoji object -The stored image is written once at creation and is never replaced, so `name` is the only mutable field. +Only `name` can be changed after creation. The stored image is written once at creation and is never replaced. ### Structure @@ -29,7 +29,7 @@ The stored image is written once at creation and is never replaced, so `name` is 1 Detected from the submitted image at creation, and copied unchanged by [Clone guild emoji](#clone-guild-emoji) -2 Present only on [List guild emojis](#list-guild-emojis), where it is populated for every caller and requires no permission +2 Present only on [List guild emojis](#list-guild-emojis), where it is set for every caller and requires no permission ### Example @@ -58,7 +58,7 @@ The image and name submitted for one new emoji. 3 The decoded ceiling is the resolved `emoji_max_size` [limit key](/http-api/instance/#limit-keys) in guild scope, 524288 bytes by default -The remaining encoded portion is bounded to 699052 characters, the base64 expansion of the 524288 byte default, and a longer value returns the validation code `BASE64_LENGTH_INVALID` at the `image` path. A value that is not canonical base64 with correct padding returns `INVALID_BASE64_FORMAT`. An image over the resolved ceiling returns `IMAGE_SIZE_EXCEEDS_LIMIT` at the same path, and `maxSize` is the ceiling that applied. +The remaining encoded part is bounded to 699052 characters, the base64 expansion of the 524288 byte default. A longer value returns the validation code `BASE64_LENGTH_INVALID` at the `image` path. A value that is not canonical base64 with correct padding returns `INVALID_BASE64_FORMAT`. An image over the resolved ceiling returns `IMAGE_SIZE_EXCEEDS_LIMIT` at the same path, and `maxSize` is the ceiling that applied. The 699052 character bound does not move with the limit key, so a resolved ceiling above 524288 bytes admits no more than 524289 decoded bytes. @@ -148,7 +148,7 @@ Fluxer returns the complete collection in one response, and the operation has no Creates one emoji from submitted image data and returns its [guild emoji object](#guild-emoji-object) without `user`. Requires membership of the guild and [CREATE_EXPRESSIONS](/http-api/permissions/). Emits a [Guild Emojis Update](/gateway/events/#guild-emojis-update) Gateway event. -The admission checks run in a fixed order: the body schema, then guild existence and membership, then `CREATE_EXPRESSIONS`, then the slot limit, and only then the image. A guild at its slot limit therefore returns `MAX_EMOJIS` even when the image would also have been rejected. +Admission checks run in a fixed order: the body schema, then guild existence and membership, then `CREATE_EXPRESSIONS`, then the slot limit, and only then the image. A guild at its slot limit therefore returns `MAX_EMOJIS` even when the image would also have been rejected. The slot limit is the operator-configured [max_guild_emojis](/http-api/instance/#limit-keys) value resolved against the guild's complete feature set, defaulting to 500. A guild with [UNLIMITED_EMOJI](/http-api/guilds/#guild-features) bypasses that configuration and receives a fixed ceiling of 999999. @@ -179,7 +179,7 @@ The name scan runs before the route is reached, so it precedes the rate limit bu ### Side effects -The operation consumes one guild emoji slot, stores the metadata-stripped image, emits [Guild Emojis Update](/gateway/events/#guild-emojis-update) with the guild's complete emoji collection to every session connected to the guild, and then writes an [`EMOJI_CREATE`](/http-api/guild-audit-logs/#audit-actions) audit entry with the supplied reason. A failed creation leaves no emoji, audit entry, or Dispatch and consumes no slot. Fluxer logs a failure to write the audit entry and still returns 200, so the emoji exists and the Dispatch is emitted. +The operation consumes one guild emoji slot and stores the metadata-stripped image. It emits [Guild Emojis Update](/gateway/events/#guild-emojis-update) with the guild's complete emoji collection to every session connected to the guild, and then writes an [`EMOJI_CREATE`](/http-api/guild-audit-logs/#audit-actions) audit entry with the supplied reason. A failed creation leaves no emoji, audit entry, or Dispatch and consumes no slot. Fluxer logs a failure to write the audit entry and still returns 200, so the emoji exists and the Dispatch is emitted. ### Rate limit @@ -228,7 +228,7 @@ Every item name is scanned together before the route is reached, so one blocked ### Side effects -Each successful item consumes one guild emoji slot, stores its image, and writes an [`EMOJI_CREATE`](/http-api/guild-audit-logs/#audit-actions) audit entry with the supplied reason. When at least one item succeeded, Fluxer emits one [Guild Emojis Update](/gateway/events/#guild-emojis-update) with the guild's complete emoji collection after the batch, then writes the audit entries. A failed item leaves no emoji, audit entry, or slot consumption. +Each successful item consumes one guild emoji slot, stores its image, and writes an [`EMOJI_CREATE`](/http-api/guild-audit-logs/#audit-actions) audit entry with the supplied reason. When at least one item succeeded, Fluxer emits one [Guild Emojis Update](/gateway/events/#guild-emojis-update) with the guild's complete emoji collection after the batch, then writes the audit entries. A failed item leaves no emoji, audit entry, or slot use. ### Rate limit @@ -277,7 +277,7 @@ A source guild that no longer exists returns 403 `MISSING_ACCESS`, so a caller c ### Side effects -The operation consumes one target guild emoji slot and creates a copy whose uploader is the caller. It emits [Guild Emojis Update](/gateway/events/#guild-emojis-update) with the target guild's complete emoji collection and then writes an [`EMOJI_CREATE`](/http-api/guild-audit-logs/#audit-actions) audit entry. The source emoji and source guild are unchanged and receive no Dispatch. +The operation consumes one target guild emoji slot and creates a copy whose uploader is the caller. It emits [Guild Emojis Update](/gateway/events/#guild-emojis-update) with the target guild's complete emoji collection, then writes an [`EMOJI_CREATE`](/http-api/guild-audit-logs/#audit-actions) audit entry. The source emoji and source guild are unchanged and receive no Dispatch. ### Rate limit @@ -343,7 +343,7 @@ Deletes the emoji record and returns 204 with an empty body. Emits a [Guild Emoj - The uploader can delete their own emoji with [CREATE_EXPRESSIONS](/http-api/permissions/). - Any other caller requires [MANAGE_EXPRESSIONS](/http-api/permissions/). -Neither permission is subject to the guild [MFA level](/http-api/guilds/#mfa-levels). Fluxer resolves the guild, then the purge eligibility, then the emoji, and only then the permission, so a purging request in a guild without the required feature is refused before the emoji is looked up. +Neither permission is subject to the guild [MFA level](/http-api/guilds/#mfa-levels). Fluxer resolves the guild, then the purge eligibility, then the emoji, and only then the permission. A purging request in a guild without the required feature is refused before the emoji is looked up. :::caution[A purge cannot be undone through the API] Without `purge`, the emoji leaves the guild but its image remains available through the [Media Proxy](/media-proxy/routes/#image-asset-contract). With `purge`, the stored object and its `webp` and `gif` CDN representations are queued for permanent deletion. 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 a894922f5..b0df781b7 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 @@ -87,7 +87,7 @@ The result has no `avatar`, `banner`, `accent_color`, `mute`, `deaf`, `communica ## Guild member search supplemental object -A guild member search supplemental object has the join provenance the [guild member object](/http-api/guild-members/#guild-member-object) never exposes. Only a caller that can already manage the guild reads it. +A guild member search supplemental object has the join source the [guild member object](/http-api/guild-members/#guild-member-object) never exposes. Only a caller that can already manage the guild reads it. ### Structure @@ -121,7 +121,7 @@ One search returns this envelope. Every field is always present. 3 A true value always comes with an empty `members` array and both counts at zero -An Elasticsearch instance reports the true `total_result_count`. A Meilisearch instance reports an estimate that saturates at 10000. In a very large guild that value can be lower than the true number of matches. +An Elasticsearch instance reports the true `total_result_count`. A Meilisearch instance reports an estimate that caps at 10000. In a very large guild that value can be lower than the true number of matches. ### Example @@ -149,7 +149,7 @@ Searches the guild member index. Returns a [guild member search response](#guild A guild that does not exist returns 404 `UNKNOWN_GUILD`. -[List guild members](/http-api/guild-members/#list-guild-members) needs only membership and returns the complete member object for everyone. The permissions above gate the filtering, the sorting, the deep paging, and the [supplemental](#guild-member-search-supplemental-object) join provenance. +[List guild members](/http-api/guild-members/#list-guild-members) needs only membership and returns the complete member object for everyone. The permissions above gate the filtering, the sorting, the deep paging, and the [supplemental](#guild-member-search-supplemental-object) join source. ### Path parameters @@ -212,7 +212,7 @@ Elasticsearch walks past its 10000 document window with an internal cursor and s 1 The [error code](/http-api/errors/) is `MISSING_ACCESS` for an unavailable guild, `CONTENT_BLOCKED` for a blocked body string, and `MISSING_PERMISSIONS` otherwise -An index that has never been built, or that predates the instance forced reindex point, returns 200 with an empty page and `indexing` set to true, and the request schedules the rebuild. The same empty body with `indexing` false covers a guild whose record disappeared between the membership check and the index read, an instance with no search backend configured, and an instance still initialising. This operation never returns `FEATURE_TEMPORARILY_DISABLED`. +An index that has never been built, or that predates the instance forced reindex point, returns 200 with an empty page and `indexing` set to true, and the request schedules the rebuild. The same empty body with `indexing` false covers a guild whose record disappeared between the membership check and the index read, an instance with no search backend configured, and an instance still starting up. This operation never returns `FEATURE_TEMPORARILY_DISABLED`. :::note[`indexing` false does not mean zero matches] True means the rebuild is scheduled and the page is empty for that reason alone. False covers a genuine zero-match search and every other index that could not answer. @@ -220,7 +220,7 @@ True means the rebuild is scheduled and the page is empty for that reason alone. ### Side effects -The route enqueues a rebuild of the guild's member index when the guild has never been indexed or was last indexed before the instance forced reindex point. +The route queues a rebuild of the guild's member index when the guild has never been indexed or was last indexed before the instance forced reindex point. ### Rate limit 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 de719df30..36fc4a59e 100644 --- a/fluxer_docs/src/content/docs/http-api/guild-members.mdx +++ b/fluxer_docs/src/content/docs/http-api/guild-members.mdx @@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro'; A guild member is an account that has joined a guild. The membership has a nickname, avatar, role set, and moderation state that apply in that guild alone. Indexed member queries live on [Guild member search](/http-api/guild-member-search/), and bans on [Guild moderation](/http-api/guild-moderation/). -A guild that no Gateway process serves returns 404 `UNKNOWN_GUILD`. An existing guild that does not hold the caller as 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. +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. @@ -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 suppresses inheritance and renders the default. +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. | Value | Name | Description | | --- | --- | --- | @@ -127,7 +127,7 @@ The request body shared by [Modify current guild member](#modify-current-guild-m A `roles` entry that does not resolve to an existing role of the guild is dropped from the replacement, so a request naming only unknown roles clears the member's role set. -An `avatar` or `banner` base64 payload longer than 13981016 characters returns `BASE64_LENGTH_INVALID`, and a malformed one returns `INVALID_BASE64_FORMAT`. Fluxer then checks the decoded bytes against the instance-configured `avatar_max_size` [limit](/http-api/instance/#limit-keys), whose stock value is 10485760, and the same ceiling covers both fields. The decoded image must also pass the format allowlist and the animation rules of the asset policy for the field it sets. Pixel dimensions are not enforced. A value that is too large returns `IMAGE_SIZE_EXCEEDS_LIMIT`, and one whose format or animation is not allowed returns `INVALID_IMAGE_FORMAT`. +An `avatar` or `banner` base64 payload longer than 13981016 characters returns `BASE64_LENGTH_INVALID`, and a malformed one returns `INVALID_BASE64_FORMAT`. Fluxer then checks the decoded bytes against the instance-configured `avatar_max_size` [limit](/http-api/instance/#limit-keys), whose stock value is 10485760. The same limit applies to both fields. The decoded image must also pass the format allowlist and the animation rules of the asset policy for the field it sets. Pixel dimensions are not enforced. A value that is too large returns `IMAGE_SIZE_EXCEEDS_LIMIT`, and one whose format or animation is not allowed returns `INVALID_IMAGE_FORMAT`. Null and any `communication_disabled_until` that is not in the future both clear the timeout. A time more than 365.25 days ahead returns `TIMEOUT_CANNOT_EXCEED_365_DAYS`, and a value that passes the schema but is not a real instant returns `INVALID_TIMEOUT_VALUE`. `timeout_reason` has no effect unless `communication_disabled_until` is also supplied. @@ -139,7 +139,7 @@ Omitting `connection_id` applies the move or the disconnect to every voice conne Returns a page of [guild member objects](#guild-member-object) in ascending user ID order. Requires membership of the guild and no permission. -This route consults no permission bit, including [VIEW_CHANNEL_MEMBERS](/http-api/permissions/). +This route checks no permission bit, including [VIEW_CHANNEL_MEMBERS](/http-api/permissions/). ### Path parameters @@ -233,14 +233,14 @@ Modifies the authenticated account's own membership and returns the resulting [g - Changing the nickname requires [CHANGE_NICKNAME](/http-api/permissions/). - Supplying `channel_id` requires [MOVE_MEMBERS](/http-api/permissions/), even for the caller's own membership. -- Supplying `mute` requires [MUTE_MEMBERS](/http-api/permissions/) and supplying `deaf` requires [DEAFEN_MEMBERS](/http-api/permissions/). +- Supplying `mute` requires [MUTE_MEMBERS](/http-api/permissions/), and `deaf` requires [DEAFEN_MEMBERS](/http-api/permissions/). - Supplying `nick`, `avatar`, `banner`, `bio`, `pronouns`, `accent_color`, or `profile_flags` requires a verified email address, and a bot account is exempt. An unverified account is rejected with 403 `PROFILE_EMAIL_VERIFICATION_REQUIRED`. -A caller without `CHANGE_NICKNAME` does not fail the request. Fluxer discards the nickname change and applies every other supplied field. A member who holds the permission but is currently timed out is rejected with 403 `COMMUNICATION_DISABLED`. +A caller without `CHANGE_NICKNAME` does not fail the request. Fluxer discards the nickname change and applies every other supplied field. A member who holds the permission but is timed out is rejected with 403 `COMMUNICATION_DISABLED`. A supplied `nick`, `bio`, or `pronouns` is scanned against the instance phrase, URL, and profile substring blocklists, and a match returns 403 `CONTENT_BLOCKED`. -Guild avatar, banner, biography, and accent colour additionally require the instance-configured `feature_per_guild_profiles` [limit](/http-api/instance/#limit-keys) for the calling account. Fluxer discards any of those fields supplied without that capability and applies every other field. Pronouns, `profile_flags`, and `mention_flags` do not require it. +Guild avatar, banner, biography, and accent colour also require the instance-configured `feature_per_guild_profiles` [limit](/http-api/instance/#limit-keys) for the calling account. Fluxer discards any of those fields supplied without that capability and applies every other field. Pronouns, `profile_flags`, and `mention_flags` do not require it. A `channel_id` destination must be a guild voice channel, and the caller must hold both [VIEW_CHANNEL](/http-api/permissions/) and [CONNECT](/http-api/permissions/) there. Supplying `channel_id` for a member with no live voice connection returns 400 `USER_NOT_IN_VOICE`. Supplying `mute` or `deaf` for such a member succeeds, stores the flags, and emits no voice Dispatch. @@ -250,7 +250,7 @@ Fluxer rejects a supplied `communication_disabled_until` with 403 `MISSING_PERMI Avatar, banner, biography, pronouns, and accent colour each have a limit of 25 changes per 30 minutes for each authenticated user and guild. Exhausting one returns 400 `INVALID_FORM_BODY` and applies no field at all. ::: -The avatar and banner limits are consumed whenever the field is present. The biography, pronouns, and accent colour limits are consumed only when the supplied value differs from the stored one. The field code is `AVATAR_CHANGED_TOO_MANY_TIMES`, `BANNER_CHANGED_TOO_MANY_TIMES`, `BIO_CHANGED_TOO_MANY_TIMES`, `PRONOUNS_CHANGED_TOO_MANY_TIMES`, or `ACCENT_COLOR_CHANGED_TOO_MANY_TIMES`. +Avatar and banner consume their limit whenever the field is present. Biography, pronouns, and accent colour consume theirs only when the supplied value differs from the stored one. The field code is `AVATAR_CHANGED_TOO_MANY_TIMES`, `BANNER_CHANGED_TOO_MANY_TIMES`, `BIO_CHANGED_TOO_MANY_TIMES`, `PRONOUNS_CHANGED_TOO_MANY_TIMES`, or `ACCENT_COLOR_CHANGED_TOO_MANY_TIMES`. ### Path parameters @@ -277,11 +277,11 @@ The body is a [guild member update object](#guild-member-update-object) without ### Side effects -The operation updates the membership, replaces any supplied guild avatar or banner, updates member search results, records a [`MEMBER_UPDATE`](/http-api/guild-audit-logs/#audit-actions) audit entry, and emits [Guild Member Update](/gateway/events/#guild-member-update). +Fluxer updates the membership, replaces any supplied guild avatar or banner, updates member search results, records a [`MEMBER_UPDATE`](/http-api/guild-audit-logs/#audit-actions) audit entry, and emits [Guild Member Update](/gateway/events/#guild-member-update). The target's own sessions always receive the Dispatch. Other guild sessions receive it subject to [event filtering](/gateway/event-filtering/). The audit entry and Dispatch are produced even when the resulting membership is unchanged, and an entry whose change list is empty is published without a `changes` member. -Supplying `channel_id` moves or disconnects every voice connection the member holds, or only the one named by `connection_id`. It records one [`MEMBER_MOVE`](/http-api/guild-audit-logs/#audit-actions) or [`MEMBER_DISCONNECT`](/http-api/guild-audit-logs/#audit-actions) audit entry for the whole request, whatever the number of connections it touched, and emits [Voice State Update](/gateway/events/#voice-state-update) to sessions that can view the affected channel. Supplying `mute` or `deaf` updates the membership, records no separate audit entry, and emits the same voice Dispatch once for each live connection. +Supplying `channel_id` moves or disconnects every voice connection the member holds, or only the one named by `connection_id`. It records one [`MEMBER_MOVE`](/http-api/guild-audit-logs/#audit-actions) or [`MEMBER_DISCONNECT`](/http-api/guild-audit-logs/#audit-actions) audit entry for the whole request, however many connections it touched. It emits [Voice State Update](/gateway/events/#voice-state-update) to sessions that can view the affected channel. Supplying `mute` or `deaf` updates the membership, records no separate audit entry, and emits the same voice Dispatch once for each live connection. Fluxer emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create) for every audit entry. A request that fails after uploading a new avatar or banner leaves the previous asset in place. @@ -305,15 +305,15 @@ Modifies another member's guild state and returns the resulting [guild member ob A supplied `nick` is scanned against the instance phrase, URL, and profile substring blocklists, and a match returns 403 `CONTENT_BLOCKED`. -Hierarchy authority over the target is not consulted for `roles`, so a caller who outranks every affected role can replace the role set of a member who outranks them. The everyone role in the array is rejected with the field code `INVALID_ROLE_ID`. +Hierarchy authority over the target is not checked for `roles`, so a caller who outranks every affected role can replace the role set of a member who outranks them. The everyone role in the array is rejected with the field code `INVALID_ROLE_ID`. Fluxer never checks the target's own permissions in the destination, so a member can be moved into a channel they could not join themselves. Disconnecting sets `channel_id` to null and requires the same permission. Supplying `channel_id` for a target with no live voice connection, or a `connection_id` that is not one of the target's own, returns 400 `USER_NOT_IN_VOICE`. Supplying `mute` or `deaf` for such a target succeeds and stores the flags. A destination that does not exist returns the field code `CHANNEL_DOES_NOT_EXIST`, and one that is not a voice channel `CHANNEL_MUST_BE_VOICE`. -`MANAGE_ROLES` and `MODERATE_MEMBERS` are [elevated permissions](/http-api/permissions/#elevated-permissions), so replacing `roles` or applying `communication_disabled_until` additionally requires an enrolled multi-factor authenticator when the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated and the caller is not the guild owner. +`MANAGE_ROLES` and `MODERATE_MEMBERS` are [elevated permissions](/http-api/permissions/#elevated-permissions). When the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated, a caller who is not the guild owner also needs an enrolled multi-factor authenticator to replace `roles` or apply `communication_disabled_until`. -A caller addressing their own user ID here falls under the self-targeted rules of [Modify current guild member](#modify-current-guild-member) for the guild profile fields. The body remains the complete update object, so `roles` is accepted. Replacing their own role set still requires `MANAGE_ROLES` and hierarchy authority, and `communication_disabled_until` is still rejected with 403 `MISSING_PERMISSIONS`. +A caller addressing their own user ID here follows the self-targeted rules of [Modify current guild member](#modify-current-guild-member) for the guild profile fields. The body remains the complete update object, so `roles` is accepted. Replacing their own role set still requires `MANAGE_ROLES` and hierarchy authority, and `communication_disabled_until` is still rejected with 403 `MISSING_PERMISSIONS`. :::note[Guild profile fields belong to their own account] `avatar`, `banner`, `bio`, `pronouns`, `accent_color`, `profile_flags`, and `mention_flags` describe the target's own profile, so a moderator cannot set them. Supplying one for another member leaves it unwritten, and the request still applies every field the caller may set. @@ -364,7 +364,7 @@ The operation has the same membership, audit, search, [Guild Member Update](/gat Removes a member from the guild and returns 204 with an empty body. Requires membership, [KICK_MEMBERS](/http-api/permissions/), and role hierarchy authority over the target. Emits a [Guild Member Remove](/gateway/events/#guild-member-remove) Gateway event. -`KICK_MEMBERS` is an [elevated permission](/http-api/permissions/#elevated-permissions), so it additionally requires an enrolled multi-factor authenticator when the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated and the caller is not the guild owner. +`KICK_MEMBERS` is an [elevated permission](/http-api/permissions/#elevated-permissions), so a caller who is not the guild owner needs an enrolled multi-factor authenticator while the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated. The caller and the guild owner cannot be removed through this operation, and both are reported as 404 `UNKNOWN_MEMBER`. @@ -410,9 +410,9 @@ Transfers guild ownership to another current member and returns the updated [gui - No permission bit substitutes for ownership. - The target must be a current member. -- The target must not be a bot account, and a bot target returns 400 `CANNOT_TRANSFER_OWNERSHIP_TO_BOT`. +- A bot target returns 400 `CANNOT_TRANSFER_OWNERSHIP_TO_BOT`. -The operation requires sudo mode as described by the [sudo verification fields](/http-api/guilds/#sudo-verification-fields), which a bot credential satisfies without a proof. Fluxer verifies sudo mode before ownership, so a member who is not the owner still has to prove sudo mode before receiving 403 `MISSING_PERMISSIONS`. An account that supplies no usable proof receives 403 `SUDO_MODE_REQUIRED`, whose payload names the second factors the account holds. +The operation requires sudo mode as described by the [sudo verification fields](/http-api/guilds/#sudo-verification-fields), which a bot credential satisfies without a proof. Fluxer verifies sudo mode before ownership, so a member who is not the owner must still prove sudo mode before receiving 403 `MISSING_PERMISSIONS`. An account that supplies no usable proof receives 403 `SUDO_MODE_REQUIRED`, whose payload names the second factors the account holds. ### Path parameters @@ -470,7 +470,7 @@ Adds one role to a member and returns 204 with an empty body. Requires membershi - The guild owner bypasses the permission and the hierarchy check. - The everyone role cannot be assigned and is rejected with the field code `INVALID_ROLE_ID`. -`MANAGE_ROLES` is an [elevated permission](/http-api/permissions/#elevated-permissions), so it additionally requires an enrolled multi-factor authenticator when the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated and the caller is not the guild owner. +`MANAGE_ROLES` is an [elevated permission](/http-api/permissions/#elevated-permissions), so a caller who is not the guild owner needs an enrolled multi-factor authenticator while the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated. The guild owner receives 404 `UNKNOWN_ROLE` for a role that does not exist in the guild. Any other caller receives 403 `MISSING_PERMISSIONS`. @@ -516,7 +516,7 @@ Removes one role from a member and returns 204 with an empty body. Requires memb - The guild owner bypasses the permission and the hierarchy check. - The everyone role cannot be removed and is rejected with the field code `INVALID_ROLE_ID`. -`MANAGE_ROLES` is an [elevated permission](/http-api/permissions/#elevated-permissions), so it additionally requires an enrolled multi-factor authenticator when the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated and the caller is not the guild owner. A role that does not exist is reported exactly as it is for [Add guild member role](#add-guild-member-role). +`MANAGE_ROLES` is an [elevated permission](/http-api/permissions/#elevated-permissions), so a caller who is not the guild owner needs an enrolled multi-factor authenticator while the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated. A role that does not exist is reported the same way as for [Add guild member role](#add-guild-member-role). ### Path parameters diff --git a/fluxer_docs/src/content/docs/http-api/guild-moderation.mdx b/fluxer_docs/src/content/docs/http-api/guild-moderation.mdx index 1fcfff6b1..6481d4693 100644 --- a/fluxer_docs/src/content/docs/http-api/guild-moderation.mdx +++ b/fluxer_docs/src/content/docs/http-api/guild-moderation.mdx @@ -52,7 +52,7 @@ Accepting an [invite](/http-api/invites/) compares the joining account against t Fluxer skips the address match when the joining address is on the instance exemption list. It also skips the match when the banned address is a single address and the joining address is reported as high mobile or carrier-grade NAT blast radius. The match runs when that report is unavailable or fails. -The email comparison is exact and runs only when an invite is accepted. An email match refuses the join with 403 `USER_BANNED_FROM_GUILD`. Every other path that adds a member compares the account identifier and the address alone, and several compare nothing. [Grant OAuth2 consent](/http-api/oauth2/#grant-oauth2-consent) installing a bot, the admin membership operations, the stock community auto-join, and the premium entitlement guild join all add the member without consulting the ban list. +The email comparison is exact and runs only when an invite is accepted. An email match refuses the join with 403 `USER_BANNED_FROM_GUILD`. Every other path that adds a member compares the account identifier and the address alone, and several compare nothing. [Grant OAuth2 consent](/http-api/oauth2/#grant-oauth2-consent) installing a bot, the admin membership operations, the stock community auto-join, and the premium entitlement guild join all add the member without checking the ban list. [Remove guild ban](#remove-guild-ban) releases both blocks. Permanently deleting the banned account deletes every guild ban it holds, and that releases both blocks in every guild at once. @@ -77,7 +77,7 @@ The operation returns the complete collection in one response and has no limit o | Status | Body | Condition | | --- | --- | --- | | 200 | array[[guild ban](#guild-ban-object) object] | Bans were returned | -| 4001 | [error response](/http-api/#error-response) | Caller holds `BAN_MEMBERS` without an enrolled authenticator in an elevated-MFA guild | +| 4001 | [error response](/http-api/#error-response) | Caller holds `BAN_MEMBERS` with no enrolled authenticator in an elevated-MFA guild | | 4032 | [error response](/http-api/#error-response) | Guild is unavailable, or `BAN_MEMBERS` is absent | | 404 | [error response](/http-api/#error-response) | Guild does not exist and the request returns `UNKNOWN_GUILD` | @@ -98,7 +98,7 @@ Creates a guild ban, or replaces an existing one, and returns 204 with an empty ### Limitations - The caller cannot ban themselves, and Fluxer reports a self-target as 404 `UNKNOWN_MEMBER`. -- Banning a target who is currently a member also requires role hierarchy authority over that member. The guild owner holds that authority over everyone, and no other caller holds it over the owner. +- Banning a target who is a member also requires role hierarchy authority over that member. The guild owner holds that authority over everyone, and no other caller holds it over the owner. - A target who is not a member can still be banned, and the ban pre-empts a future join. Fluxer scans every string in a JSON request body against the instance phrase and URL blocklists, and that includes `reason`. A blocked value returns 403 `CONTENT_BLOCKED` before the rate limit bucket, the credential check, and the permission check. @@ -142,7 +142,7 @@ The second ban overwrites the stored reason, moderator, issue time, expiry, addr | Status | Body | Condition | | --- | --- | --- | | 204 | empty | Ban was created or replaced | -| 4001 | [error response](/http-api/#error-response) | Duration, deletion interval, or reason is invalid, or the caller holds `BAN_MEMBERS` without an enrolled authenticator in an elevated-MFA guild | +| 4001 | [error response](/http-api/#error-response) | Duration, deletion interval, or reason is invalid, or the caller holds `BAN_MEMBERS` with no enrolled authenticator in an elevated-MFA guild | | 4032 | [error response](/http-api/#error-response) | Guild is unavailable, blocked content was supplied, `BAN_MEMBERS` is absent, or hierarchy authority over a member target is absent | | 404 | [error response](/http-api/#error-response) | Guild does not exist and the request returns `UNKNOWN_GUILD` | | 4043 | [error response](/http-api/#error-response) | Target account does not exist, or the target is the caller | @@ -157,7 +157,7 @@ The second ban overwrites the stored reason, moderator, issue time, expiry, addr The operation stores the ban together with the target's last known IP address and account email. It records a [`MEMBER_BAN_ADD`](/http-api/guild-audit-logs/#audit-actions) audit entry whose change list has the stored [guild ban change fields](/http-api/guild-audit-logs/#guild-ban-change-fields), emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create), and emits [Guild Ban Add](/gateway/events/#guild-ban-add) to the guild's sessions, subject to [event filtering](/gateway/event-filtering/). A positive deletion interval schedules permanent deletion of the target's newer guild messages. -A target that is a current member also loses that membership. The guild member count decreases, the guild's sessions receive [Guild Member Remove](/gateway/events/#guild-member-remove), the banned account's sessions receive [Guild Delete](/gateway/events/#guild-delete), and the membership is dropped from the member search index of an indexed guild. Fluxer records no `MEMBER_KICK` entry alongside the ban entry. +A target that is a current member also loses that membership. The guild member count decreases. The guild's sessions receive [Guild Member Remove](/gateway/events/#guild-member-remove), the banned account's sessions receive [Guild Delete](/gateway/events/#guild-delete), and the membership is dropped from the member search index of an indexed guild. Fluxer records no `MEMBER_KICK` entry alongside the ban entry. A ban does not preserve an active communication timeout for a later rejoin, and [Remove guild member](/http-api/guild-members/#remove-guild-member) does. The banned account loses its guild nickname, guild avatar hash, guild banner hash, biography, pronouns, accent colour, and role set. Its read states and guild settings stay in place. @@ -173,7 +173,7 @@ From that point Fluxer refuses a join attempt by the banned account. The address Removes an existing guild ban and returns 204 with an empty body. Requires [BAN_MEMBERS](/http-api/permissions/). Emits a [Guild Ban Remove](/gateway/events/#guild-ban-remove) Gateway event. -Fluxer rejects a target that is not currently banned with 400 `INVALID_FORM_BODY`. +Fluxer rejects a target that is not banned with 400 `INVALID_FORM_BODY`. ### Path parameters @@ -187,7 +187,7 @@ Fluxer rejects a target that is not currently banned with 400 `INVALID_FORM_BODY | Status | Body | Condition | | --- | --- | --- | | 204 | empty | Ban was removed | -| 4001 | [error response](/http-api/#error-response) | Target is not banned, or the caller holds `BAN_MEMBERS` without an enrolled authenticator in an elevated-MFA guild | +| 4001 | [error response](/http-api/#error-response) | Target is not banned, or the caller holds `BAN_MEMBERS` with no enrolled authenticator in an elevated-MFA guild | | 4032 | [error response](/http-api/#error-response) | Guild is unavailable, or `BAN_MEMBERS` is absent | | 404 | [error response](/http-api/#error-response) | Guild does not exist and the request returns `UNKNOWN_GUILD` | 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 9319e61d7..bb9e5b4d4 100644 --- a/fluxer_docs/src/content/docs/http-api/guild-stickers.mdx +++ b/fluxer_docs/src/content/docs/http-api/guild-stickers.mdx @@ -35,7 +35,7 @@ A guild sticker object is the complete stored form of one sticker. The stored im 2 Detected from the submitted image at creation, and copied unchanged by [Clone guild sticker](#clone-guild-sticker) -3 Present only on [List guild stickers](#list-guild-stickers), where it is populated for every caller and requires no permission +3 Present only on [List guild stickers](#list-guild-stickers), where it is set for every caller and requires no permission A sticker is always a guild sticker, and `animated` alone describes its format. The object has no sticker type, format type, or availability field. @@ -76,7 +76,7 @@ The item shape shared by [Create guild sticker](#create-guild-sticker) and each 3 A data URL prefix is accepted and everything up to the first comma is removed before any bound applies -The remaining encoded portion is bounded to 699052 characters, which encodes a full 524288 byte image, and a longer value returns the validation code `BASE64_LENGTH_INVALID` at the `image` path. A value that is not canonical base64 with correct padding returns `INVALID_BASE64_FORMAT`. +The remaining encoded part is bounded to 699052 characters, which encodes a full 524288 byte image, and a longer value returns the validation code `BASE64_LENGTH_INVALID` at the `image` path. A value that is not canonical base64 with correct padding returns `INVALID_BASE64_FORMAT`. Decoded bytes are at most the operator-configured `sticker_max_size` [limit key](/http-api/instance/#limit-keys) value, resolved against the guild's complete feature set and defaulting to 524288. A larger image is rejected on the `image` path with `IMAGE_SIZE_EXCEEDS_LIMIT`, whose message names the resolved ceiling. An image over 524289 bytes exceeds the 699052 character bound and returns `BASE64_LENGTH_INVALID`, so a configured value above 524289 never applies. @@ -168,7 +168,7 @@ One response has the complete collection. The operation is not paginated. A stic Creates one sticker from submitted image data and returns its [guild sticker object](#guild-sticker-object) without `user`. Requires membership of the guild and [CREATE_EXPRESSIONS](/http-api/permissions/). Emits a [Guild Stickers Update](/gateway/events/#guild-stickers-update) Gateway event. -The admission checks run in a fixed order. Fluxer scans the submitted name, description, and tags for prohibited content, then resolves guild existence and membership, then `CREATE_EXPRESSIONS`, then the slot limit, and only then the image. A guild at its slot limit therefore returns `MAX_STICKERS` even when the image would also have been rejected. +Admission checks run in a fixed order. Fluxer scans the submitted name, description, and tags for prohibited content, then resolves guild existence and membership, then `CREATE_EXPRESSIONS`, then the slot limit, and only then the image. A guild at its slot limit therefore returns `MAX_STICKERS` even when the image would also have been rejected. The slot limit is the operator-configured [max_guild_stickers](/http-api/instance/#limit-keys) value resolved against the guild's complete feature set, defaulting to 500. A guild with [UNLIMITED_STICKERS](/http-api/guilds/#guild-features) bypasses that configuration and receives a fixed ceiling of 999999. @@ -248,7 +248,7 @@ The global body screen runs before the batch starts and ignores strings shorter ### Side effects -Each successful item consumes one guild sticker slot, stores its image, and writes the supplied audit reason into its [`STICKER_CREATE`](/http-api/guild-audit-logs/#audit-actions) entry. A failed item leaves no sticker, audit entry, or slot consumption. +Each successful item consumes one guild sticker slot, stores its image, and writes the supplied audit reason into its [`STICKER_CREATE`](/http-api/guild-audit-logs/#audit-actions) entry. A failed item leaves no sticker or audit entry and consumes no slot. Fluxer emits one [Guild Stickers Update](/gateway/events/#guild-stickers-update) after the batch when at least one item succeeded, with the guild's complete sticker collection. A batch in which no item succeeded emits nothing. diff --git a/fluxer_docs/src/content/docs/http-api/guilds.mdx b/fluxer_docs/src/content/docs/http-api/guilds.mdx index 202238dbb..23377df9a 100644 --- a/fluxer_docs/src/content/docs/http-api/guilds.mdx +++ b/fluxer_docs/src/content/docs/http-api/guilds.mdx @@ -10,9 +10,9 @@ A guild is a community with its own channels, roles, members, and configuration. [List current user guilds](#list-current-user-guilds) and [Get guild](#get-guild) declare the `guilds` [OAuth2 scope](/http-api/oauth2/#oauth2-scopes), and a bearer credential without that scope receives 403 `MISSING_OAUTH_SCOPE`. Every other route rejects a bearer credential with 403 `ACCESS_DENIED`. -A guild that does not exist returns 404 `UNKNOWN_GUILD`, and an existing guild that does not hold the caller as a member returns 403 `MISSING_PERMISSIONS`, so any authenticated caller can tell a missing guild from one they are not in. A guild whose stored record exists while the main Gateway reports it unknown returns 403 `ACCESS_DENIED`. +A guild that does not exist returns 404 `UNKNOWN_GUILD`. An existing guild that does not hold the caller as a member returns 403 `MISSING_PERMISSIONS`. So any authenticated caller can tell a missing guild from one they are not in. A guild whose stored record exists while the main Gateway reports it unknown returns 403 `ACCESS_DENIED`. -Fluxer screens a route whose path opens with `/guilds/{guild_id}` for [guild availability](#guild-features) before the operation runs. [Create guild](#create-guild), [List current user guilds](#list-current-user-guilds), [Leave guild](#leave-guild), and [Bulk delete current user's guild messages](#bulk-delete-current-users-guild-messages) do not have the guild in their first two path segments, so a forced-unavailable guild is still listed and can still be left. +Fluxer checks [guild availability](#guild-features) on a route whose path opens with `/guilds/{guild_id}` before the operation runs. [Create guild](#create-guild), [List current user guilds](#list-current-user-guilds), [Leave guild](#leave-guild), and [Bulk delete current user's guild messages](#bulk-delete-current-users-guild-messages) do not have the guild in their first two path segments, so a forced-unavailable guild is still listed and can still be left. The instance-wide content filter screens the JSON body of every `POST`, `PUT`, and `PATCH` route here before the route runs. A string of at least 3 characters matching the phrase blocklist, or a URL matching the URL blocklist, returns 403 `CONTENT_BLOCKED`. The `icon`, `banner`, `splash`, `embed_splash`, `permissions`, `roles`, `channels`, `password`, `mfa_code`, `webauthn_response`, and `webauthn_challenge` members are exempt. The guild `name` is scanned a second time inside [Create guild](#create-guild) and [Modify guild](#modify-guild), without the 3 character floor. @@ -130,7 +130,7 @@ A guild object contains the guild's configuration. The operation that returns it ## Partial guild object -A partial guild object is the smaller guild shape that appears inside invite payloads. It has identity and presentation fields only, and the feature-gated asset rules of the complete [guild object](#guild-object) apply to it unchanged. +A partial guild object is the smaller shape that appears inside invite payloads. It has identity and display fields only, and the feature-gated asset rules of the complete [guild object](#guild-object) apply to it unchanged. ### Structure @@ -303,7 +303,7 @@ The guild owner, a bot, and any member holding at least one role bypass the chec 1 Only the guild owner can change this value, the owner account needs a second factor already configured, and the change requires sudo mode -While the level is ELEVATED, a caller other than the guild owner exercises the [elevated permissions](/http-api/permissions/#elevated-permissions) only with an enrolled authenticator, and a bot inherits the enrolment state of its application owner. An operation that asserts an elevated permission the caller holds but cannot exercise returns 400 `TWO_FACTOR_REQUIRED` after the permission itself has been confirmed. +While the level is ELEVATED, a non-owner caller uses the [elevated permissions](/http-api/permissions/#elevated-permissions) only with an enrolled authenticator. A bot inherits the enrolment state of its application owner. An operation that asserts an elevated permission the caller holds but cannot use returns 400 `TWO_FACTOR_REQUIRED` after confirming the permission itself. ## Splash card alignments @@ -372,7 +372,7 @@ A set bit disables the named behaviour across the guild. Only the [Admin API](/a ## Guild features -A feature is a capability or availability flag in the guild's `features` array. +Each value in the guild's `features` array is a capability or availability flag. | Value | Description | | --- | --- | @@ -413,7 +413,7 @@ A feature is a capability or availability flag in the guild's `features` array. 4 An authenticated request whose path opens with `/guilds/{guild_id}` or `/channels/{channel_id}`, where that channel belongs to this guild, is rejected before the operation runs with 403 `MISSING_ACCESS`. `UNAVAILABLE_FOR_EVERYONE` applies to the guild owner exactly as it applies to every other member, while `UNAVAILABLE_FOR_EVERYONE_BUT_STAFF` exempts an account with the instance staff flag -5 The feature raises the default `max_guild_members` limit from 1000000 to 10000000 before the ordered [limit configuration](/http-api/instance/#limit-keys) is consulted, so the raised ceiling applies even when no configured rule names the feature +5 The feature raises the default `max_guild_members` limit from 1000000 to 10000000 before the ordered [limit configuration](/http-api/instance/#limit-keys) is checked, so the raised ceiling applies even when no configured rule names the feature ## Custom invite URL object @@ -483,7 +483,7 @@ Creates a guild owned by the caller. Requires a user session credential. Returns - A caller already at the configured guild limit is rejected with 400 `MAX_GUILDS`. - While the instance's single community policy is active, every caller is rejected with 400 `SINGLE_COMMUNITY_CANNOT_CREATE_GUILDS`. -Fluxer decides the single community refusal first, so it precedes the email, bot, unclaimed, and guild limit failures. +Fluxer decides the single community refusal before the email, bot, unclaimed, and guild limit failures. ### JSON body @@ -492,7 +492,7 @@ Fluxer decides the single community refusal first, so it precedes the email, bot | name1 | string | Guild name (1-100 characters) | | icon?2 | ?base64 string | Guild icon | | empty_features?3 | boolean | Whether to create the guild with no features (default false) | -| template? | [guild creation template object](#guild-creation-template-object) | Roles and channels to provision for the new guild | +| template? | [guild creation template object](#guild-creation-template-object) | Roles and channels to create for the new guild | 1 Fluxer trims the value before it measures the length bound @@ -549,7 +549,7 @@ Returns an array of [guild objects](#guild-object), one for every guild the auth 2 A guild whose counts cannot be fetched is returned without them -Fluxer drops a membership whose guild record can no longer be resolved before it applies the cursor and `limit`, so the page still has up to `limit` guilds when further guilds remain. This route runs no availability screen, and a guild with `UNAVAILABLE_FOR_EVERYONE` is still listed. +Fluxer drops a membership whose guild record can no longer be resolved before it applies the cursor and `limit`, so the page still has up to `limit` guilds when further guilds remain. This route runs no availability check, and a guild with `UNAVAILABLE_FOR_EVERYONE` is still listed. :::caution[`permissions` needs a limit of 100 or lower] A page of more than 100 guilds, or a failed lookup, omits `permissions` from every guild. The request still succeeds. @@ -602,9 +602,9 @@ The response has no `permissions` field. Read [List current user guilds](#list-c Modifies guild configuration and returns the updated [guild object](#guild-object). Requires membership and [MANAGE_GUILD](/http-api/permissions/). Emits a [Guild Update](/gateway/events/#guild-update) Gateway event to every session that can see the guild. -`MANAGE_GUILD` is an [elevated permission](/http-api/permissions/#elevated-permissions). While the guild [MFA level](#mfa-levels) is elevated, a caller other than the owner also needs an enrolled authenticator, and receives 400 `TWO_FACTOR_REQUIRED` without one. +`MANAGE_GUILD` is an [elevated permission](/http-api/permissions/#elevated-permissions). While the guild [MFA level](#mfa-levels) is elevated, a non-owner caller also needs an enrolled authenticator, and receives 400 `TWO_FACTOR_REQUIRED` without one. -Changing `mfa_level` to a different value additionally requires the guild owner, an owner account that already has a second factor, and sudo mode. A caller who is not the owner receives 403 `MISSING_PERMISSIONS` for that change even while holding `MANAGE_GUILD`. Sudo mode is verified before ownership, so a non-owner without a proof receives 403 `SUDO_MODE_REQUIRED` first. +Changing `mfa_level` to a different value also requires the guild owner, an owner account that already has a second factor, and sudo mode. A caller who is not the owner receives 403 `MISSING_PERMISSIONS` for that change even while holding `MANAGE_GUILD`. Sudo mode is verified before ownership, so a non-owner without a proof receives 403 `SUDO_MODE_REQUIRED` first. ### Path parameters @@ -664,7 +664,7 @@ Every field is optional. An omitted field preserves its current value, and a fie 7 Sending the value the guild already holds needs neither ownership nor sudo mode, and an owner without a configured second factor is rejected with the field code `MUST_ENABLE_2FA_BEFORE_REQUIRING_FOR_MODS` -8 The only accepted values are 0 and 3, and `nsfw` takes precedence when both fields are supplied. Setting 3 through this field also raises the content warning level to CONTENT_WARNING when `content_warning_level` is absent from the same body and the guild currently sits at INHERIT +8 The only accepted values are 0 and 3, and `nsfw` takes precedence when both fields are supplied. Setting 3 through this field also raises the content warning level to CONTENT_WARNING when `content_warning_level` is absent from the same body and the guild content warning level is INHERIT 9 The write also sets `nsfw_level` to 3 when true and 0 when false, and unlike `nsfw_level` it never changes the content warning level @@ -672,7 +672,7 @@ Every field is optional. An omitted field preserves its current value, and a fie 11 The value is trimmed, and a value that is empty after trimming is stored as null -12 A non-null value requires `BANNER`, and an animated image additionally requires `ANIMATED_BANNER`, otherwise the request is rejected with the field codes `GUILD_BANNER_REQUIRES_FEATURE` or `ANIMATED_GUILD_BANNER_REQUIRES_FEATURE` +12 A non-null value requires `BANNER`, and an animated image also requires `ANIMATED_BANNER`, otherwise the request is rejected with the field codes `GUILD_BANNER_REQUIRES_FEATURE` or `ANIMATED_GUILD_BANNER_REQUIRES_FEATURE` 13 A non-null value requires `INVITE_SPLASH` and is rejected with the field codes `INVITE_SPLASH_REQUIRES_FEATURE` or `EMBED_SPLASH_REQUIRES_FEATURE`. Both splash fields take static images only, so GIF and APNG are not accepted for either and an animated upload is rejected with the field code `INVALID_IMAGE_FORMAT` @@ -690,11 +690,11 @@ Send the current array with the intended changes applied. A feature without the | --- | --- | --- | | 200 | [guild object](#guild-object) | Guild was modified, or the supplied values matched current state | | 4001 | [error response](/http-api/#error-response) | Body, image, feature set, channel reference, timestamp, or sudo proof is invalid | -| 4001 | [error response](/http-api/#error-response) | Caller cannot exercise `MANAGE_GUILD` | +| 4001 | [error response](/http-api/#error-response) | Caller cannot use `MANAGE_GUILD` | | 4032 | [error response](/http-api/#error-response) | Guild is unavailable, the name is blocked, `MANAGE_GUILD` or ownership is absent, or sudo mode is required | | 4043 | [error response](/http-api/#error-response) | Guild does not exist | -1 The [error code](/http-api/errors/) is `TWO_FACTOR_REQUIRED` for a caller who holds `MANAGE_GUILD` but cannot exercise it, and `INVALID_FORM_BODY` otherwise +1 The [error code](/http-api/errors/) is `TWO_FACTOR_REQUIRED` for a caller who holds `MANAGE_GUILD` but cannot use it, and `INVALID_FORM_BODY` otherwise 2 The [error code](/http-api/errors/) is `MISSING_ACCESS` for an unavailable guild, `CONTENT_BLOCKED` for a blocked name or body string, `SUDO_MODE_REQUIRED` when an MFA level change is unproven, and `MISSING_PERMISSIONS` for a non-member, a missing `MANAGE_GUILD`, or a non-owner changing the MFA level @@ -704,9 +704,9 @@ Send the current array with the intended changes applied. A feature without the Every successful request emits [Guild Update](/gateway/events/#guild-update) to every session that can see the guild, and a request that writes no field still emits it. -Only a change to an audited field records a [`GUILD_UPDATE`](/http-api/guild-audit-logs/#audit-actions) audit entry with the previous and new values, which in turn emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create) to sessions that can read the audit log. +Only a change to an audited field records a [`GUILD_UPDATE`](/http-api/guild-audit-logs/#audit-actions) audit entry with the previous and new values. That entry emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create) to sessions that can read the audit log. -Removing `TEXT_CHANNEL_FLEXIBLE_NAMES` renames every guild text channel whose stored name does not satisfy the strict naming policy and, when at least one channel is renamed, emits one [Channel Update Bulk](/gateway/events/#channel-update-bulk) Dispatch with every channel in the guild. +Removing `TEXT_CHANNEL_FLEXIBLE_NAMES` renames every guild text channel whose stored name does not satisfy the strict naming policy. When at least one channel is renamed, the removal emits one [Channel Update Bulk](/gateway/events/#channel-update-bulk) Dispatch with every channel in the guild. Replacing an image queues the previous asset for deletion after the change succeeds. A failed replacement rolls back the newly uploaded asset and leaves the previous image unchanged. @@ -722,7 +722,7 @@ Permanently deletes the guild and returns 204 with an empty body. Requires the g Fluxer refuses a non-owner with 403 `MISSING_PERMISSIONS`. A bot can never own a guild, so a bot credential never satisfies the requirement. -A guild protected by an active single community policy cannot be deleted and returns 400 `SINGLE_COMMUNITY_CANNOT_DELETE`. Fluxer decides that refusal before the ownership check and before sudo mode is verified, so a non-owner of such a guild receives the 400 rather than the 403. +A guild protected by an active single community policy cannot be deleted and returns 400 `SINGLE_COMMUNITY_CANNOT_DELETE`. That refusal comes before the ownership check and before sudo mode is verified, so a non-owner of such a guild receives the 400 rather than the 403. ### Path parameters @@ -824,7 +824,7 @@ The operation snapshots the membership's first join time, leave time, and any ac Remaining guild sessions receive [Guild Member Remove](/gateway/events/#guild-member-remove) and the leaving account's sessions receive [Guild Delete](/gateway/events/#guild-delete). -Fluxer leaves the guild's read states and per-guild settings in place. The membership row is destroyed with everything on it, so the guild nickname, guild avatar, guild banner, bio, pronouns, accent colour, and roles do not survive the leave. Rejoining within the retention window restores an unexpired communication timeout and nothing else. No audit log entry is recorded, and an `X-Audit-Log-Reason` header on this request is read and discarded. +Fluxer leaves the guild's read states and per-guild settings in place. The membership row is destroyed with everything on it, so the guild nickname, guild avatar, guild banner, bio, pronouns, accent colour, and roles do not survive the leave. Rejoining within the retention window restores an unexpired communication timeout and nothing else. The leave records no audit log entry, and an `X-Audit-Log-Reason` header on this request is read and discarded. ### Rate limit @@ -885,7 +885,7 @@ The operation deletes the caller's messages in this guild's channels in batches Returns the guild's [custom invite URL object](#custom-invite-url-object). Requires membership and [MANAGE_GUILD](/http-api/permissions/). -`MANAGE_GUILD` is an [elevated permission](/http-api/permissions/#elevated-permissions). While the guild [MFA level](#mfa-levels) is elevated, a caller other than the owner also needs an enrolled authenticator, and receives 400 `TWO_FACTOR_REQUIRED` without one. +`MANAGE_GUILD` is an [elevated permission](/http-api/permissions/#elevated-permissions). While the guild [MFA level](#mfa-levels) is elevated, a non-owner caller also needs an enrolled authenticator, and receives 400 `TWO_FACTOR_REQUIRED` without one. ### Path parameters @@ -898,7 +898,7 @@ Returns the guild's [custom invite URL object](#custom-invite-url-object). Requi | Status | Body | Condition | | --- | --- | --- | | 200 | [custom invite URL object](#custom-invite-url-object) | Custom invite URL state was returned | -| 4001 | [error response](/http-api/#error-response) | Caller cannot exercise `MANAGE_GUILD` | +| 4001 | [error response](/http-api/#error-response) | Caller cannot use `MANAGE_GUILD` | | 4032 | [error response](/http-api/#error-response) | Guild is unavailable, or `MANAGE_GUILD` is absent | | 404 | [error response](/http-api/#error-response) | Guild does not exist and the request returns `UNKNOWN_GUILD` | @@ -916,9 +916,9 @@ Returns the guild's [custom invite URL object](#custom-invite-url-object). Requi Sets or removes the guild's custom invite code and returns the [custom invite URL selection object](#custom-invite-url-selection-object). Requires membership and [MANAGE_GUILD](/http-api/permissions/). An effective change emits a [Guild Update](/gateway/events/#guild-update) Gateway event to every session that can see the guild. -`MANAGE_GUILD` is an [elevated permission](/http-api/permissions/#elevated-permissions). While the guild [MFA level](#mfa-levels) is elevated, a caller other than the owner also needs an enrolled authenticator, and receives 400 `TWO_FACTOR_REQUIRED` without one. +`MANAGE_GUILD` is an [elevated permission](/http-api/permissions/#elevated-permissions). While the guild [MFA level](#mfa-levels) is elevated, a non-owner caller also needs an enrolled authenticator, and receives 400 `TWO_FACTOR_REQUIRED` without one. -A non-null code additionally requires the `VANITY_URL` guild feature and is otherwise rejected with the field code `VANITY_URL_REQUIRES_FEATURE`. +A non-null code also requires the `VANITY_URL` guild feature and is otherwise rejected with the field code `VANITY_URL_REQUIRES_FEATURE`. ### Path parameters @@ -946,17 +946,17 @@ Selecting a code claims it in the global invite namespace, and replacing or remo | --- | --- | --- | | 200 | [custom invite URL selection object](#custom-invite-url-selection-object) | Code was claimed, removed, or already matched | | 4001 | [error response](/http-api/#error-response) | Code, reserved term policy, feature requirement, or global uniqueness check rejects the request | -| 4001 | [error response](/http-api/#error-response) | Caller cannot exercise `MANAGE_GUILD` | +| 4001 | [error response](/http-api/#error-response) | Caller cannot use `MANAGE_GUILD` | | 4032 | [error response](/http-api/#error-response) | Guild is unavailable, the code is blocked, or `MANAGE_GUILD` is absent | | 404 | [error response](/http-api/#error-response) | Guild does not exist and the request returns `UNKNOWN_GUILD` | -1 The [error code](/http-api/errors/) is `TWO_FACTOR_REQUIRED` for a caller who holds `MANAGE_GUILD` but cannot exercise it, and `INVALID_FORM_BODY` otherwise +1 The [error code](/http-api/errors/) is `TWO_FACTOR_REQUIRED` for a caller who holds `MANAGE_GUILD` but cannot use it, and `INVALID_FORM_BODY` otherwise 2 The [error code](/http-api/errors/) is `MISSING_ACCESS` for an unavailable guild, `CONTENT_BLOCKED` for a blocked code, and `MISSING_PERMISSIONS` for a non-member or a caller lacking `MANAGE_GUILD` ### Side effects -Supplying the current code, or removing a code when the guild has none, changes nothing, records no audit entry, and emits no Dispatch. Any other request deletes the invite backing the previous code. Where a code was supplied, it claims the new one. It records a [`GUILD_UPDATE`](/http-api/guild-audit-logs/#audit-actions) audit entry with the previous and new code, emits [Guild Update](/gateway/events/#guild-update) to every session that can see the guild, and emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create) to sessions that can read the audit log. +Supplying the current code, or removing a code when the guild has none, changes nothing, records no audit entry, and emits no Dispatch. Any other request deletes the invite backing the previous code. Where a code was supplied, it claims the new one. It records a [`GUILD_UPDATE`](/http-api/guild-audit-logs/#audit-actions) audit entry with the previous and new code. It also emits [Guild Update](/gateway/events/#guild-update) to every session that can see the guild, and [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create) to sessions that can read the audit log. ### Rate limit diff --git a/fluxer_docs/src/content/docs/http-api/index.md b/fluxer_docs/src/content/docs/http-api/index.md index 24a2c79e1..ec5bab763 100644 --- a/fluxer_docs/src/content/docs/http-api/index.md +++ b/fluxer_docs/src/content/docs/http-api/index.md @@ -62,7 +62,7 @@ The `attachments` array inside `payload_json` maps attachment metadata to files [Messages](/http-api/messages/) defines the attachment metadata and pre-uploaded attachment form, and [Attachment uploads](/topics/uploads/) defines the separate relay upload flow. :::caution[Multipart indices are attachment IDs] -Each direct file's `files[n]` index is also the `id` in its attachment metadata entry. Fluxer reads `n` from the field name, so part order sets no identity. The indices need not begin at zero and need not be contiguous. +Each direct file's `files[n]` index is also the `id` in its attachment metadata entry. Fluxer reads `n` from the field name, so part order sets no identity. The indices need not begin at zero and need not be consecutive. ::: ## Input normalisation @@ -111,7 +111,7 @@ These headers are accepted across resources. An operation-specific header is doc 2 The configured locale of the authenticated account takes precedence, so this header selects the locale only for an unauthenticated request or an account with no configured locale -3 The value is read verbatim with no percent-decoding, then stripped of form feed and right-to-left override characters and trimmed. A blank or over-long value is treated as absent +3 The value is read verbatim with no percent-decoding, then stripped of form feed and right-to-left override characters and trimmed. A blank or too-long value is treated as absent 4 Read only for a native Fluxer `User-Agent`, at most 4096 characters, and only the `os` member is used @@ -152,7 +152,7 @@ An `X-Audit-Log-Reason` normalised to more than 512 characters is discarded, and 2 A generated UUID unless the request supplied its own, in which case that value is echoed back unchanged and unvalidated -3 A token newly issued where the caller proved sudo mode afresh, and otherwise the incoming proof echoed back with no extension of its lifetime +3 A token newly issued where the caller proved sudo mode again, and otherwise the incoming proof echoed back with no extension of its lifetime 4 Absent from a response with no body @@ -168,7 +168,7 @@ An operation that sets its own `Cache-Control` keeps that value. A response whos ## Rate limits -Every route consumes its own rate limit bucket and is additionally 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. +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] 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`. diff --git a/fluxer_docs/src/content/docs/http-api/instance.mdx b/fluxer_docs/src/content/docs/http-api/instance.mdx index b9ec60225..5343e7ddb 100644 --- a/fluxer_docs/src/content/docs/http-api/instance.mdx +++ b/fluxer_docs/src/content/docs/http-api/instance.mdx @@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro'; The Instance resource describes how one Fluxer deployment is set up. It publishes the discovery document, the client geolocation lookup, and the served OpenAPI document, and it owns the [limit key](#limit-keys) registry. -[Get instance discovery](#get-instance-discovery) is the entry point of the API. A client that knows only a Fluxer origin reads `/.well-known/fluxer` first, and that one unauthenticated response has every field of the [instance discovery object](#instance-discovery-object). Those values include the API base URLs, the [main Gateway](/gateway/overview/) WebSocket URL, and the [Media Proxy](/media-proxy/overview/) base URL, and they override the [endpoint path defaults](#instance-endpoints-object) a deployment would otherwise serve from its canonical public origin. +[Get instance discovery](#get-instance-discovery) is the entry point of the API. A client that knows only a Fluxer origin reads `/.well-known/fluxer` first, and that one unauthenticated response has every field of the [instance discovery object](#instance-discovery-object). Those values include the API base URLs, the [main Gateway](/gateway/overview/) WebSocket URL, and the [Media Proxy](/media-proxy/overview/) base URL. They override the [endpoint path defaults](#instance-endpoints-object) a deployment would otherwise serve from its canonical public origin. 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. @@ -124,8 +124,8 @@ Which CAPTCHA provider this deployment uses, and the site keys a client needs to | Value | Description | | --- | --- | | none | The deployment requires no CAPTCHA | -| hcaptcha | hCaptcha challenges gate the operations that demand one | -| turnstile | Cloudflare Turnstile challenges gate the operations that demand one | +| hcaptcha | hCaptcha challenges gate the operations that require one | +| turnstile | Cloudflare Turnstile challenges gate the operations that require one | The complete challenge handshake, including the `X-Captcha-Token` and `X-Captcha-Type` request headers, is defined in [CAPTCHA handling](/topics/captcha/). @@ -174,7 +174,7 @@ Who can register an account on this deployment. | mode | string | The [registration mode](#registration-modes) | | admin_registration_urls_enabled1 | boolean | Whether an administrator-issued registration URL code is accepted | -1 When true, [Register an account](/http-api/authentication/#register-an-account) accepts `registration_url_code`, a valid code admits registration even while `mode` is `closed`, and the code's own approval requirement replaces the one the mode would impose +1 When true, [Register an account](/http-api/authentication/#register-an-account) accepts `registration_url_code`, a valid code admits registration even while `mode` is `closed`, and the code's own approval requirement replaces the one the mode would set ### Registration modes @@ -501,7 +501,7 @@ A resolved result is cached for ten minutes against the address. An IPv4 address | Status | Body | Condition | | --- | --- | --- | -| 200 | [geolocation](#geolocation-object) object | The location was resolved, wholly, partly, or not at all | +| 200 | [geolocation](#geolocation-object) object | The location was resolved, fully, partly, or not at all | ### Rate limit diff --git a/fluxer_docs/src/content/docs/http-api/invites.mdx b/fluxer_docs/src/content/docs/http-api/invites.mdx index 5a1fac054..d453c8e7f 100644 --- a/fluxer_docs/src/content/docs/http-api/invites.mdx +++ b/fluxer_docs/src/content/docs/http-api/invites.mdx @@ -34,7 +34,7 @@ An invite object describes one code and the target it admits into. The `type` se 3 Null when the record stores no creator, which is always the case for a custom invite URL -4 A guild invite reports the counts the main Gateway currently holds, and both are `0` when it holds none +4 A guild invite reports the counts the main Gateway holds, and both are `0` when it holds none 5 Computed from the creation time plus the stored lifetime, and null whenever that lifetime is `0` @@ -80,11 +80,11 @@ The metadata object extends the invite with its creation and usage state. Creati | max_uses2 | integer | The maximum admissions the invite permits, where `0` is unlimited | | max_age?3 | integer | The stored lifetime in seconds, where `0` never expires | -1 The counter advances only when the admission actually adds the account, so accepting an invite for a target the account already belongs to leaves it unchanged +1 The counter advances only when the admission adds the account, so accepting an invite for a target the account already belongs to leaves it unchanged 2 The value is `0` for every custom invite URL and for any invite created without an explicit maximum -3 Present only when `type` is `0`, because a group direct message invite reports its expiry solely through `expires_at` +3 Present only when `type` is `0`, because a group direct message invite reports its expiry only through `expires_at` ### Example @@ -111,7 +111,7 @@ The partial channel has only enough identity to describe the target before admis | id | snowflake | The ID of the channel | | name1 | ?string | The name of the channel, or null | | type | integer | [Channel type](/http-api/channels/#channel-types) | -| recipients?2 | array[[partial invite recipient](#partial-invite-recipient-object) object] | The accounts currently in the group direct message | +| recipients?2 | array[[partial invite recipient](#partial-invite-recipient-object) object] | The accounts in the group direct message | 1 A guild channel always stores a name, and a group direct message that stores no name reports null @@ -137,7 +137,7 @@ A partial recipient has only the username. ## Custom invite URLs -[Modify guild custom invite URL](/http-api/guilds/#modify-guild-custom-invite-url) creates and replaces a custom invite code and stores it as a guild invite record of type `0`. It has no fixed target channel, creator, maximum uses, or lifetime, so a resolved custom invite URL reports a null `inviter`, a null `expires_at`, `max_uses` of `0`, and `max_age` of `0`. Its channel is the first guild text channel in channel order that the default role can view. +[Modify guild custom invite URL](/http-api/guilds/#modify-guild-custom-invite-url) creates and replaces a custom invite code and stores it as a guild invite record of type `0`. It stores no target channel, creator, maximum uses, or lifetime, so a resolved custom invite URL reports a null `inviter`, a null `expires_at`, `max_uses` of `0`, and `max_age` of `0`. Its channel is the first guild text channel in channel order that the default role can view. A custom invite code is stored lowercased, so [Get invite](#get-invite) and [Accept invite](#accept-invite) resolve it in any case through their lowercase retry. Clearing or replacing the code deletes the invite record outright, and a released code stops resolving immediately. @@ -180,7 +180,7 @@ A record's storage lifetime equals `max_age`. An exhausted record survives until ### Rate limit -100 requests per 10 seconds for each identity and invite code, on the `invite:read::invite_code` bucket, where the identity is the authenticated account when a credential resolves and otherwise the derived key for the client IP address. +100 requests per 10 seconds for each identity and invite code, on the `invite:read::invite_code` bucket. The identity is the authenticated account when a credential resolves, and otherwise the derived key for the client IP address. ## Accept invite @@ -229,7 +229,7 @@ An acceptance attempt against an invite whose maximum uses are already spent del An account already in the target receives the invite unchanged without consuming a use or emitting a Dispatch. Otherwise `uses` advances exactly once after admission completes, and the record is deleted when that advance spends its last use. A failed admission consumes no use. Fluxer builds the response body after the admission has committed, through the same target resolution [Get invite](#get-invite) performs. A guild invite whose stored target channel has since been deleted therefore admits the account and advances `uses` before the request returns 404 `UNKNOWN_CHANNEL`. -A guild admission creates the membership, records whether the join used a custom invite URL or an instant invite, adds the guild to the caller's settings and folder layout, and marks the membership temporary when the invite is temporary. +A guild admission creates the membership, records whether the join used a custom invite URL or an instant invite, and adds the guild to the caller's settings and folder layout. A temporary invite marks the membership temporary. The joining account receives [Guild Create](/gateway/events/#guild-create). [Guild Member Add](/gateway/events/#guild-member-add) is guild-wide and reaches every session connected to the guild, subject to the [event filtering](/gateway/event-filtering/) gates, which suppress it for a passive session in a large guild. The joining account receives [User Settings Update](/gateway/events/#user-settings-update) when the join changes its stored settings, and [User Guild Settings Update](/gateway/events/#user-guild-settings-update) when its `default_hide_muted_channels` setting is enabled. Unless join notifications are suppressed or no system channel exists, the operation creates a join system message and delivers [Message Create](/gateway/events/#message-create). @@ -275,7 +275,7 @@ The invite code can be allocated again later, and every outstanding link with th | --- | --- | --- | | 204 | empty | Invite was deleted | | 400 | [error response](/http-api/#error-response) | The caller is not the creator and the guild requires elevated multi-factor authentication that the caller lacks, and the request returns `TWO_FACTOR_REQUIRED` | -| 403 | [error response](/http-api/#error-response) | Caller presents a bearer credential and the request returns `ACCESS_DENIED`, or the caller is not a member of the guild, is neither the creator nor a holder of [MANAGE_GUILD](/http-api/permissions/), or does not own the group direct message, each returning `MISSING_PERMISSIONS` | +| 403 | [error response](/http-api/#error-response) | Caller presents a bearer credential and the request returns `ACCESS_DENIED`, or the caller is not a guild member, is neither the creator nor a [MANAGE_GUILD](/http-api/permissions/) holder, or does not own the group direct message, each returning `MISSING_PERMISSIONS` | | 404 | [error response](/http-api/#error-response) | The named guild no longer exists and the request returns `UNKNOWN_GUILD` | | 404 | [error response](/http-api/#error-response) | No record has the code, the record is the guild's current custom invite URL, or a guild invite names no target, each returning `UNKNOWN_INVITE` | | 404 | [error response](/http-api/#error-response) | The group direct message no longer exists or the caller is not one of its recipients and the request returns `UNKNOWN_CHANNEL` | @@ -342,7 +342,7 @@ Every field is optional and nullable, and an omitted or null value takes the sta | 403 | [error response](/http-api/#error-response) | Instant invites are temporarily disabled for the guild and the request returns `FEATURE_TEMPORARILY_DISABLED` | | 403 | [error response](/http-api/#error-response) | The caller is not a guild member or lacks [VIEW_CHANNEL](/http-api/permissions/) or [CREATE_INSTANT_INVITE](/http-api/permissions/) and the request returns `MISSING_PERMISSIONS` | | 403 | [error response](/http-api/#error-response) | The channel is age restricted and the account is not age verified and the request returns `NSFW_CONTENT_AGE_RESTRICTED` | -| 404 | [error response](/http-api/#error-response) | Channel does not exist, is pending deletion, or the caller is not a recipient of the private channel, each returning `UNKNOWN_CHANNEL`, or the guild the channel belongs to no longer exists and the request returns `UNKNOWN_GUILD` | +| 404 | [error response](/http-api/#error-response) | Channel does not exist, is pending deletion, or the caller is not a recipient of the private channel, each returning `UNKNOWN_CHANNEL`, or the channel's guild no longer exists and the request returns `UNKNOWN_GUILD` | ### Side effects @@ -390,7 +390,7 @@ The operation is not paginated. Every matching record is returned in one respons | 403 | [error response](/http-api/#error-response) | The guild is unavailable to the caller and the request returns `MISSING_ACCESS` | | 403 | [error response](/http-api/#error-response) | The caller is not a guild member, lacks [VIEW_CHANNEL](/http-api/permissions/) or [MANAGE_CHANNELS](/http-api/permissions/), or does not own the group direct message and the request returns `MISSING_PERMISSIONS` | | 403 | [error response](/http-api/#error-response) | The channel is age restricted and the account is not age verified and the request returns `NSFW_CONTENT_AGE_RESTRICTED` | -| 404 | [error response](/http-api/#error-response) | Channel does not exist, is pending deletion, the caller is not a recipient of the private channel, or the private channel is not a group direct message, each returning `UNKNOWN_CHANNEL`, or the guild the channel belongs to no longer exists and the request returns `UNKNOWN_GUILD` | +| 404 | [error response](/http-api/#error-response) | Channel does not exist, is pending deletion, the caller is not a recipient of the private channel, or the private channel is not a group direct message, each returning `UNKNOWN_CHANNEL`, or the channel's guild no longer exists and the request returns `UNKNOWN_GUILD` | ### Rate limit diff --git a/fluxer_docs/src/content/docs/http-api/memes.mdx b/fluxer_docs/src/content/docs/http-api/memes.mdx index 40aae898b..3b75ff9c1 100644 --- a/fluxer_docs/src/content/docs/http-api/memes.mdx +++ b/fluxer_docs/src/content/docs/http-api/memes.mdx @@ -40,9 +40,9 @@ One media asset owned by one account. Fluxer resolves every media field at creat | content_type | string | The MIME type of the stored copy | | content_hash | ?string | The content hash used for [deduplication](#deduplication), or null when it is unavailable | | size | integer | The stored size of the copy in bytes | -| width | ?integer | The width of the media in pixels, or null when inapplicable or unknown | -| height | ?integer | The height of the media in pixels, or null when inapplicable or unknown | -| duration | ?number | The duration of the media in seconds, or null when inapplicable or unknown | +| width | ?integer | The width of the media in pixels, or null when not applicable or unknown | +| height | ?integer | The height of the media in pixels, or null when not applicable or unknown | +| duration | ?number | The duration of the media in seconds, or null when not applicable or unknown | | url3 | string | The Fluxer attachment URL of the stored copy | | is_gifv4 | boolean | Whether the stored media is animated or a video converted from a GIF | | gif_slug5 | ?string | The provider-issued slug this meme was sourced from, or null | @@ -60,7 +60,7 @@ One media asset owned by one account. Fluxer resolves every media field at creat 5 Both fields are null together. `klipy` is the only value a new meme records, and `tenor` appears on legacy records alone -6 Keyed by [GIF media format name](#gif-media-format-names). Persisted only by [Save meme from URL](#save-meme-from-url), and null on every message-sourced meme +6 Keyed by [GIF media format name](#gif-media-format-names). Stored only by [Save meme from URL](#save-meme-from-url), and null on every message-sourced meme A message-sourced meme records a slug only when the selected embed is a `gifv` embed whose URL the configured provider recognises. An attachment selection never records one. @@ -201,9 +201,9 @@ Fetches an absolute media URL, stores a durable Fluxer copy, and returns the cre 3 Honoured together only when `gif_provider` names the configured provider. Otherwise Fluxer extracts a slug from `url` with the configured provider -4 Persisted only when the request resolved to a provider GIF and the map is non-empty, and discarded otherwise +4 Stored only when the request resolved to a provider GIF and the map is non-empty, and discarded otherwise -The stored filename is the last path segment of `url` after percent-decoding. Fluxer appends an extension inferred from the resolved content type when that segment has no dot, then normalises the result so whitespace becomes `_` and every character outside letters, digits, combining marks, `_`, `.`, and `-` is dropped. A URL path with no last segment yields `media.{extension}`. Where the segment normalises to nothing, or to only dots and underscores, the filename is `unnamed`. The extension is `bin` when the content type maps to none. +The stored filename is the last path segment of `url` after percent-decoding. Fluxer appends an extension inferred from the resolved content type when that segment has no dot. It then normalises the result so whitespace becomes `_` and every character outside letters, digits, combining marks, `_`, `.`, and `-` is dropped. A URL path with no last segment yields `media.{extension}`. Where the segment normalises to nothing, or to only dots and underscores, the filename is `unnamed`. The extension is `bin` when the content type maps to none. Fluxer fetches the asset and resolves its metadata before it stores anything. A URL that resolves to no usable media fails with 400 `MEDIA_METADATA_ERROR`. When the request resolves to a provider GIF, the provider's canonical share URL is unfurled and the video content hash it reports replaces the fetched hash for [deduplication](#deduplication). @@ -263,7 +263,7 @@ A guild caller without [READ_MESSAGE_HISTORY](/http-api/permissions/) can select When neither key selects anything, Fluxer takes the message's first attachment. It takes the first usable embed when the message has no attachment, or when that attachment has no supported media type. An embed selection has no alt text of its own. -Only an `image/*`, `video/*`, or `audio/*` asset can be saved. `attachment_id` is matched only when the message or one of its snapshots has at least one attachment, and an ID matching none of them fails with `ATTACHMENT_ID_NOT_FOUND_IN_MESSAGE` at the `attachment_id` path. When the message has no attachment at all, Fluxer ignores the ID and takes the first usable embed. Only the selected attachment is tried, so a message whose chosen attachment has an unsupported type falls through to its embeds. A selection that resolves to no supported media fails with `NO_VALID_MEDIA_IN_MESSAGE` at the `media` path. +Only an `image/*`, `video/*`, or `audio/*` asset can be saved. `attachment_id` is matched only when the message or one of its snapshots has at least one attachment. An ID matching none of them fails with `ATTACHMENT_ID_NOT_FOUND_IN_MESSAGE` at the `attachment_id` path. When the message has no attachment at all, Fluxer ignores the ID and takes the first usable embed. Only the selected attachment is tried, so a message whose chosen attachment has an unsupported type falls through to its embeds. A selection that resolves to no supported media fails with `NO_VALID_MEDIA_IN_MESSAGE` at the `media` path. An embed selection uses the embed's image, video, or thumbnail, in that order. When the URL is one the instance's own media endpoint serves, Fluxer copies the asset without a fetch. Any other URL is fetched over the network, and a fetch that resolves no metadata fails with 400 `MEDIA_METADATA_ERROR`. @@ -423,7 +423,7 @@ A client uses it to turn stored URL-only favourite GIFs into renderable picker e | --- | --- | --- | | Accept-Language?1 | string | The preferred locale used for a GIF provider lookup | -1 Resolved against the [supported locale registry](/topics/locales/#supported-locales), superseded by the account's own stored locale, and defaulting to `en-US` +1 Resolved against the [supported locale registry](/topics/locales/#supported-locales), replaced by the account's own stored locale, and defaulting to `en-US` Fluxer derives a two-letter country from the requesting address by geolocation, with `US` as the fallback. @@ -439,7 +439,7 @@ Fluxer derives a two-letter country from the requesting address by geolocation, | --- | --- | --- | | entries1 | array[[resolved GIF entry](#resolved-gif-entry-object) object] | One entry for each submitted URL | -1 Exactly one entry per submitted URL, in the submitted order, so a client may index the result positionally against its request +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. diff --git a/fluxer_docs/src/content/docs/http-api/messages.mdx b/fluxer_docs/src/content/docs/http-api/messages.mdx index 3e9b88f8f..fa8e671d5 100644 --- a/fluxer_docs/src/content/docs/http-api/messages.mdx +++ b/fluxer_docs/src/content/docs/http-api/messages.mdx @@ -8,15 +8,15 @@ import RouteHeader from '@/components/RouteHeader.astro'; A message is one post in a channel, together with the text, attachments, embeds, stickers, and reactions stored with it. Channel metadata and membership are defined by the [Channels resource](/http-api/channels/). -A text-bearing channel is a [channel type](/http-api/channels/#channel-types) that stores messages, which is every type except a guild category and a guild link channel. The message history cutoff is an instant a guild stores on itself, and a member without [READ_MESSAGE_HISTORY](/http-api/permissions/) reads nothing created before it. +A text-bearing channel is a [channel type](/http-api/channels/#channel-types) that stores messages, which is every type except a guild category and a guild link channel. The message history cutoff is a time stored on the guild, and a member without [READ_MESSAGE_HISTORY](/http-api/permissions/) reads nothing created before it. ## Channel resolution -Fluxer resolves the channel before it applies any per-route authorisation. A private channel returns 404 `UNKNOWN_CHANNEL` to a caller who is not a recipient. A guild channel returns 403 `MISSING_PERMISSIONS` to a non-member and to a member without [VIEW_CHANNEL](/http-api/permissions/). A guild an operator has marked unavailable returns 403 `MISSING_ACCESS` before the route runs, and a guild that no Gateway process serves returns 404 `UNKNOWN_GUILD`, or 403 `ACCESS_DENIED` when its stored record survives. +Fluxer resolves the channel before it applies any per-route authorisation. A private channel returns 404 `UNKNOWN_CHANNEL` to a caller who is not a recipient. A guild channel returns 403 `MISSING_PERMISSIONS` to a non-member and to a member without [VIEW_CHANNEL](/http-api/permissions/). A guild an operator has marked unavailable returns 403 `MISSING_ACCESS` before the route runs. A guild that no Gateway process serves returns 404 `UNKNOWN_GUILD`, or 403 `ACCESS_DENIED` when its stored record survives. The message read, create, modify, and delete routes, [Bulk delete messages](#bulk-delete-messages), [Bulk delete own messages](#bulk-delete-own-messages), and [List pinned messages](#list-pinned-messages) also enforce age verification on an effectively age restricted guild channel. They return 403 `NSFW_CONTENT_AGE_RESTRICTED` until the account satisfies it. The attachment routes, the pin routes, the reaction routes, and [Indicate typing](#indicate-typing) resolve the channel without that check. -[Clear channel read state](#clear-channel-read-state) and [Acknowledge message](#acknowledge-message) resolve no channel. [Acknowledge pins](#acknowledge-pins) reads the channel without checking the caller's access. None of the three evaluates a permission. +[Clear channel read state](#clear-channel-read-state) and [Acknowledge message](#acknowledge-message) resolve no channel. [Acknowledge pins](#acknowledge-pins) reads the channel without checking the caller's access. None of the three checks a permission. ## Message object @@ -37,7 +37,7 @@ A message object is the full stored form of one post in a channel. | edited_timestamp | ?ISO8601 timestamp | Most recent edit time, or null when the message has never been edited | | pinned | boolean | Whether the message is pinned | | mention_everyone | boolean | Whether the message mentions everyone | -| tts2 | boolean | Whether the message requested text-to-speech presentation | +| tts2 | boolean | Whether the message requested text-to-speech | | mentions3 | array[[partial user](/http-api/users/#partial-user-object) object] | The users the message actively mentions | | mention_roles | array[snowflake] | The IDs of the roles the message actively mentions | | mention_channels? | array[[channel mention](#channel-mention-object) object] | The channels the message content links by ID | @@ -46,14 +46,14 @@ A message object is the full stored form of one post in a channel. | attachments5 | array[[message attachment](#message-attachment-object) object] | The files attached to the message | | stickers | array[[sticker item](#sticker-item-object) object] | The stickers sent with the message | | nsfw_emojis? | array[snowflake] | IDs of the custom emojis in the message that are classified as explicit | -| reactions? | array[[reaction](#reaction-object) object] | Ordinary reaction summaries | +| reactions? | array[[reaction](#reaction-object) object] | Reaction summaries | | message_reference? | [message reference](#message-reference-object) object | Reply or forward reference | | message_snapshots? | array[[message snapshot](#message-snapshot-object) object] | The immutable copies captured for a forward | | nonce?6 | string | Caller-supplied message nonce, echoed to the sender as a string of 1 through 32 characters | | call? | [message call](#message-call-object) object | Call state attached to a call message | | referenced_message?7 | ?[message](#message-object) object | Resolved referenced message without a nested `referenced_message` field | -1 Synthesised for a webhook-authored message and for a stored author ID that no longer resolves +1 Built for a webhook-authored message and for a stored author ID that no longer resolves 2 Echoed from the create request, so a message read through any other operation always reports false @@ -69,10 +69,10 @@ A message object is the full stored form of one post in a channel. A webhook-authored message has no stored author user. Fluxer builds its author from the webhook, with the webhook ID as `id`, the stored webhook name as `username`, the discriminator `0000`, the stored webhook avatar hash as `avatar`, and `bot` true. A message whose stored author ID no longer resolves to an account is served with a deleted-user placeholder. The placeholder has that ID, the discriminator `0000`, and no avatar. -`mentions`, `mention_roles`, `embeds`, `attachments`, and `stickers` are always present and can be empty. Every optional field above is omitted entirely, with `referenced_message` as the one exception. A message whose author user and webhook are both absent is omitted from every read. +`mentions`, `mention_roles`, `embeds`, `attachments`, and `stickers` are always present and can be empty. Every optional field above is omitted entirely, and `referenced_message` is the one that can instead be present and null. A message whose author user and webhook are both absent is omitted from every read. :::note[A forward has immutable copies in `message_snapshots`] -A reply reference resolves into `referenced_message`, and a forward has none. Fluxer captures the snapshots at forward time, and they never follow later edits to the source. [Message reference types](#message-reference-types) is the discriminator. +A reply reference resolves into `referenced_message`, and a forward has none. Fluxer captures the snapshots at forward time, and they never follow later edits to the source. [Message reference types](#message-reference-types) tells the two apart. ::: ### Example @@ -230,7 +230,7 @@ A reaction object summarises one emoji group on a message. Fluxer has ordinary r 2 Present and `true` only when the authenticated user has this reaction, and omitted entirely otherwise, so an absent key must be read as false -A message has at most the resolved `max_reactions_per_message` distinct groups, which defaults to 30. Groups are ordered by the time each group's earliest reaction was added, then by emoji name, then by emoji ID. +Each message has at most the resolved `max_reactions_per_message` distinct groups, which defaults to 30. Groups are ordered by the time each group's earliest reaction was added, then by emoji name, then by emoji ID. ### Example @@ -291,7 +291,7 @@ A message reference input names the source message a create request replies to o ## Message snapshot object -A snapshot is the flat immutable copy of one forwarded message. It has no message ID, channel ID, or author, so a client cannot trace a forward back to its source through this object. +Each snapshot is a flat immutable copy of one forwarded message. It has no message ID, channel ID, or author, so a client cannot trace a forward back to its source through this object. ### Structure @@ -309,7 +309,7 @@ A snapshot is the flat immutable copy of one forwarded message. It has no messag | type | integer | Original [message type](#message-types) | | flags | integer | Original [message flags](#message-flags) | -1 A client resolves each snapshotted ID against the enclosing message's `users` array +1 A client resolves each snapshotted ID against the parent message's `users` array 2 Snapshot attachments are independent stored objects, so deleting the source message leaves them intact @@ -340,7 +340,7 @@ A channel mention names one channel that the message content links by ID. ## Sticker item object -A sticker item is the trimmed sticker record on a message. +Each sticker item is the trimmed sticker record on a message. ### Structure @@ -401,7 +401,7 @@ Every optional field above is omitted entirely. | Value | Description | | --- | --- | -| rich | An embed the unfurler assembled itself, and the type of every accepted rich embed input | +| rich | An embed the unfurler built itself, and the type of every accepted rich embed input | | link | A generic link preview with page metadata and no resolved media | | image | An image preview resolved from a linked URL | | video | A video preview resolved from a linked URL | @@ -462,7 +462,7 @@ Every optional field above is omitted entirely. ## Embed provider object -Fluxer populates a provider only from resolved embed metadata. +Fluxer sets a provider only from resolved embed metadata. ### Structure @@ -473,7 +473,7 @@ Fluxer populates a provider only from resolved embed metadata. ## Allowed mentions object -An allowed mentions object decides which mentions written in the message text become active mentions, meaning the ones that notify a user or role. Omitting the object lets every mention in the text become active, subject to the caller's permissions. +An allowed mentions object selects which mentions written in the message text become active mentions, meaning the ones that notify a user or role. ### Structure @@ -527,7 +527,7 @@ A new message can refer to a previously uploaded attachment or attach a direct m 1 Accepted either as a JSON number or as a decimal string -2 The presence of this key is the discriminator that selects this variant over the [direct multipart metadata](#direct-multipart-attachment-metadata-object) variant +2 The presence of this key selects this variant over the [direct multipart metadata](#direct-multipart-attachment-metadata-object) variant 3 Both are required when the message has the `VOICE_MESSAGE` flag. A `waveform` on an attachment whose resolved media type is not `audio/*` fails with the field code `VOICE_MESSAGES_ATTACHMENT_MUST_BE_AUDIO` @@ -574,7 +574,7 @@ Fluxer discards a key outside the table above and accepts the rest of the entry. ## Rich embed input objects -A rich embed input is an embed a client supplies on a message, and Fluxer always stores it with the `rich` [embed type](#embed-types). The number of embeds on one message is bounded by the resolved `max_embeds_per_message` limit, which defaults to 10, and exceeding it fails with the field code `TOO_MANY_EMBEDS`. +A rich embed input is an embed a client supplies on a message, and Fluxer always stores it with the `rich` [embed type](#embed-types). The number of embeds on one message is bounded by the resolved `max_embeds_per_message` limit, which defaults to 10. Exceeding it fails with the field code `TOO_MANY_EMBEDS`. ### Rich embed object @@ -639,11 +639,11 @@ An `attachment://` reference on a request that has no attachment fails with the ## Attachment upload objects -Fluxer issues an upload URL for an attachment before any message can reference it. [Request attachment upload URLs](#request-attachment-upload-urls) plans the upload. A plan whose declared size exceeds 10 MiB is a multipart plan, and [Complete attachment upload](#complete-attachment-upload) finalises it. +Fluxer issues an upload URL for an attachment before any message can reference it. [Request attachment upload URLs](#request-attachment-upload-urls) plans the upload. A plan whose declared size exceeds 10 MiB is a multipart plan, and [Complete attachment upload](#complete-attachment-upload) finishes it. -An `upload_url` is either a presigned object storage URL or a Fluxer [upload relay](/media-proxy/upload-relay/) URL, and the instance decides which per request from the caller's country. A caller whose IP is unknown, or whose country is not on the instance's direct-upload list, is handed a relay URL. A client MUST send the URL exactly as issued and MUST NOT assume either shape. +An `upload_url` is either a presigned object storage URL or a Fluxer [upload relay](/media-proxy/upload-relay/) URL, and the instance picks which per request from the caller's country. A caller whose IP is unknown, or whose country is not on the instance's direct-upload list, is given a relay URL. A client MUST send the URL exactly as issued and MUST NOT assume either shape. -Only a message that the same identity creates in the same channel can reference the key an upload plan yields. +An upload key can be referenced only by a message that the same identity creates in the same channel. ### Attachment upload request item object @@ -658,7 +658,7 @@ One declared upload inside a [Request attachment upload URLs](#request-attachmen | file_size1 | integer | Declared byte count (0-9,007,199,254,740,991) | | content_type2 | string | Media type of 1 through 255 characters the client uploads | -1 A decimal string is coerced to the integer +1 A decimal string is converted to the integer 2 The issued capability and the stored attachment both use the media type derived from `filename` @@ -814,7 +814,7 @@ Returns a reverse-chronological array of [message](#message-object) objects. Req 1 Combining the two selects the page before `before`, then discards entries at or below `after` :::caution[A page can hold fewer than `limit` items] -Fluxer selects the page first, then removes messages outside the caller's message history cutoff and messages with neither a stored author nor a stored webhook. A page can therefore hold fewer than `limit` items. +Fluxer selects the page first, then removes messages outside the caller's message history cutoff and messages with neither a stored author nor a stored webhook. ::: :::note[Missing history is an empty page] @@ -890,7 +890,7 @@ Returns one [message](#message-object) object. Requires [VIEW_CHANNEL](/http-api | Field | Type | Description | | --- | --- | --- | | channel_id | snowflake | The ID of the channel | -| message_id | snowflake | The ID of the message | +| message_id | snowflake | The ID of the message to return | ### Response @@ -915,7 +915,7 @@ Plans from 1 through 10 attachment uploads. Returns a [singlepart](#singlepart-a - The channel must support messages, and one that does not fails with 400 `CANNOT_SEND_MESSAGES_IN_NON_TEXT_CHANNEL`. - A guild channel requires [SEND_MESSAGES](/http-api/permissions/) and [ATTACH_FILES](/http-api/permissions/), and a timed-out member is refused with 403 `COMMUNICATION_DISABLED`. -Fluxer checks each declared `file_size` separately against the attachment size limit it resolves for the caller and the guild context. A declaration above that limit returns 400 `FILE_SIZE_TOO_LARGE` with the resolved ceiling. The resolved ceiling defaults to 26214400 bytes, the 25 MiB non-premium allowance, and 524288000 bytes, the 500 MiB premium allowance. A bot credential is additionally clamped to 52428800 bytes, the 50 MiB bot ceiling, even when the resolved limit is higher. +Fluxer checks each declared `file_size` separately against the attachment size limit it resolves for the caller and the guild context. A declaration above that limit returns 400 `FILE_SIZE_TOO_LARGE` with the resolved ceiling. The resolved ceiling defaults to 26214400 bytes, the 25 MiB non-premium allowance, and 524288000 bytes, the 500 MiB premium allowance. A bot credential is clamped to 52428800 bytes, the 50 MiB bot ceiling, even when the resolved limit is higher. ### Path parameters @@ -964,7 +964,7 @@ An issued `upload_filename` becomes an attachment only when a later [Create mess -Finalises from 1 through 10 multipart uploads after every part has been sent. Returns the finalised upload keys. +Completes from 1 through 10 multipart uploads after every part has been sent. Returns their upload keys. ### Limitations @@ -988,13 +988,13 @@ The request has no part list and no entity tags. | Field | Type | Description | | --- | --- | --- | -| uploads | array[object] | One entry for each finalised upload, each with `upload_filename` | +| uploads | array[object] | One entry for each completed upload, each with `upload_filename` | ### Response | Status | Body | Condition | | --- | --- | --- | -| 200 | response body | Every multipart upload was finalised | +| 200 | response body | Every multipart upload was completed | | 400 | [error response](/http-api/#error-response) | Body is invalid, or an upload is not a pending multipart upload of this caller in this channel and the request returns `INVALID_FORM_BODY` with the code `UPLOADED_ATTACHMENT_NOT_FOUND` on the path `uploads.{index}.upload_filename` | | 400 | [error response](/http-api/#error-response) | No part was uploaded and the request returns `INVALID_FORM_BODY` with the code `NO_UPLOADED_PARTS_TO_FINALIZE` on the path `parts` | | 400 | [error response](/http-api/#error-response) | The uploaded bytes exceed the resolved limit and the request returns `FILE_SIZE_TOO_LARGE` | @@ -1013,7 +1013,7 @@ Fluxer reports the field code `UPLOADED_ATTACHMENT_NOT_FOUND` for an upload key ### Side effects -The operation finalises each multipart upload and records its completion time and completion IP. It creates no message, and the upload key stays available until a message consumes it. +The operation finishes each multipart upload and records its completion time and completion IP. It creates no message, and the upload key stays available until a message consumes it. ### Rate limit @@ -1032,7 +1032,7 @@ Creates a message from a JSON body or from multipart form data. Returns the crea - A timed-out member is refused with 403 `COMMUNICATION_DISABLED`. - Embeds require [EMBED_LINKS](/http-api/permissions/), attachments require [ATTACH_FILES](/http-api/permissions/), a favourite meme requires both, and active everyone mentions require [MENTION_EVERYONE](/http-api/permissions/). - Slowmode applies to a non-bot caller unless they hold [BYPASS_SLOWMODE](/http-api/permissions/). -- A one-to-one direct message additionally applies the recipient's direct message policy and relationship state, and a denial returns 400 `CANNOT_SEND_MESSAGES_TO_USER`. +- A one-to-one direct message also applies the recipient's direct message policy and relationship state, and a denial returns 400 `CANNOT_SEND_MESSAGES_TO_USER`. - An unclaimed account can send only to its own personal notes channel, and a send to any other channel is refused with 400 `UNCLAIMED_ACCOUNT_CANNOT_SEND_MESSAGES` before the channel is resolved. - A non-bot caller must have started a session, and one that has not is refused with the field code `MUST_START_SESSION_BEFORE_SENDING`. @@ -1057,11 +1057,11 @@ The request body is read as a multipart form when `Content-Type` contains `multi | flags?3 | integer | [Message flags](#message-flags), defaulting to `0` | | favorite_meme_id?4 | ?snowflake | Favorite meme to attach | | sticker_ids? | ?array[snowflake] | At most 3 sticker IDs | -| tts?5 | boolean | Whether to request text-to-speech presentation | +| tts?5 | boolean | Whether to request text-to-speech | 1 Echoed back on the created message and on its [Message Create](/gateway/events/#message-create) Dispatch so a client can match the result to its optimistic entry. An integer is converted to its decimal string form -2 The schema imposes no length bound. Exceeding the effective one returns 400 `INVALID_FORM_BODY` with `CONTENT_EXCEEDS_MAX_LENGTH` on the path `content` +2 The schema sets no length bound. Exceeding the effective one returns 400 `INVALID_FORM_BODY` with `CONTENT_EXCEEDS_MAX_LENGTH` on the path `content` 3 Fluxer keeps only the bits in [message flags](#message-flags) and silently drops every other bit @@ -1083,7 +1083,7 @@ The smallest body that works is one line of text: Fluxer resolves the effective `max_message_length` for the author and guild context, and it defaults to 2,000 characters. A bot or webhook author takes whichever is larger, the resolved value or 4,000 characters. -A message must have at least one of content with visible characters, an embed, an attachment, a favourite meme, or a sticker, and an otherwise empty request fails with 400 `CANNOT_SEND_EMPTY_MESSAGE`. `SUPPRESS_NOTIFICATIONS` changes notification generation, and the message still has its mention data. +A message must have at least one of content with visible characters, an embed, an attachment, a favourite meme, or a sticker. An otherwise empty request fails with 400 `CANNOT_SEND_EMPTY_MESSAGE`. `SUPPRESS_NOTIFICATIONS` changes notification generation, and the message still has its mention data. :::caution[A reused nonce replays the first result] A nonce is remembered for the authenticated identity for five minutes, so a client that retries a send after a timeout MUST reuse the same nonce to avoid a duplicate. @@ -1093,7 +1093,7 @@ Reusing a nonce in the same channel inside that window returns the message the f A reply applies the caller's message history cutoff to its target, and a forward applies none to its source. A reply whose target is a system message fails with the field code `CANNOT_REPLY_TO_SYSTEM_MESSAGE`, and a reference that resolves to no message fails with 404 `UNKNOWN_MESSAGE`. -A `FORWARD` reference has `channel_id`. One that does not is refused by body validation before the operation runs, with 400 `INVALID_MESSAGE_DATA` on a JSON body and a per-field 400 `INVALID_FORM_BODY` on a multipart body. +Every `FORWARD` reference has `channel_id`. One that does not is refused by body validation before the operation runs, with 400 `INVALID_MESSAGE_DATA` on a JSON body and a per-field 400 `INVALID_FORM_BODY` on a multipart body. A forward request has no `content`, `embeds`, `attachments`, or `sticker_ids`, and one that does fails with the field code `FORWARD_MESSAGES_CANNOT_CONTAIN_CONTENT`. A `guild_id` that disagrees with the source channel's guild fails with the field code `GUILD_ID_MUST_MATCH_REFERENCED_MESSAGE`. Reading the source channel requires [VIEW_CHANNEL](/http-api/permissions/) when that channel belongs to a guild. @@ -1116,7 +1116,7 @@ Snapshots with embeds require `EMBED_LINKS` in the destination guild channel, an 2 The index runs from 0 through the resolved `max_attachments_per_message` limit minus one -A body that cannot be parsed as a multipart form fails with the field code `FAILED_TO_PARSE_MULTIPART_FORM_DATA`, and a `payload_json` field that is not a JSON string fails with the field code `INVALID_JSON_IN_PAYLOAD_JSON`. A field name beginning with `files[` that does not match `files[]` fails with the field code `INVALID_FILE_FIELD_NAME`. +A body that cannot be parsed as a multipart form fails with the field code `FAILED_TO_PARSE_MULTIPART_FORM_DATA`. A `payload_json` field that is not a JSON string fails with the field code `INVALID_JSON_IN_PAYLOAD_JSON`, and a field name beginning with `files[` that does not match `files[]` fails with the field code `INVALID_FILE_FIELD_NAME`. | File field condition | Error | | --- | --- | @@ -1153,7 +1153,9 @@ The resolved `max_voice_message_duration` limit defaults to 1200 seconds. | Status | Body | Condition | | --- | --- | --- | | 200 | [message](#message-object) object | Message was created | -| 400 | [error response](/http-api/#error-response) | Body, multipart form, content, reference, mention, attachment, embed, sticker, or voice message contract is invalid, the recipient's direct message policy denies the send and the request returns `CANNOT_SEND_MESSAGES_TO_USER`, or slowmode denies the send and the request returns `SLOWMODE_RATE_LIMITED` | +| 400 | [error response](/http-api/#error-response) | Body, multipart form, content, reference, mention, attachment, embed, sticker, or voice message contract is invalid | +| 400 | [error response](/http-api/#error-response) | The recipient's direct message policy denies the send and the request returns `CANNOT_SEND_MESSAGES_TO_USER` | +| 400 | [error response](/http-api/#error-response) | Slowmode denies the send and the request returns `SLOWMODE_RATE_LIMITED` | | 403 | [error response](/http-api/#error-response) | Caller lacks a required permission and the request returns `MISSING_PERMISSIONS` | | 403 | [error response](/http-api/#error-response) | Is timed out and the request returns `COMMUNICATION_DISABLED` | | 403 | [error response](/http-api/#error-response) | Is age restricted from the channel and the request returns `NSFW_CONTENT_AGE_RESTRICTED` | @@ -1164,7 +1166,7 @@ The resolved `max_voice_message_duration` limit defaults to 1200 seconds. Slowmode uses no rate limit bucket. The denial returns 400 `SLOWMODE_RATE_LIMITED` with a `Retry-After` header in whole seconds and a top-level `retry_after` member in decimal seconds. ::: -`retry_after` is never more than one second below the header. The response has no bucket headers and no `X-RateLimit-Scope`, so a client can tell it apart from a route or [global limit](/topics/rate-limits/) denial. +`retry_after` is never more than one second below the `Retry-After` header. The response has no bucket headers and no `X-RateLimit-Scope`, so a client can tell it apart from a route or [global limit](/topics/rate-limits/) denial. ### Side effects @@ -1174,7 +1176,7 @@ It advances the author's read state unless the author is a bot, reopens a direct It emits [Message Create](/gateway/events/#message-create) to every session that can see the channel. Mention and reply notifications follow the resolved allowed mentions policy. -It creates no audit log entry. +It writes no audit log entry. ### Rate limit @@ -1192,9 +1194,10 @@ Modifies a message. Returns the updated [message](#message-object) object. Emits - 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`. -- `MANAGE_MESSAGES` is an [elevated permission](/http-api/permissions/#elevated-permissions), so a caller holding the bit without an 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`. +- `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/). -- The route takes a per-message write lock for the duration of the edit. A request that cannot take the lock fails with 429 `RESOURCE_LOCKED` and a `Retry-After` of 1. +- The route takes a per-message write lock for the duration of the edit, and a request that cannot take it fails with 429 `RESOURCE_LOCKED` and a `Retry-After` of 1. The body is read exactly as it is for [Create message](#create-message), so a `multipart/form-data` request is accepted with the same `payload_json` and `files[N]` fields. A file that is not uploaded directly must first be planned through [Request attachment upload URLs](#request-attachment-upload-urls) and then referenced by its `upload_filename`. @@ -1226,7 +1229,7 @@ A request that supplies `message_snapshots` is rejected with 403 `MISSING_PERMIS An omitted field preserves its stored value, and the message's `edited_timestamp` advances only when `content` changes. The request itself must supply at least one of content with visible characters, an embed, an attachment, or `flags`, and one that supplies none of them fails with 400 `CANNOT_SEND_EMPTY_MESSAGE`. :::caution[Supplying attachments replaces the whole collection] -To retain an attachment, list its existing ID again, because any attachment left out is dropped. The same array adds new attachments as [pre-uploaded attachment](#pre-uploaded-attachment-object) entries. The same rule applies to `embeds`. +Any attachment left out is dropped, so list an existing ID again to retain it. The same array adds new attachments as [pre-uploaded attachment](#pre-uploaded-attachment-object) entries. The same rule applies to `embeds`. ::: An `id` that names no attachment on the message is skipped, and the edit still succeeds. @@ -1248,7 +1251,7 @@ An `id` that names no attachment on the message is skipped, and the edit still s The operation updates the message, advances its edit timestamp when `content` changed, attaches every newly referenced upload, and drops each attachment the request did not retain. It recalculates mentions when `content`, `allowed_mentions`, or `embeds` changed, updates search results when the channel is indexed, and resolves newly added eligible URLs into embeds. -It emits [Message Update](/gateway/events/#message-update) to every session that can see the channel. It creates no audit log entry, even for a moderator edit. +It emits [Message Update](/gateway/events/#message-update) to every session that can see the channel. It writes no audit log entry, even for a moderator edit. ### Rate limit @@ -1262,7 +1265,7 @@ Deletes the authenticated identity's read state entry for one channel. Returns 2 ### Limitations -- Fluxer does not resolve the channel, so the operation mutates only the caller's own read state. +- Fluxer does not resolve the channel, so the operation changes only the caller's own read state. - The operation is idempotent, and a channel with no stored entry still returns 204. ### Path parameters @@ -1296,8 +1299,8 @@ Deletes one message. Returns 204 with an empty body on success. Emits a [Message - The target must be a deletable [message type](#message-types). - The author can delete their own message anywhere. -- Deleting another user's guild message requires both [SEND_MESSAGES](/http-api/permissions/) and [MANAGE_MESSAGES](/http-api/permissions/), and no caller can delete another user's private channel message. -- `MANAGE_MESSAGES` is an [elevated permission](/http-api/permissions/#elevated-permissions), so a caller holding the bit without an 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. +- Deleting another user's guild message requires both [SEND_MESSAGES](/http-api/permissions/) and [MANAGE_MESSAGES](/http-api/permissions/), which is an [elevated permission](/http-api/permissions/#elevated-permissions). No caller can delete another user's private channel message. +- A caller who holds `MANAGE_MESSAGES` 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. - The operation reads no audit reason header. ### Path parameters @@ -1308,7 +1311,7 @@ Deletes one message. Returns 204 with an empty body on success. Emits a [Message | message_id | snowflake | The ID of the message to delete | :::danger[Deleting a message destroys its attachments too] -The message, its reactions, and its attachments are removed permanently and disappear from search. There is no grace period or restore operation. +There is no grace period and no restore operation. ::: ### Response @@ -1354,7 +1357,7 @@ Deletes one attachment from the caller's own message. Returns 204 with an empty | attachment_id | snowflake | The ID of the attachment to delete | :::caution[Removing the last payload deletes the message] -When the attachment is the only attachment and the message would otherwise be empty, Fluxer deletes the whole message. The response is 204 in both cases, so the emitted Gateway Dispatch is the only signal that distinguishes them. +The response is 204 whether the message survives or is deleted, so the emitted Gateway Dispatch is the only signal that tells the two apart. ::: ### Response @@ -1382,8 +1385,8 @@ Deletes from 1 through 100 messages in one guild channel. Returns 204 with an em ### Limitations - The channel must belong to a guild, and a private channel fails with 400 `CANNOT_EXECUTE_ON_DM`. -- The caller requires [MANAGE_MESSAGES](/http-api/permissions/). -- `MANAGE_MESSAGES` is an [elevated permission](/http-api/permissions/#elevated-permissions), so a caller holding the bit without an 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. +- The caller requires [MANAGE_MESSAGES](/http-api/permissions/), which is an [elevated permission](/http-api/permissions/#elevated-permissions). +- A caller who holds `MANAGE_MESSAGES` 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. - The operation applies no age boundary, so a message of any age can be selected. - The operation reads no audit reason header. @@ -1456,7 +1459,7 @@ The deletion runs inside the request in pages of 100 messages, so a channel with | deleted_count | integer | Number of messages removed by this request | :::danger[Purging personal notes cannot be undone] -The operation deletes every message it finds, with no age boundary and no confirmation step. There is no restore step once it has started. +Deletion has no age boundary and no confirmation step, and there is no restore once it has started. ::: ### Response @@ -1486,7 +1489,7 @@ Deletes every message the caller authored in one channel. Returns 202 with an em ### Limitations - User session and bot credentials are accepted, and a bot credential satisfies the sudo requirement without any proof. -- The caller must be able to resolve the channel and must be in [sudo mode](/http-api/users/mfa/#sudo-mode), established by the `X-Fluxer-Sudo-Mode-JWT` header or by supplying a proof in the body. +- The caller must be able to resolve the channel and be in [sudo mode](/http-api/users/mfa/#sudo-mode), established by the `X-Fluxer-Sudo-Mode-JWT` header or by supplying a proof in the body. - A caller who satisfies none of those receives 403 `SUDO_MODE_REQUIRED` with `has_mfa` and the available `methods`. - An account that holds no verifiable credential at all, meaning it is not a bot, has no enrolled authenticator, and has no stored password, satisfies the sudo requirement with an empty body. @@ -1521,7 +1524,7 @@ The deletion runs inside the request despite the 202 status, so every matching m 2 Accepted only when the account has an enrolled authenticator. A failed verification returns the field code `INVALID_MFA_CODE` :::danger[Every message the caller authored is destroyed] -The deletion has no age boundary and no selection. It permanently removes every message the caller has authored in the channel, together with its attachments and reactions, and removes each from search. There is no cancellation and no restore. +The deletion has no age boundary and no selection. There is no cancellation and no restore. ::: ### Response @@ -1536,7 +1539,7 @@ The deletion has no age boundary and no selection. It permanently removes every ### Side effects -Each matching message and its attachments are permanently deleted and removed from search. Deletions emit batched [Message Delete Bulk](/gateway/events/#message-delete-bulk) Dispatches. +Each matching message, with its attachments and reactions, is permanently deleted and removed from search. Deletions emit batched [Message Delete Bulk](/gateway/events/#message-delete-bulk) Dispatches. The operation emits no [Channel Pins Update](/gateway/events/#channel-pins-update) and no individual [Message Delete](/gateway/events/#message-delete), and it writes no guild audit log entry. @@ -1665,7 +1668,7 @@ Unpins a message. Returns 204 with an empty body on success. Emits [Message Upda ### Side effects -Unpinning a currently pinned message clears its pin timestamp and emits [Message Update](/gateway/events/#message-update) and [Channel Pins Update](/gateway/events/#channel-pins-update) to every session that can see the channel. A guild unpin also writes one `MESSAGE_UNPIN` guild audit log entry with no reason. +Unpinning a pinned message clears its pin timestamp and emits [Message Update](/gateway/events/#message-update) and [Channel Pins Update](/gateway/events/#channel-pins-update) to every session that can see the channel. A guild unpin also writes one `MESSAGE_UNPIN` guild audit log entry with no reason. It creates no system message. The `CHANNEL_PINNED_MESSAGE` system message the original pin created stays in the channel, and the channel's last-pin timestamp is unchanged. @@ -1687,6 +1690,10 @@ The request has no body. Fluxer acknowledges the channel's own stored last-pin t | --- | --- | --- | | channel_id | snowflake | The ID of the channel whose current pin state is acknowledged | +:::note[The channel is read without an access check] +A channel the caller cannot see is acknowledged like any other whenever it stores a last-pin timestamp. +::: + ### Response | Status | Body | Condition | @@ -1694,15 +1701,11 @@ The request has no body. Fluxer acknowledges the channel's own stored last-pin t | 204 | empty | Pin state was absent or acknowledged | | 403 | [error response](/http-api/#error-response) | Credential type or account state denies the request and it returns `ACCESS_DENIED` or `ACCOUNT_SUSPICIOUS_ACTIVITY` | -:::note[The channel is read without an access check] -A channel the caller cannot see is acknowledged like any other whenever it stores a last-pin timestamp. Fluxer writes nothing when the channel does not exist or has never held a pin. -::: - ### Side effects -When the channel stores a last-pin timestamp, Fluxer writes that exact instant to the caller's read state and emits one [Channel Pins ACK](/gateway/events/#channel-pins-ack) with the channel ID and that timestamp to the caller's own sessions. The write replaces the stored value. When the channel stores no last-pin timestamp, Fluxer writes nothing and emits nothing. +When the channel stores a last-pin timestamp, Fluxer writes that exact instant to the caller's read state. It then emits one [Channel Pins ACK](/gateway/events/#channel-pins-ack) with the channel ID and that timestamp to the caller's own sessions. The write replaces the stored value. Fluxer writes nothing and emits nothing when the channel does not exist or has never held a pin. -When the Dispatch fails, the request returns 502, 503, or 504 after the read-state write has landed. A client treats those statuses as indeterminate and reconciles from a later read. +When the Dispatch fails, the request returns 502, 503, or 504 after the read-state write has landed. A client treats those statuses as unknown and reconciles from a later read. ### Rate limit @@ -1712,13 +1715,15 @@ When the Dispatch fails, the request returns 502, 503, or 504 after the read-sta The `emoji` path value is URI-encoded and is 1 through 64 characters. A custom emoji uses `name:id`, where `id` is the trailing decimal custom emoji snowflake and `name` is everything before the final colon. Any other decoded value is read as a Unicode emoji. -Fluxer validates the emoji in full only when a request would create a new reaction group on the message. At that point a Unicode value must be exactly one valid emoji, and one that is not fails with the field code `NOT_A_VALID_UNICODE_EMOJI`. A custom emoji must exist, and one that does not fails with the field code `CUSTOM_EMOJI_NOT_FOUND`. Every other reaction operation parses the value without validating it, so removing a reaction with a nonsensical emoji succeeds silently. +Fluxer validates the emoji in full only when a request would create a new reaction group on the message. At that point a Unicode value must be exactly one valid emoji, and one that is not fails with the field code `NOT_A_VALID_UNICODE_EMOJI`. A custom emoji must exist, and one that does not fails with the field code `CUSTOM_EMOJI_NOT_FOUND`. Every other reaction operation parses the value without validating it, so removing a reaction with an invalid emoji succeeds silently. + +Every reaction operation is idempotent and writes no audit log entry. Every removal route emits its Dispatch whether or not the message held the reaction, group, or reactions the request names. [Add own reaction](#add-own-reaction), [Remove own reaction](#remove-own-reaction), and [Remove another user's reaction](#remove-another-users-reaction) each accept a `session_id` query parameter, which is echoed in that route's Dispatch so the originating session can match its own optimistic update, and it never affects authorisation or the stored reaction. ## List reaction users unpaged -Returns a bare array of [partial user](/http-api/users/#partial-user-object) objects for one ordinary reaction. +Returns a bare array of [partial user](/http-api/users/#partial-user-object) objects for one reaction. ### Limitations @@ -1760,7 +1765,7 @@ An emoji with no reaction group on the message returns 200 with an empty array. -Returns a [reaction users page](#reaction-users-page-object) object holding the users who added one ordinary reaction. +Returns a [reaction users page](#reaction-users-page-object) object holding the users who added one reaction. ### Limitations @@ -1801,7 +1806,7 @@ An emoji with no reaction group on the message returns 200 with an empty `items` -Adds the authenticated identity's ordinary reaction. Returns 204 with an empty body. Emits a [Message Reaction Add](/gateway/events/#message-reaction-add) Gateway event. +Adds the authenticated identity's reaction. Returns 204 with an empty body. Emits a [Message Reaction Add](/gateway/events/#message-reaction-add) Gateway event. ### Limitations @@ -1809,10 +1814,10 @@ Adds the authenticated identity's ordinary reaction. Returns 204 with an empty b - A non-bot caller must have a verified email, and one that does not receives 403 `REACTION_EMAIL_VERIFICATION_REQUIRED`. - Creating a new emoji reaction group in a guild requires [ADD_REACTIONS](/http-api/permissions/), while adding to an existing group does not. - A custom emoji whose source guild is not the channel's guild requires the `feature_global_expressions` entitlement, and a caller without it receives the field code `CUSTOM_EMOJIS_REQUIRE_PREMIUM_OUTSIDE_SOURCE`. -- In a guild channel a custom emoji additionally requires [USE_EXTERNAL_EMOJIS](/http-api/permissions/). +- In a guild channel a custom emoji also requires [USE_EXTERNAL_EMOJIS](/http-api/permissions/). - A non-bot caller must have started a session, and one that has not receives the field code `MUST_START_SESSION_BEFORE_SENDING`. - An unclaimed account can react only in its personal notes channel, and elsewhere receives 400 `UNCLAIMED_ACCOUNT_CANNOT_ADD_REACTIONS`. -- The operation is idempotent, and this route applies no direct message send policy. +- This route applies no direct message send policy. ### Path parameters @@ -1828,13 +1833,13 @@ Adds the authenticated identity's ordinary reaction. Returns 204 with an empty b | --- | --- | --- | | session_id?1 | string | Originating Gateway session ID (1-64 characters) | -1 Echoed in the resulting Dispatch so the originating session can correlate its own optimistic update, and it never affects authorisation or the stored reaction +1 See [reaction session correlation](#reaction-emoji-path-value) :::note[Permission is checked only for a new group] `ADD_REACTIONS`, Unicode emoji validation, custom emoji existence, and the external emoji checks apply only when the request would open a new group. Joining an existing group is authorised by channel access alone. ::: -A message has at most the resolved `max_reactions_per_message` distinct reaction groups, which defaults to 30, and one group at most the resolved `max_users_per_message_reaction` users, which defaults to 1,000,000. Both ceilings are reported as 400 `MAX_REACTIONS`. +[Reaction object](#reaction-object) states the group and user ceilings for a message, and reaching either returns 400 `MAX_REACTIONS`. ### Response @@ -1856,7 +1861,7 @@ A message has at most the resolved `max_reactions_per_message` distinct reaction ### Side effects -Adding a reaction the caller does not already hold creates it and emits [Message Reaction Add](/gateway/events/#message-reaction-add) to every session that can see the channel, with `session_id` for originating session correlation. Adding a reaction the caller already has changes nothing and emits no Dispatch. It writes no audit log entry. +Adding a reaction the caller does not already hold creates it and emits [Message Reaction Add](/gateway/events/#message-reaction-add) to every session that can see the channel, with `session_id` for originating session correlation. Adding a reaction the caller already has changes nothing and emits no Dispatch. ### Rate limit @@ -1866,13 +1871,12 @@ Adding a reaction the caller does not already hold creates it and emits [Message -Removes the authenticated identity's own ordinary reaction. Returns 204 with an empty body. Emits a [Message Reaction Remove](/gateway/events/#message-reaction-remove) Gateway event. +Removes the authenticated identity's own reaction. Returns 204 with an empty body. Emits a [Message Reaction Remove](/gateway/events/#message-reaction-remove) Gateway event. ### Limitations - The caller must be able to view the text-bearing channel and reach the message under the message history cutoff. - A removal re-runs none of the timeout, email verification, and emoji validity checks. -- The operation is idempotent. ### Path parameters @@ -1888,7 +1892,7 @@ Removes the authenticated identity's own ordinary reaction. Returns 204 with an | --- | --- | --- | | session_id?1 | string | Originating Gateway session ID (1-64 characters) | -1 Echoed in the resulting Dispatch so the originating session can correlate its own optimistic update, and it never affects authorisation +1 See [reaction session correlation](#reaction-emoji-path-value) ### Response @@ -1903,7 +1907,7 @@ A message that does not exist returns 204 rather than 404. ### Side effects -Fluxer deletes the reaction and emits [Message Reaction Remove](/gateway/events/#message-reaction-remove) to every session that can see the channel, with `session_id` for originating session correlation. The Dispatch is emitted whether or not the caller held the reaction. +Fluxer deletes the reaction and emits [Message Reaction Remove](/gateway/events/#message-reaction-remove) to every session that can see the channel, with `session_id` for originating session correlation. ### Rate limit @@ -1913,15 +1917,14 @@ Fluxer deletes the reaction and emits [Message Reaction Remove](/gateway/events/ -Removes one named user's ordinary reaction. Returns 204 with an empty body. Emits a [Message Reaction Remove](/gateway/events/#message-reaction-remove) Gateway event. +Removes one named user's reaction. Returns 204 with an empty body. Emits a [Message Reaction Remove](/gateway/events/#message-reaction-remove) Gateway event. ### Limitations - The caller must be able to view the text-bearing channel and reach the message under the message history cutoff. - The message author may remove any user's reaction from their own message in any channel. - Any other caller must be in a guild channel and must hold [MANAGE_MESSAGES](/http-api/permissions/), so a non-author in a private channel is refused with 403 `MISSING_PERMISSIONS`. -- `MANAGE_MESSAGES` is an [elevated permission](/http-api/permissions/#elevated-permissions), so a caller holding the bit without an 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. -- The operation is idempotent. +- `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. ### Path parameters @@ -1940,7 +1943,7 @@ Removes one named user's ordinary reaction. Returns 204 with an empty body. Emit | --- | --- | --- | | session_id?1 | string | Originating Gateway session ID (1-64 characters) | -1 Echoed in the resulting Dispatch so the originating session can correlate its own optimistic update, and it never affects authorisation +1 See [reaction session correlation](#reaction-emoji-path-value) ### Response @@ -1955,7 +1958,7 @@ A message that does not exist returns 204 before the authorship and permission c ### Side effects -Fluxer deletes the target user's reaction and emits [Message Reaction Remove](/gateway/events/#message-reaction-remove) with the target user ID to every session that can see the channel, plus `session_id` when the request supplied one. The Dispatch is emitted whether or not the target held the reaction. It creates no audit log entry. +Fluxer deletes the target user's reaction and emits [Message Reaction Remove](/gateway/events/#message-reaction-remove) with the target user ID to every session that can see the channel, plus `session_id` when the request supplied one. ### Rate limit @@ -1965,15 +1968,14 @@ Fluxer deletes the target user's reaction and emits [Message Reaction Remove](/g -Removes every ordinary reaction for one emoji. Returns 204 with an empty body. Emits a [Message Reaction Remove Emoji](/gateway/events/#message-reaction-remove-emoji) Gateway event. +Removes every reaction for one emoji. Returns 204 with an empty body. Emits a [Message Reaction Remove Emoji](/gateway/events/#message-reaction-remove-emoji) Gateway event. ### Limitations - The caller must be able to view the text-bearing channel and reach the message under the message history cutoff. - The message author may clear the emoji on their own message in any channel. -- Any other caller must be in a guild channel and must hold [MANAGE_MESSAGES](/http-api/permissions/). -- `MANAGE_MESSAGES` is an [elevated permission](/http-api/permissions/#elevated-permissions), so a caller holding the bit without an 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. -- The operation is idempotent. +- Any other caller must be in a guild channel and must hold [MANAGE_MESSAGES](/http-api/permissions/), an [elevated permission](/http-api/permissions/#elevated-permissions). +- A caller who holds `MANAGE_MESSAGES` 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. ### Path parameters @@ -1994,7 +1996,7 @@ Removes every ordinary reaction for one emoji. Returns 204 with an empty body. E ### Side effects -Fluxer deletes every reaction for the emoji and emits one [Message Reaction Remove Emoji](/gateway/events/#message-reaction-remove-emoji) to every session that can see the channel. Fluxer emits no [Message Reaction Remove](/gateway/events/#message-reaction-remove) for the individual users. The Dispatch is emitted whether or not the message had a group for the emoji. It creates no audit log entry. +Fluxer deletes every reaction for the emoji and emits one [Message Reaction Remove Emoji](/gateway/events/#message-reaction-remove-emoji) to every session that can see the channel. Fluxer emits no [Message Reaction Remove](/gateway/events/#message-reaction-remove) for the individual users. ### Rate limit @@ -2004,15 +2006,14 @@ Fluxer deletes every reaction for the emoji and emits one [Message Reaction Remo -Removes every ordinary reaction from a message. Returns 204 with an empty body. Emits a [Message Reaction Remove All](/gateway/events/#message-reaction-remove-all) Gateway event. +Removes every reaction from a message. Returns 204 with an empty body. Emits a [Message Reaction Remove All](/gateway/events/#message-reaction-remove-all) Gateway event. ### Limitations - The caller must be able to view the text-bearing channel and reach the message under the message history cutoff. - The message author may clear their own message in any channel. -- Any other caller must be in a guild channel and must hold [MANAGE_MESSAGES](/http-api/permissions/). -- `MANAGE_MESSAGES` is an [elevated permission](/http-api/permissions/#elevated-permissions), so a caller holding the bit without an 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. -- The operation is idempotent. +- Any other caller must be in a guild channel and must hold [MANAGE_MESSAGES](/http-api/permissions/), an [elevated permission](/http-api/permissions/#elevated-permissions). +- A caller who holds `MANAGE_MESSAGES` 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. ### Path parameters @@ -2032,7 +2033,7 @@ Removes every ordinary reaction from a message. Returns 204 with an empty body. ### Side effects -Fluxer deletes every reaction on the message and emits one [Message Reaction Remove All](/gateway/events/#message-reaction-remove-all) to every session that can see the channel. Fluxer emits no event for the individual groups or users. The Dispatch is emitted whether or not the message had any reaction. It creates no audit log entry. +Fluxer deletes every reaction on the message and emits one [Message Reaction Remove All](/gateway/events/#message-reaction-remove-all) to every session that can see the channel. Fluxer emits no event for the individual groups or users. ### Rate limit @@ -2053,7 +2054,7 @@ Publishes typing activity in a text-bearing channel. Returns 204 with an empty b | Field | Type | Description | | --- | --- | --- | -| channel_id | snowflake | The ID of the channel in which typing is indicated | +| channel_id | snowflake | The ID of the channel where typing is shown | ### Response @@ -2066,7 +2067,7 @@ Publishes typing activity in a text-bearing channel. Returns 204 with an empty b ### Side effects -The operation emits [Typing Start](/gateway/events/#typing-start) to every session that can see the channel, with the channel ID, the caller's user ID, and a Unix timestamp in seconds. It creates no durable state, so no read operation reports a typing indicator and the indicator expires on the client. When the guild has typing events disabled, the response is still 204 and no Dispatch is emitted. +The operation emits [Typing Start](/gateway/events/#typing-start) to every session that can see the channel, with the channel ID, the caller's user ID, and a Unix timestamp in seconds. It creates no durable state, so no read operation reports a typing indicator, and the indicator expires on the client. When the guild has typing events disabled, the response is still 204 and no Dispatch is emitted. ### Rate limit @@ -2081,7 +2082,7 @@ Advances the authenticated user's read state through one message. Returns 204 wi ### Limitations - This is a user-only operation, and a bot credential is refused with 403 `ACCESS_DENIED`. -- Fluxer resolves neither the channel nor the message, so the operation writes the supplied identifiers straight into the caller's own read state and mutates nothing else. +- Fluxer resolves neither the channel nor the message, so the operation writes the supplied identifiers straight into the caller's own read state and changes nothing else. ### Path parameters @@ -2099,7 +2100,7 @@ An empty body is read as `{}`, and `{}` is the minimal valid form. | Field | Type | Description | | --- | --- | --- | | mention_count?1 | integer | Mention count to store for the channel (0-2,147,483,647, default `0`) | -| manual?2 | boolean | Whether the acknowledgement was explicitly initiated by the user (default false) | +| manual?2 | boolean | Whether the acknowledgement was explicitly started by the user (default false) | 1 Fluxer stores the value verbatim. A client that acknowledges partway through a channel must send the number of mentions that remain above the watermark, and one that omits the field resets the stored count to `0` @@ -2118,9 +2119,9 @@ Without `manual`, the marker advances to the larger of its current value and `me ### Side effects -The operation stores the new read-through marker and the supplied mention count for the caller and channel. It invalidates the caller's cached unread badge count, clears the channel's delivered notifications through the acknowledged watermark, and emits [Message ACK](/gateway/events/#message-ack) to the caller's own sessions only. No message state is modified. +The operation stores the new read-through marker and the supplied mention count for the caller and channel. It invalidates the caller's cached unread badge count, clears the channel's delivered notifications through the acknowledged watermark, and emits [Message ACK](/gateway/events/#message-ack) to the caller's own sessions only. -A non-manual acknowledgement whose `message_id` is below the stored watermark leaves the stored watermark and mention count untouched. The Dispatch reports the stored values. +A non-manual acknowledgement whose `message_id` is below the stored watermark leaves the stored watermark and mention count unchanged. The Dispatch reports the stored values. ### Rate limit diff --git a/fluxer_docs/src/content/docs/http-api/oauth2.mdx b/fluxer_docs/src/content/docs/http-api/oauth2.mdx index b8997cdc6..c374ccf4b 100644 --- a/fluxer_docs/src/content/docs/http-api/oauth2.mdx +++ b/fluxer_docs/src/content/docs/http-api/oauth2.mdx @@ -103,7 +103,7 @@ The same route can return either one. [Authorise application](#authorise-applica 3 The client application is unknown, supplied no client secret, or supplied a client secret that does not match the stored hash -4 The authorisation code or refresh token is unknown, expired, already used, bound to another application, or bound to another redirect URI, or the authorisation code has a PKCE challenge the presented verifier does not prove +4 The authorisation code or refresh token is unknown, expired, already used, bound to another application, or bound to another redirect URI, or the authorisation code has a PKCE challenge the presented verifier does not match ## OAuth2 token object @@ -119,7 +119,7 @@ The access and refresh token pair a successful token exchange returns. | refresh_token2 | string | The refresh token that replaces this pair when it is exchanged | | scope | string | The granted scopes, space separated in registry order | -1 The configured access token lifetime, restated in every successful response +1 The configured access token lifetime, repeated in every successful response 2 Every grant Fluxer issues is bound to a user, so a successful exchange always returns a refresh token @@ -162,7 +162,7 @@ The account identity an application reads with the `identify` scope. 3 Emitted only when the `email` scope is granted and the account has an address. An unverified address reports false -4 Declared optional by the response schema and populated on every response +4 Declared optional by the response schema and emitted on every response 5 Emitted on every response although the response schema does not declare it, so a client MUST NOT depend on it @@ -177,7 +177,7 @@ The liveness and scope report Fluxer returns for one presented token. | active | boolean | Whether the presented token is live and belongs to the authenticated client | | scope? | string | The granted scopes, space separated in registry order | | client_id? | snowflake | The ID of the application the token was issued to | -| username?1 | string | A reserved member that Fluxer never populates | +| username?1 | string | A reserved member that Fluxer never sets | | token_type? | string | The kind of token presented, `Bearer` for an access token and `refresh_token` for a refresh token | | exp?2 | integer | The access token expiry time in Unix seconds | | iat? | integer | The issue time in Unix seconds | @@ -191,11 +191,7 @@ The liveness and scope report Fluxer returns for one presented token. Apart from `username` and `exp`, every optional field above is present when `active` is true and absent when it is false. An active access token has `exp` and an active refresh token does not. -:::note[Several conditions produce the same inactive body] -The body has only `active` set to false, and names no reason. -::: - -Fluxer reports an unknown token, an expired token, and a token issued to another application as inactive. Introspection never resolves the account behind the token, so an active result reports on the token alone. +Fluxer reports an unknown token, an expired token, and a token issued to another application as inactive. An inactive body has only `active` set to false, and names no reason. Introspection never resolves the account behind the token, so an active result reports on the token alone. ## Current OAuth2 authorisation object @@ -230,7 +226,7 @@ The reduced application record in a [current OAuth2 authorisation](#current-oaut | bot_require_code_grant | boolean | Whether bot installation requires an OAuth2 code grant | | flags2 | integer | The application flags, always 0 | -1 Null even when the application's bot has an avatar or biography +1 Null even when the application's bot has an avatar or bio 2 No application flag bit is defined @@ -358,7 +354,7 @@ Without `prompt=none`, the redirect targets the Fluxer consent interface and for With `prompt=none`, a request that resolves no user redirects with `error=login_required`. Every other silent failure redirects with `error=consent_required`. That covers every failure to issue a code, including an unknown application, an unrecognised scope, a missing or unregistered redirect URI, and a private bot the caller does not own. It also covers a requested non-bot scope the user has not already granted to this application. A silent request that asks only for `bot` skips the prior-grant test. -An error redirect has `error` and the normalised `state` when the caller supplied one. It has no `error_description`, and never the more specific [OAuth2 error code](#oauth2-error-codes) the underlying failure raised. +An error redirect has `error` and the normalised `state` when the caller supplied one, but no `error_description` and never the more specific [OAuth2 error code](#oauth2-error-codes) the underlying failure raised. :::caution[An unregistered redirect URI never receives the error] The error redirect targets the supplied `redirect_uri` only when the named application has registered that exact string, so this route cannot bounce a browser to an arbitrary origin. @@ -427,7 +423,7 @@ A guild installation emits [Guild Create](/gateway/events/#guild-create), [Guild Consent issues one authorisation code bound to the application, user, resolved redirect URI, granted scopes, and PKCE challenge, and returns the callback URL that has it. -With the `bot` scope and a guild target, consent adds the bot to the guild with the bot-invite join source and the caller recorded as inviter. Consent bypasses both the bot account's own guild count limit and the guild ban list. The guild member limit still applies. When a non-zero permission mask is requested, consent also creates one role with exactly those permissions, names the role after the application, and assigns it to the bot. +With the `bot` scope and a guild target, consent adds the bot to the guild with the bot-invite join source and the caller recorded as inviter. Consent bypasses both the bot account's own guild count limit and the guild ban list. The guild member limit still applies. When a non-zero permission mask is requested, consent also creates one role with exactly the filtered permission mask, names the role after the application, and assigns it to the bot. The installation either succeeds with exactly that mask or fails with a missing-permissions error. That role is created at position 1 with no colour and is neither hoisted nor mentionable. It consumes a guild role slot and is not removed when the bot leaves. The installation records bot add, role create, and member role update [audit log](/http-api/guild-audit-logs/) entries. @@ -435,10 +431,6 @@ With the `bot` scope and a group direct message target, consent adds the bot as When any part of the installation fails, Fluxer cancels the newly issued authorisation code, so the caller never receives a usable code. -:::caution[The installed role has the filtered mask] -The installation either succeeds with exactly that mask or fails with a missing-permissions error. -::: - ### Rate limit 60 requests per minute for each authenticated user, on the `oauth:authorize` bucket. @@ -506,11 +498,11 @@ When the code is PKCE-bound, an omitted or incorrect verifier is rejected as `in | 400 | [error response](/http-api/#error-response) | The form fails validation, including an unsupported `grant_type` | | 4002 | [OAuth2 error](#oauth2-error-response-object) object | Client authentication or the presented grant fails | -2 Client authentication, the authorisation code, the refresh token, the redirect URI, or a required PKCE verifier is missing or does not prove the grant +2 Client authentication, the authorisation code, the refresh token, the redirect URI, or a required PKCE verifier is missing or does not match the grant An authorisation code is single-use. Fluxer consumes it before it issues the token pair. A replayed code is rejected as an invalid grant. Deleting an account purges every OAuth2 access and refresh token it holds, so a refresh exchange for a deleted account is rejected as an invalid grant as well. An authorisation code issued before the deletion still exchanges until it expires. -Fluxer verifies client authentication before it examines the grant, so an unknown application, an absent client secret, and a wrong client secret are all reported as `invalid_client`. +Fluxer verifies client authentication before it checks the grant, so an unknown application, an absent client secret, and a wrong client secret are all reported as `invalid_client`. :::note[No client credentials grant] Only `authorization_code` and `refresh_token` exist, and Fluxer issues no token that is unbound to a user. @@ -532,7 +524,7 @@ A successful exchange issues one access token that expires after 7 days and one Returns the [OAuth2 user information](#oauth2-user-information-object) object for the account the token was issued to. Requires the `identify` [scope](#oauth2-scopes). -An OAuth2 bearer access token is required. The `email` scope populates the address fields. +An OAuth2 bearer access token is required. The `email` scope fills in the address fields. ### Request headers @@ -645,11 +637,11 @@ A client that presents a refresh token MUST send `token_type_hint=refresh_token` Fluxer ignores an `Authorization` header that is not well-formed HTTP Basic and reads the client credentials from the form. -An unknown token and a token issued to another application both return 200, so the response never reveals whether a token exists. +An unknown token and a token issued to another application both return 200, so the response never shows whether a token exists. ### Side effects -On a match, Fluxer deletes the user's complete token set for the authenticated application. Those tokens stop authenticating requests at once. A token that matched nothing mutates no state. +On a match, Fluxer deletes the user's complete token set for the authenticated application. Those tokens stop authenticating requests at once. A token that matched nothing changes no state. No Gateway Dispatch is emitted, so a client holding a revoked token learns of the revocation from its next rejected request. @@ -753,7 +745,7 @@ Revokes the current user's authorisations for several applications. | --- | --- | --- | | application_ids1 | array[snowflake] | The IDs of the applications to revoke (1-100) | -1 Repeated IDs are collapsed before any revocation is performed +1 Repeated IDs are collapsed before any revocation is done ### Response diff --git a/fluxer_docs/src/content/docs/http-api/permissions.mdx b/fluxer_docs/src/content/docs/http-api/permissions.mdx index 8da3d7b42..3f590d575 100644 --- a/fluxer_docs/src/content/docs/http-api/permissions.mdx +++ b/fluxer_docs/src/content/docs/http-api/permissions.mdx @@ -82,7 +82,7 @@ Every permission response field uses the decimal representation of an unsigned 6 A value that is not a run of decimal digits is rejected with the validation code `INVALID_INTEGER_FORMAT`, and one above the maximum with `INTEGER_OUT_OF_INT64_RANGE`. A JSON number cannot represent the full 64-bit range, so a client MUST parse and combine masks with unsigned 64-bit integer arithmetic and MUST NOT compare them with string equality. -Fluxer discards a bit it does not define. A role mutation intersects the requested mask with the set of defined bits before authorisation. An unassigned bit never appears in a response. A channel permission overwrite uses the same mask and the same defined-bit set, and Fluxer does not filter bits by channel type, so an overwrite can name a permission its channel does not use. +Fluxer discards a bit it does not define. A role mutation intersects the requested mask with the set of defined bits before authorisation. An unassigned bit never appears in a response. A channel permission overwrite uses the same mask and the same defined-bit set. Fluxer does not filter bits by channel type, so an overwrite can name a permission its channel does not use. ### Feature-gated permission bits @@ -98,9 +98,9 @@ On [Create guild role](#create-guild-role), a request that supplies `permissions ## Elevated permissions -`KICK_MEMBERS`, `BAN_MEMBERS`, `ADMINISTRATOR`, `MANAGE_CHANNELS`, `MANAGE_GUILD`, `MANAGE_MESSAGES`, `MANAGE_ROLES`, `MANAGE_WEBHOOKS`, and `MODERATE_MEMBERS` are elevated permissions. In a guild whose [MFA level](/http-api/guilds/#mfa-levels) is elevated, only the guild owner exercises them without an enrolled authenticator. +`KICK_MEMBERS`, `BAN_MEMBERS`, `ADMINISTRATOR`, `MANAGE_CHANNELS`, `MANAGE_GUILD`, `MANAGE_MESSAGES`, `MANAGE_ROLES`, `MANAGE_WEBHOOKS`, and `MODERATE_MEMBERS` are elevated permissions. In a guild whose [MFA level](/http-api/guilds/#mfa-levels) is elevated, only the guild owner uses them without an enrolled authenticator. -An operation that asserts an elevated permission the caller holds but cannot exercise returns 400 `TWO_FACTOR_REQUIRED`. The check runs after Fluxer has confirmed the permission itself, so a caller who lacks the permission outright receives 403 `MISSING_PERMISSIONS` instead. +An operation that asserts an elevated permission the caller holds but cannot use returns 400 `TWO_FACTOR_REQUIRED`. The check runs after Fluxer has confirmed the permission itself, so a caller who lacks the permission outright receives 403 `MISSING_PERMISSIONS` instead. [List guild roles](#list-guild-roles) asserts no permission, so it never produces `TWO_FACTOR_REQUIRED`. @@ -119,13 +119,13 @@ Otherwise Fluxer starts from the permissions of the everyone role, whose [snowfl When no channel was named, the union is the final result. When a channel was named, Fluxer applies that channel's stored [permission overwrites](/http-api/channels/#permission-overwrite-object) to the union in this order: 1. The everyone role overwrite, by removing its denied bits and then adding its allowed bits. -2. The overwrites targeting roles assigned to the member, accumulated into one union of allowed bits and one union of denied bits and applied as a single step in the same order. An allow on any one of the member's roles defeats a deny on another. +2. The overwrites targeting roles assigned to the member, combined into one union of allowed bits and one union of denied bits and applied as a single step in the same order. An allow on any one of the member's roles defeats a deny on another. 3. The member overwrite, again denied bits first and allowed bits second. When the named channel does not exist in the guild, Fluxer leaves the guild-level union unchanged. :::caution[`ADMINISTRATOR` grants every bit and skips overwrites] -No deny overwrite in any channel restricts a member who holds the bit. Removing `ADMINISTRATOR` from every role they hold is the only remedy. +No deny overwrite in any channel restricts a member who holds the bit. Removing `ADMINISTRATOR` from every role they hold is the only fix. ::: A guild channel is visible when its channel-scoped mask contains `VIEW_CHANNEL`, or when the member holds temporary access through an in-flight voice or call transition. A category is also visible when the mask of at least one child channel contains `VIEW_CHANNEL`. Temporary access applies only to the channel it was granted on. @@ -179,7 +179,7 @@ Roles are named permission masks that a guild assigns to any number of its membe | mentionable | boolean | Whether a member without `MENTION_EVERYONE` can mention the role | | unicode_emoji?3 | ?string | The Unicode emoji shown beside the role name | -1 The everyone role is always at position 0, and every other role occupies a dense position from 1 through the number of remaining roles after any hierarchy mutation +1 The everyone role is always at position 0, and every other role has a dense position from 1 through the number of remaining roles after any hierarchy mutation 2 A null value means the role has no explicit member list position, and the role is then ordered by its hierarchy `position` instead @@ -311,7 +311,7 @@ Creates a role and returns its [guild role object](#guild-role-object). Requires | Status | Body | Condition | | --- | --- | --- | | 200 | [guild role object](#guild-role-object) | Role was created | -| 400 | [error response](/http-api/#error-response) | The caller cannot exercise `MANAGE_ROLES` because of the guild MFA level and the request returns `TWO_FACTOR_REQUIRED`, or the guild role limit is reached and the request returns `MAX_GUILD_ROLES` | +| 400 | [error response](/http-api/#error-response) | The caller cannot use `MANAGE_ROLES` because of the guild MFA level and the request returns `TWO_FACTOR_REQUIRED`, or the guild role limit is reached and the request returns `MAX_GUILD_ROLES` | | 403 | [error response](/http-api/#error-response) | Bearer credential is used and returns `ACCESS_DENIED`, the guild is unavailable and returns `MISSING_ACCESS`, or the caller is not a member, lacks `MANAGE_ROLES`, or requested a permission they do not hold, each returning `MISSING_PERMISSIONS` | | 404 | [error response](/http-api/#error-response) | Guild does not exist and returns `UNKNOWN_GUILD` | @@ -331,7 +331,7 @@ Modifies one role and returns its [guild role object](#guild-role-object). Requi ### Limitations -- A caller who is not the guild owner additionally requires [hierarchy authority](#role-hierarchy) over the target role and cannot grant a permission absent from their own effective guild permissions. +- A caller who is not the guild owner also requires [hierarchy authority](#role-hierarchy) over the target role and cannot grant a permission absent from their own effective guild permissions. The everyone role is a valid target. @@ -370,7 +370,7 @@ Every field is optional, and an omitted field leaves the stored value unchanged. | Status | Body | Condition | | --- | --- | --- | | 200 | [guild role object](#guild-role-object) | Role was modified, or every supplied value already matched the stored value | -| 400 | [error response](/http-api/#error-response) | The caller cannot exercise `MANAGE_ROLES` because of the guild MFA level and the request returns `TWO_FACTOR_REQUIRED` | +| 400 | [error response](/http-api/#error-response) | The caller cannot use `MANAGE_ROLES` because of the guild MFA level and the request returns `TWO_FACTOR_REQUIRED` | | 403 | [error response](/http-api/#error-response) | Bearer credential is used and returns `ACCESS_DENIED`, the guild is unavailable and returns `MISSING_ACCESS`, or the caller is not a member, lacks `MANAGE_ROLES`, does not outrank the target role, or requested a permission they do not hold, each returning `MISSING_PERMISSIONS` | | 404 | [error response](/http-api/#error-response) | Guild does not exist and returns `UNKNOWN_GUILD`, or the role does not exist and returns `UNKNOWN_ROLE` | @@ -391,14 +391,14 @@ Reorders the role hierarchy and returns 204 with an empty body. Requires [MANAGE ### Limitations - The caller needs [hierarchy authority](#role-hierarchy) over every role whose entry supplies a position differing from that role's stored `position`. -- An entry that omits the field or restates the stored value is accepted without that check. +- An entry that omits the field or repeats the stored value is accepted without that check. Fluxer holds a guild-wide lock while the operation runs, so a concurrent position write on the same guild returns 423 `GENERAL_ERROR`. -Every role the caller can manage is sorted by its requested position in descending order, with a role that supplied no position keeping its stored position as the sort key and any remaining tie resolved by the current order. Fluxer then puts those roles back into the same set of slots they held before, so a role the caller cannot manage never moves. Every role other than the everyone role is then renumbered to a dense position. The highest-ranked role receives the number of roles other than the everyone role, the lowest-ranked role receives 1, and the everyone role stays at 0. +Every role the caller can manage is sorted by its requested position in descending order. A role that supplied no position keeps its stored position as the sort key, and any remaining tie is resolved by the current order. Fluxer then puts those roles back into the same set of slots they held before, so a role the caller cannot manage never moves. Every role other than the everyone role is then renumbered to a dense position. The highest-ranked role receives the number of roles other than the everyone role, the lowest-ranked role receives 1, and the everyone role stays at 0. :::note[A requested position is an ordering key] -The dense renumbering that follows the sort decides the stored value, so it can differ from the one requested. A client reads the resulting `position` from the [Guild Role Update Bulk](/gateway/events/#guild-role-update-bulk) payload. +Dense renumbering after the sort decides the stored value, so it can differ from the one requested. A client reads the resulting `position` from the [Guild Role Update Bulk](/gateway/events/#guild-role-update-bulk) payload. ::: ### Path parameters @@ -409,21 +409,21 @@ The dense renumbering that follows the sort decides the stored value, so it can ### JSON body -The top-level body is an array of [role position objects](#role-position-object). The everyone role cannot appear in the array and is rejected with the validation code `CANNOT_REORDER_EVERYONE_ROLE`, and a role that does not exist in the guild is rejected with `INVALID_ROLE_ID`. Both checks run over the whole array before the hierarchy check and before any write. +The top-level body is an array of [role position objects](#role-position-object). An entry naming the everyone role is rejected with the validation code `CANNOT_REORDER_EVERYONE_ROLE`, and a role that does not exist in the guild is rejected with `INVALID_ROLE_ID`. Both checks run over the whole array before the hierarchy check and before any write. ### Response | Status | Body | Condition | | --- | --- | --- | | 204 | empty | Positions were applied| -| 400 | [error response](/http-api/#error-response) | An entry names the everyone role or a role that does not exist, or the caller cannot exercise `MANAGE_ROLES` because of the guild MFA level| +| 400 | [error response](/http-api/#error-response) | An entry names the everyone role or a role that does not exist, or the caller cannot use `MANAGE_ROLES` because of the guild MFA level| | 403 | [error response](/http-api/#error-response) | Bearer credential is used and returns `ACCESS_DENIED`, the guild is unavailable and returns `MISSING_ACCESS`, or the caller is not a member, lacks `MANAGE_ROLES`, or supplied a position differing from the stored `position` of a role they do not outrank, each returning `MISSING_PERMISSIONS`| | 404 | [error response](/http-api/#error-response) | Guild does not exist and returns `UNKNOWN_GUILD`| | 423 | [error response](/http-api/#error-response) | Another position write holds the guild lock, returning `GENERAL_ERROR` with `Retry-After: 2` | ### Side effects -The operation creates one [ROLE_UPDATE](/http-api/guild-audit-logs/#audit-actions) guild audit log entry for each role whose position changed and emits one [Guild Role Update Bulk](/gateway/events/#guild-role-update-bulk) with those roles to every session that can see the guild. A request that produces no positional change creates no audit entry and emits no Dispatch. Dense renumbering can change the numeric `position` of an unnamed role without changing its rank, and such a role is included in the Dispatch. +The operation creates one [ROLE_UPDATE](/http-api/guild-audit-logs/#audit-actions) guild audit log entry for each role whose position changed. It emits one [Guild Role Update Bulk](/gateway/events/#guild-role-update-bulk) with those roles to every session that can see the guild. A request that produces no positional change creates no audit entry and emits no Dispatch. Dense renumbering can change the numeric `position` of an unnamed role without changing its rank, and such a role is included in the Dispatch. ### Rate limit @@ -457,14 +457,14 @@ The top-level body is an array of [role hoist position objects](#role-hoist-posi | Status | Body | Condition | | --- | --- | --- | | 204 | empty | Hoist positions were applied| -| 400 | [error response](/http-api/#error-response) | An entry names the everyone role or a role that does not exist, or the caller cannot exercise `MANAGE_ROLES` because of the guild MFA level| +| 400 | [error response](/http-api/#error-response) | An entry names the everyone role or a role that does not exist, or the caller cannot use `MANAGE_ROLES` because of the guild MFA level| | 403 | [error response](/http-api/#error-response) | Bearer credential is used and returns `ACCESS_DENIED`, the guild is unavailable and returns `MISSING_ACCESS`, or the caller is not a member, lacks `MANAGE_ROLES`, or does not outrank a named role, each returning `MISSING_PERMISSIONS`| | 404 | [error response](/http-api/#error-response) | Guild does not exist and returns `UNKNOWN_GUILD`| | 423 | [error response](/http-api/#error-response) | Another hoist position write holds the guild lock, returning `GENERAL_ERROR` with `Retry-After: 2` | ### Side effects -The operation creates one [ROLE_UPDATE](/http-api/guild-audit-logs/#audit-actions) guild audit log entry for each named role whose hoist position changed and emits one [Guild Role Update Bulk](/gateway/events/#guild-role-update-bulk) with the changed roles to every session that can see the guild. Reasserting the current positions changes nothing, creates no audit entry, and emits no Dispatch. +The operation creates one [ROLE_UPDATE](/http-api/guild-audit-logs/#audit-actions) guild audit log entry for each named role whose hoist position changed. It emits one [Guild Role Update Bulk](/gateway/events/#guild-role-update-bulk) with the changed roles to every session that can see the guild. Setting the current positions again changes nothing, creates no audit entry, and emits no Dispatch. ### Rate limit @@ -495,14 +495,14 @@ The request has no body. | Status | Body | Condition | | --- | --- | --- | | 204 | empty | Hoist positions were cleared, or no role had one| -| 400 | [error response](/http-api/#error-response) | The caller cannot exercise `MANAGE_ROLES` because of the guild MFA level and the request returns `TWO_FACTOR_REQUIRED`| +| 400 | [error response](/http-api/#error-response) | The caller cannot use `MANAGE_ROLES` because of the guild MFA level and the request returns `TWO_FACTOR_REQUIRED`| | 403 | [error response](/http-api/#error-response) | Bearer credential is used and returns `ACCESS_DENIED`, the guild is unavailable and returns `MISSING_ACCESS`, or the caller is not a member or lacks `MANAGE_ROLES`, each returning `MISSING_PERMISSIONS`| | 404 | [error response](/http-api/#error-response) | Guild does not exist and returns `UNKNOWN_GUILD`| | 423 | [error response](/http-api/#error-response) | Another hoist position write holds the guild lock, returning `GENERAL_ERROR` with `Retry-After: 2` | ### Side effects -The operation creates one [ROLE_UPDATE](/http-api/guild-audit-logs/#audit-actions) guild audit log entry for each role whose hoist position is cleared and emits one [Guild Role Update Bulk](/gateway/events/#guild-role-update-bulk) with those roles to every session that can see the guild. When no role held a hoist position, no audit entry is created and no Dispatch is emitted. +The operation creates one [ROLE_UPDATE](/http-api/guild-audit-logs/#audit-actions) guild audit log entry for each role whose hoist position is cleared. It emits one [Guild Role Update Bulk](/gateway/events/#guild-role-update-bulk) with those roles to every session that can see the guild. When no role held a hoist position, no audit entry is created and no Dispatch is emitted. ### Rate limit @@ -516,7 +516,7 @@ Deletes one role and returns 204 with an empty body. Requires [MANAGE_ROLES](#pe ### Limitations -- A caller who is not the guild owner additionally requires [hierarchy authority](#role-hierarchy) over the target role. +- A caller who is not the guild owner also requires [hierarchy authority](#role-hierarchy) over the target role. - The everyone role cannot be deleted. ### Path parameters @@ -533,7 +533,7 @@ The request has no body. | Status | Body | Condition | | --- | --- | --- | | 204 | empty | Role was deleted | -| 400 | [error response](/http-api/#error-response) | The caller cannot exercise `MANAGE_ROLES` because of the guild MFA level and the request returns `TWO_FACTOR_REQUIRED` | +| 400 | [error response](/http-api/#error-response) | The caller cannot use `MANAGE_ROLES` because of the guild MFA level and the request returns `TWO_FACTOR_REQUIRED` | | 403 | [error response](/http-api/#error-response) | Bearer credential is used and returns `ACCESS_DENIED`, the guild is unavailable and returns `MISSING_ACCESS`, or the caller is not a member, lacks `MANAGE_ROLES`, or does not outrank the target role, each returning `MISSING_PERMISSIONS` | | 404 | [error response](/http-api/#error-response) | Guild does not exist and returns `UNKNOWN_GUILD`, or the role does not exist or is the everyone role, each returning `UNKNOWN_ROLE` | @@ -543,7 +543,7 @@ A deleted role cannot be restored, and its permissions, colour, and hoist state ### Side effects -The operation removes the role from every member that held it, removes the role record, frees the guild role slot it occupied, and creates a [ROLE_DELETE](/http-api/guild-audit-logs/#audit-actions) guild audit log entry with the supplied audit reason. It emits [Guild Role Delete](/gateway/events/#guild-role-delete) to every session that can see the guild. Subsequent member reads and [Guild Member Update](/gateway/events/#guild-member-update) payloads reflect the removed role. +The operation removes the role from every member that held it, removes the role record, and frees the guild role slot it occupied. It creates a [ROLE_DELETE](/http-api/guild-audit-logs/#audit-actions) guild audit log entry with the supplied audit reason and emits [Guild Role Delete](/gateway/events/#guild-role-delete) to every session that can see the guild. Subsequent member reads and [Guild Member Update](/gateway/events/#guild-member-update) payloads reflect the removed role. ### Rate limit diff --git a/fluxer_docs/src/content/docs/http-api/premium.mdx b/fluxer_docs/src/content/docs/http-api/premium.mdx index 454d6dfc8..484cbde68 100644 --- a/fluxer_docs/src/content/docs/http-api/premium.mdx +++ b/fluxer_docs/src/content/docs/http-api/premium.mdx @@ -45,7 +45,7 @@ Every route except [get price IDs](#get-price-ids) is user-only. A [self-hosted ## Display currencies -Every amount field is expressed in the minor unit of its own currency field. Two amounts in one object can have different currencies, so read each amount against the currency field named in that field's description. +Every amount field is in the minor unit of the currency field its description names, and two amounts in one object can have different currencies. | Value | Description | | --- | --- | @@ -106,8 +106,8 @@ The price the account is billed against, and the current list price for the same | Field | Type | Description | | --- | --- | --- | -| price_id1 | string | The price the account is currently billed against | -| amount_minor | integer | The amount actually charged, in the minor unit of `currency` | +| price_id1 | string | The price the account is billed against | +| amount_minor | integer | The amount charged, in the minor unit of `currency` | | currency2 | string | [Display currency](#display-currencies) of the charged amount | | billing_cycle | string | [Billing cycle](#billing-cycles) of the active subscription | | is_grandfathered3 | boolean | Whether the account is billed against a price other than the current list price | @@ -192,7 +192,7 @@ The complete value is null in each of these cases. ## Premium state object -The account's entitlement, the premium checks that actually apply to it, and the billing and pricing data behind both. [Get premium state](#get-premium-state) and [set premium perks disabled](#set-premium-perks-disabled) both return it. +The account's entitlement, the premium checks that apply to it, and the billing and pricing data behind both. [Get premium state](#get-premium-state) and [set premium perks disabled](#set-premium-perks-disabled) both return it. A client gates a premium feature on `effective.is_premium`. @@ -220,7 +220,7 @@ The entitlement the account holds from billing alone. Nothing here reacts to the | premium_billing_cycle | ?string | [Billing cycle](#billing-cycles), or null when no recurring cycle is known | | premium_lifetime_sequence3 | ?integer | The Visionary sequence number, or null without lifetime entitlement | | premium_grace_ends_at | ?ISO8601 timestamp | The time the post-cancellation grace access ends, or null when no grace is active | -| has_active_paid_premium4 | boolean | Whether paid premium access is currently active before local disabling | +| has_active_paid_premium4 | boolean | Whether paid premium access is active before local disabling | | is_visionary | boolean | Whether the entitlement is lifetime Visionary access | | has_ever_purchased | boolean | Whether the account has ever completed a premium purchase | @@ -277,7 +277,7 @@ The effective state decides whether premium features are available. It differs f 3 Copied from the [actual premium state](#actual-premium-state-object) unchanged, so the value does not react to `is_premium` -4 Reflects the corresponding [premium flag](/admin-api/users/#premium-flags) on the account +4 Reflects the matching [premium flag](/admin-api/users/#premium-flags) on the account 5 `is_premium` follows this only while the instance [premium mode](/admin-api/instance/#premium-modes) is `everyone` @@ -287,13 +287,13 @@ The effective state decides whether premium features are available. It differs f The account's billing data behind a premium screen. -`stripe_customer_id`, `current_subscription_price`, `subscription`, `invoices` and `payment_methods` are read from the local mirror of the payment provider, and Fluxer repairs the payment method mirror while building the response. `refund_eligibility` is computed from the mirrored invoices. `pending_subscription_change` is read live from the payment provider on every request. +`stripe_customer_id`, `current_subscription_price`, `subscription`, `invoices` and `payment_methods` are read from the local mirror of the payment provider. Fluxer repairs the payment method mirror while building the response. `refund_eligibility` is computed from the mirrored invoices. `pending_subscription_change` is read live from the payment provider on every request. ### Structure | Field | Type | Description | | --- | --- | --- | -| stripe_customer_id | ?string | The payment provider customer ID, or null when none was ever provisioned | +| stripe_customer_id | ?string | The payment provider customer ID, or null when none was ever created | | current_subscription_price | ?[current subscription price](#current-subscription-price-object) object | The price the account is billed against | | pending_subscription_change | ?[pending subscription change](#pending-subscription-change-object) object | The scheduled billing cycle change | | subscription | ?[billing subscription](#billing-subscription-object) object | The most relevant mirrored subscription | @@ -302,11 +302,11 @@ The account's billing data behind a premium screen. | payment_methods3 | array[[billing payment method](#billing-payment-method-object)] | The stored payment methods | | refund_eligibility4 | [refund eligibility](/http-api/billing/#refund-eligibility-object) object | The self-service refund state for the account | -1 At most 12 entries, ordered by provider creation time descending. Fluxer gathers invoices across every payment provider customer the account owns and deduplicates them by provider identifier before the cut +1 At most 12 entries, ordered by provider creation time descending. Fluxer collects invoices across every payment provider customer the account owns and deduplicates them by provider identifier before the cut 2 True when the account has more invoices than the 12 returned. No route pages past the first 12, so use [create customer portal](#create-customer-portal) for the rest -3 The default payment method sorts first, then the remainder by provider creation time descending +3 The default payment method sorts first, then the rest by provider creation time descending 4 Computed from the mirrored invoices, and it skips an invoice that already has a pending, succeeded or action-required refund. It can therefore differ from [get refund eligibility](/http-api/billing/#get-refund-eligibility) @@ -482,7 +482,7 @@ The deployment must configure a complete recurring pair and a complete gift pair Returns the [premium state](#premium-state-object) object for the authenticated account. -The route answers from mirrored billing data, so it works when the payment provider is unconfigured. When the provider is configured, the operation also repairs missing payment method mirror rows for each of the account's customers before it builds the response. A repair failure is logged and absorbed. +The route answers from mirrored billing data, so it works when the payment provider is unconfigured. With a configured provider it also repairs missing payment method mirror rows for the account's customers before building the response. A repair failure is logged and absorbed. :::note[One read serves the whole billing screen] `billing.current_subscription_price` and `billing.refund_eligibility` cover the same ground as [get current subscription price](#get-current-subscription-price) and [get refund eligibility](/http-api/billing/#get-refund-eligibility), and `billing.pending_subscription_change` has no route of its own. @@ -571,7 +571,7 @@ When the requested value differs from the current one, the operation updates the Creates a billing portal session for the authenticated account and returns a [redirect URL](/http-api/billing/#redirect-url-object) object. The returned URL is the only handle on the session. -An account that has never had a payment provider customer provisioned receives 400 `STRIPE_NO_PURCHASE_HISTORY`. A deployment that configures no payment provider receives 400 `STRIPE_PAYMENT_NOT_AVAILABLE`, and a provider call that fails or answers without a URL receives 400 `STRIPE_ERROR`. +An account that never had a payment provider customer receives 400 `STRIPE_NO_PURCHASE_HISTORY`. A deployment that configures no payment provider receives 400 `STRIPE_PAYMENT_NOT_AVAILABLE`, and a provider call that fails or answers without a URL receives 400 `STRIPE_ERROR`. ### Response @@ -593,7 +593,7 @@ The operation creates an externally hosted billing portal session that returns t -Ends an active post-cancellation grace period immediately. Emits a [User Update](/gateway/events/#user-update) Gateway event when a grace period was actually ended. +Ends an active post-cancellation grace period immediately. Emits a [User Update](/gateway/events/#user-update) Gateway event when a grace period was ended. The operation is idempotent and reports success without changing anything when no grace deadline is recorded, when the account holds a lifetime entitlement, and when the account's premium end has not yet passed. @@ -654,7 +654,7 @@ A deployment that configures no payment provider receives 400 `STRIPE_PAYMENT_NO Fluxer sets the subscription to cancel at the current period end, sets the account's cancellation flag, and refreshes the mirrored subscription row. Every account session receives [User Update](/gateway/events/#user-update). The ordinary path writes no premium end, so the recorded end is whatever the account already has. -A subscription that has a schedule discards its pending billing cycle change and ends in cancellation at the current period end, and Fluxer then rewrites the premium end from the refreshed subscription. Premium access continues through the recorded end, and [receive Stripe webhook](/http-api/billing/#receive-stripe-webhook) later applies the final downgrade. +A subscription that has a schedule discards its pending billing cycle change and ends in cancellation at the current period end. Fluxer then rewrites the premium end from the refreshed subscription. Premium access continues through the recorded end, and [receive Stripe webhook](/http-api/billing/#receive-stripe-webhook) later applies the final downgrade. ### Rate limit @@ -694,7 +694,7 @@ Moves the active subscription between the monthly and yearly billing cycles, eit A deployment that configures no payment provider receives 400 `STRIPE_PAYMENT_NOT_AVAILABLE`, an account with no stored subscription receives 400 `STRIPE_NO_ACTIVE_SUBSCRIPTION`, and a deployment with no configured price for the target cycle in the subscription's currency receives 400 `STRIPE_INVALID_PRODUCT_CONFIGURATION`. A subscription with no recurring primary item, an unsupported recurring interval, a period end change with no future period end, and any other provider failure are all reported as 400 `STRIPE_ERROR`. -The operation reports success without changing anything when the subscription already bills on the requested cycle. When a future schedule phase already has the target price it sends nothing further to the payment provider and writes only the account's cancellation flag, premium end and stored payment provider customer identifier. +The operation reports success without changing anything when the subscription already bills on the requested cycle. When a future schedule phase already has the target price, the operation sends nothing further to the payment provider and writes only the account's cancellation flag, premium end and stored payment provider customer identifier. :::caution[An immediate change invoices at once] An immediate change swaps the subscription item to the target price and always invoices the proration, so the account is charged or credited. An incomplete payment fails the request without leaving the subscription in the `incomplete` status. @@ -735,7 +735,7 @@ A scheduled change becomes visible as `billing.pending_subscription_change` in [ -Cancels a scheduled billing cycle change and returns 204 with an empty body. Emits a [User Update](/gateway/events/#user-update) Gateway event when a scheduled change was actually cancelled. +Cancels a scheduled billing cycle change and returns 204 with an empty body. Emits a [User Update](/gateway/events/#user-update) Gateway event when a scheduled change was cancelled. A deployment that configures no payment provider receives 400 `STRIPE_PAYMENT_NOT_AVAILABLE`, an account with no stored subscription receives 400 `STRIPE_NO_ACTIVE_SUBSCRIPTION`, and a provider failure receives 400 `STRIPE_ERROR`. @@ -767,7 +767,7 @@ The route requires a lifetime entitlement, so an account whose premium type is n A join emits [Guild Create](/gateway/events/#guild-create), [Guild Member Add](/gateway/events/#guild-member-add) and, when the guild has a system channel and does not suppress join notifications, [Message Create](/gateway/events/#message-create). The role grant that follows emits [Guild Member Update](/gateway/events/#guild-member-update). An account that already belongs to the guild produces that Dispatch alone. -The operation is idempotent. For an account that already belongs to the guild, Fluxer reasserts the Visionary role and changes nothing else. The join bypasses the guild ban check and the join risk gate, so a banned account is added anyway. +The operation is idempotent. For an account that already belongs to the guild, Fluxer grants the Visionary role again and changes nothing else. The join bypasses the guild ban check and the join risk gate, so a banned account is added anyway. Both size ceilings still apply. An account already at its maximum number of guilds receives 400 `MAX_GUILDS`, and a Visionary guild at its member ceiling receives 400 `MAX_GUILD_MEMBERS`. Neither check runs for an account that already belongs to the guild. diff --git a/fluxer_docs/src/content/docs/http-api/read-states.mdx b/fluxer_docs/src/content/docs/http-api/read-states.mdx index f48963a91..6b71e385f 100644 --- a/fluxer_docs/src/content/docs/http-api/read-states.mdx +++ b/fluxer_docs/src/content/docs/http-api/read-states.mdx @@ -24,13 +24,13 @@ An account holds at most one read state per channel, keyed by the account and th | last_pin_timestamp2 | ?ISO8601 timestamp | The time of the last acknowledged pin, or null when pins have never been acknowledged | | version?3 | string | The read state version as a canonical decimal unsigned 64-bit string | -1 The value the last acknowledgement stored, incremented by the server as new mentions arrive. It is never recomputed from message history +1 The value the last acknowledgement stored, incremented by the server as new mentions arrive and never recomputed from message history 2 Neither operation on this page writes it, so an acknowledged pin timestamp survives every message acknowledgement 3 Present on every entry the API returns. The value is always `0` -An acknowledgement creates an entry. So does [Acknowledge pins](/http-api/messages/#acknowledge-pins), and so does the server when a mention arrives in a channel the account has no entry for. A pin acknowledgement writes only `last_pin_timestamp`, and the entry it creates reports `last_message_id` as null. The entry the server creates for a mention starts from the channel's own baseline watermark, which is the snowflake of the channel ID itself, so every message already in the channel stays unread. +An acknowledgement creates an entry. So does [Acknowledge pins](/http-api/messages/#acknowledge-pins), and so does the server when a mention arrives in a channel the account has no entry for. A pin acknowledgement writes only `last_pin_timestamp`, and the entry it creates reports `last_message_id` as null. The entry the server creates for a mention starts from the channel's own baseline watermark, the snowflake of the channel ID itself, so every message already in the channel stays unread. [Clear channel read state](/http-api/messages/#clear-channel-read-state) deletes the whole entry, so it drops the watermark and the mention count along with the pin timestamp. @@ -106,7 +106,7 @@ One entry in the body of [Mark channels as read](#mark-channels-as-read). It nam Applies 1 through 100 [read state acknowledgement objects](#read-state-acknowledgement-object) to the current account's aggregate and returns the resulting [read state](#read-state-object) entries. User-only. Emits one [Message ACK](/gateway/events/#message-ack) Gateway event to the caller's own sessions for each submitted entry. -The route resolves no channel and evaluates no permission. The write mutates only the caller's own read states. +The route resolves no channel and evaluates no permission. The write changes only the caller's own read states. ### JSON body @@ -128,7 +128,7 @@ The route resolves no channel and evaluates no permission. The write mutates onl When no entry sets `manual` and no entry has a `mention_count` above `0`, Fluxer applies the whole list through the batched path [Mark channels as read](#mark-channels-as-read) uses. ::: -A list with one manual entry or one positive mention count takes the entry-by-entry path in submitted order, and a later entry naming the same channel is evaluated against the earlier entry's result. +A list with one manual entry or one positive mention count takes the entry-by-entry path in submitted order. On that path, a later entry naming the same channel is evaluated against the earlier entry's result. ### Response diff --git a/fluxer_docs/src/content/docs/http-api/reports.mdx b/fluxer_docs/src/content/docs/http-api/reports.mdx index cbee8050e..9607e4862 100644 --- a/fluxer_docs/src/content/docs/http-api/reports.mdx +++ b/fluxer_docs/src/content/docs/http-api/reports.mdx @@ -6,7 +6,7 @@ description: Safety report submission, the category registries, and the DSA noti import RouteHeader from '@/components/RouteHeader.astro'; -A safety report tells moderators that a message, a user, or a guild broke the rules, and any user account can file one. A Digital Services Act notice needs no account, only a verified email address. The [Admin Reports API](/admin-api/reports/) covers reading, triaging, and closing a stored report. +A safety report tells moderators that a message, a user, or a guild broke the rules, and any user account can file one. Digital Services Act notices need no account, only a verified email address. The [Admin Reports API](/admin-api/reports/) covers reading, triaging, and closing a stored report. | Operation | Credential | Target | | --- | --- | --- | @@ -35,17 +35,17 @@ Every route bucket is keyed by the authenticated user ID when a credential resol The instance-wide content filter screens the JSON body of [Report message](#report-message), [Report user](#report-user), [Report guild](#report-guild), and [Create DSA report](#create-dsa-report) before the route runs. A string of at least 3 characters matching the instance phrase blocklist, or a URL matching the instance URL blocklist, is rejected with 403 `CONTENT_BLOCKED`. The `ticket`, `channel_id`, `message_id`, `user_id`, and `guild_id` members are exempt, and so is any value shorter than 3 characters. -:::note[The screen precedes the bucket and the credential check] -A blocked body returns `CONTENT_BLOCKED` rather than `RATE_LIMITED` or `UNAUTHORIZED`. [Send DSA report email](#send-dsa-report-email) and [Verify DSA report email](#verify-dsa-report-email) are exempt by path, so a submitted address is never scanned. +:::note[Screening precedes bucket and credential checks] +A blocked body returns `CONTENT_BLOCKED`, not `RATE_LIMITED` or `UNAUTHORIZED`. [Send DSA report email](#send-dsa-report-email) and [Verify DSA report email](#verify-dsa-report-email) are exempt by path, so a submitted address is never scanned. ::: ## Submission side effects -Every accepted submission writes one report and returns a [report object](#report-object). Submission emits no Gateway Dispatch. +Every accepted submission writes one report and returns it as a [report object](#report-object). Only [Report message](#report-message) refuses a repeat, returning 409 `CONFLICT` when the same reporter reports the same message twice. Every accepted [Report user](#report-user) and [Report guild](#report-guild) request writes a new report, and nothing deduplicates a repeat submission. Submission emits no Gateway Dispatch. ## Report object -The receipt Fluxer returns when it records a submission. +Fluxer returns this receipt when it records a submission. ### Structure @@ -301,9 +301,7 @@ Creates and returns a [report object](#report-object) for another user, optional ### Side effects -The operation writes one report recording the reporter's account ID and the email address on that account, the reported user's ID and avatar hash, and, when `guild_id` is supplied, the guild's ID, name, icon hash, NSFW flag, and content warning state. No message evidence is captured and no media bytes are copied. - -Every accepted request writes a new report, and nothing deduplicates a repeat submission. +The operation writes one report. It records the reporter's account ID and email address, and the reported user's ID and avatar hash. A supplied `guild_id` adds the guild's ID, name, icon hash, NSFW flag, and content warning state. No message evidence is captured and no media bytes are copied. ### Rate limit @@ -340,7 +338,7 @@ A reporter satisfying none of them returns 403 `CANNOT_REPORT_GUILD`. 1 Read only after the member check and the `DISCOVERABLE` check both fail -This route reads the bare code and accepts no invite URL. A code that resolves to no invite, or to an invite for another guild, returns 403 `CANNOT_REPORT_GUILD` rather than a 404. +This route reads the bare code and accepts no invite URL. A code that resolves to no invite, or to an invite for another guild, returns 403 `CANNOT_REPORT_GUILD`, not a 404. ### Response @@ -358,9 +356,7 @@ This route reads the bare code and accepts no invite URL. A code that resolves t ### Side effects -The operation writes one report recording the reporter's account ID and the email address on that account, the guild's ID, name, and icon hash, the guild's NSFW flag and content warning state, and the supplied invite code when one was sent. No message evidence is captured and no media bytes are copied. - -Every accepted request writes a new report, and nothing deduplicates a repeat submission. +The operation writes one report. It records the reporter's account ID and email address, the guild's ID, name, icon hash, NSFW flag, and content warning state, and the invite code when one was sent. No message evidence is captured and no media bytes are copied. ### Rate limit @@ -398,7 +394,7 @@ The operation stores one verification record for the normalised address. The rec Fluxer attempts delivery inside the request and discards the outcome. The operation creates no ticket and emits no Gateway Dispatch. :::caution[A 200 covers every delivery outcome] -The response is 200 when the provider accepts the message, when it refuses the message, when the address was previously recorded as a hard bounce, and when the instance has email delivery switched off entirely. +The response is 200 when the provider accepts the message and when it refuses it. It is 200 for an address recorded as a hard bounce, and on an instance with email delivery switched off. ::: ### Rate limit @@ -513,7 +509,7 @@ An unknown code returns 404 `UNKNOWN_INVITE`. A code that resolves to an invite ### Side effects -The operation resolves the target, deletes the ticket, and writes one report under a freshly generated [snowflake](/snowflakes/). A message notice captures the same conversation window as [Report message](#report-message), including the byte copy of every attachment, and applies no permission filtering to it. A user notice records the reported user's ID and avatar hash and nothing else about the target. A guild notice records the guild's ID, name, icon hash, NSFW flag, content warning state, and the sanitised invite code when one was supplied. +The operation resolves the target, deletes the ticket, and writes one report under a newly generated [snowflake](/snowflakes/). A message notice captures the same conversation window as [Report message](#report-message), including the byte copy of every attachment, and applies no permission filtering to it. A user notice records the reported user's ID and avatar hash and nothing else about the target. A guild notice records the guild's ID, name, icon hash, NSFW flag, content warning state, and the sanitised invite code when one was supplied. Every notice also records `additional_info`, the reporter's legal name, country of residence, and the verified address behind the ticket. A notice has no account ID even when the request presented a credential, and it discards `reporter_fluxer_tag`. diff --git a/fluxer_docs/src/content/docs/http-api/search.mdx b/fluxer_docs/src/content/docs/http-api/search.mdx index c2bb5cfe6..dd9051115 100644 --- a/fluxer_docs/src/content/docs/http-api/search.mdx +++ b/fluxer_docs/src/content/docs/http-api/search.mdx @@ -31,7 +31,7 @@ The body a completed search returns, holding one page of matching messages. 2 Contains one entry for each distinct channel represented in `messages`, and never contains a channel with no returned message -3 Counted over the whole resolved scope. A single channel context without [READ_MESSAGE_HISTORY](/http-api/permissions/) reports the number actually returned +3 Counted over the whole resolved scope. A single channel context without [READ_MESSAGE_HISTORY](/http-api/permissions/) reports the number returned 4 Present only when the backend produced a cursor. A single channel context, a reconciled offset page, and a Meilisearch instance each return none @@ -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 enqueues the missing indexing work, and a later identical request returns ordinary results once that work completes. +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. ### Example @@ -71,8 +71,8 @@ Tell the two bodies apart by the presence of `indexing`, which the result object } ``` -:::caution[One stale channel withholds a whole multi-guild search] -The `all_guilds`, `all`, and `open_dms_and_all_guilds` scopes check the index of every guild channel the caller can read, so a request narrowed to particular channels still receives the search indexing object when any other readable guild channel is unindexed. +:::caution[One stale channel blocks a whole multi-guild search] +The `all_guilds`, `all`, and `open_dms_and_all_guilds` scopes check the index of every guild channel the caller can read. A request narrowed to particular channels still receives the search indexing object when any other readable guild channel is unindexed. ::: ## Search scopes @@ -80,11 +80,11 @@ The `all_guilds`, `all`, and `open_dms_and_all_guilds` scopes check the index of | Value | Description | | --- | --- | | current | The single guild named by context_guild_id, or the single channel named by context_channel_id when no guild context is supplied | -| open_dms1 | Every direct message and group direct message channel the caller currently has open | +| open_dms1 | Every direct message and group direct message channel the caller has open | | all_dms | Every direct message and group direct message channel the caller has ever been a recipient of | -| all_guilds | Every guild the caller is currently a member of | -| all | Every guild the caller is currently a member of together with the all_dms channel set | -| open_dms_and_all_guilds | Every guild the caller is currently a member of together with the open_dms channel set | +| all_guilds | Every guild the caller is a member of | +| all | Every guild the caller is a member of together with the all_dms channel set | +| open_dms_and_all_guilds | Every guild the caller is a member of together with the open_dms channel set | 1 A private channel is open when the caller has not closed it, and a channel named by `context_channel_id` is added to the open set for this request even when it is closed @@ -170,7 +170,7 @@ Searches indexed messages in the resolved scope. Returns a [message search resul - An age-restricted guild or single channel context requires an account old enough for age-restricted content, and otherwise returns 403 `NSFW_CONTENT_AGE_RESTRICTED`. - An instance with no search backend configured returns 403 `FEATURE_TEMPORARILY_DISABLED`. -Every scope that touches a guild channel calls the main Gateway. A Gateway call that times out returns 504 `GATEWAY_TIMEOUT`, a reply that cannot be interpreted returns 502 `BAD_GATEWAY`, and a Gateway that is overloaded or unreachable returns 503 `SERVICE_UNAVAILABLE`. +Every scope that can include a guild channel calls the main Gateway. A Gateway call that times out returns 504 `GATEWAY_TIMEOUT`, a reply that cannot be interpreted returns 502 `BAD_GATEWAY`, and a Gateway that is overloaded or unreachable returns 503 `SERVICE_UNAVAILABLE`. ### JSON body @@ -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, while 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 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 `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`. @@ -268,11 +268,11 @@ A guild whose NSFW level is age restricted refuses a caller who is not old enoug A channel context searches only that channel and ignores `channel_ids`, `cursor`, and `include_nsfw`. When the caller cannot read the channel's history and the guild defines no message history cutoff, the search returns an empty result. -The `all_guilds`, `all`, and `open_dms_and_all_guilds` scopes include only guild channels the caller can currently view and read history in. The `open_dms` and `open_dms_and_all_guilds` scopes include the caller's open private channels. The `all_dms` and `all` scopes include every private channel available to the caller unless `channel_ids` narrows the set. A scope that resolves to no channel returns an empty result with a `total` of `0`. +The `all_guilds`, `all`, and `open_dms_and_all_guilds` scopes include only guild channels the caller can view and read history in. The `open_dms` and `open_dms_and_all_guilds` scopes include the caller's open private channels. The `all_dms` and `all` scopes include every private channel available to the caller unless `channel_ids` narrows the set. A scope that resolves to no channel returns an empty result with a `total` of `0`. Searchable text is the message content plus the collected embed text, which covers embed titles, descriptions, URLs, author names, provider names, footer text, and field names and values, including nested embeds. -Content moderation scans every string in the body whose field name does not end in `_id` or `_ids`, so a value of at least three characters that matches the instance phrase blocklist or has a blocked URL returns 403 `CONTENT_BLOCKED`. +Content moderation scans every string in the body whose field name does not end in `_id` or `_ids`. A scanned value of at least three characters that matches the instance phrase blocklist or has a blocked URL returns 403 `CONTENT_BLOCKED`. ### Response @@ -292,7 +292,7 @@ Content moderation scans every string in the body whose field name does not end ### Side effects -When a channel in the resolved scope has never been indexed or its index is stale, the operation enqueues an indexing job for it and returns the [search indexing](#search-indexing-object) object. +When a channel in the resolved scope has never been indexed or its index is stale, the operation queues an indexing job for it and returns the [search indexing](#search-indexing-object) object. ### Rate limit diff --git a/fluxer_docs/src/content/docs/http-api/streams.mdx b/fluxer_docs/src/content/docs/http-api/streams.mdx index a8ffd67a8..2e42b1db1 100644 --- a/fluxer_docs/src/content/docs/http-api/streams.mdx +++ b/fluxer_docs/src/content/docs/http-api/streams.mdx @@ -16,7 +16,7 @@ Starting and stopping a stream is a main Gateway operation. Going live publishes ## Stream key -A stream key travels as one path segment of 1 through 256 characters. It has exactly three colon-separated segments. The form is `{guild_id}:{channel_id}:{connection_id}` for a guild voice stream and `dm:{channel_id}:{connection_id}` for a private call stream. +A stream key is one path segment of 1 through 256 characters. It has exactly three colon-separated segments. The form is `{guild_id}:{channel_id}:{connection_id}` for a guild voice stream and `dm:{channel_id}:{connection_id}` for a private call stream. | Field | Type | Description | | --- | --- | --- | @@ -44,7 +44,7 @@ That comparison is the last check of the read boundary. Channel lookup, the scop Two boundaries apply across this page. -Read access matches [Get channel](/http-api/channels/#get-channel) for the [resolved channel](#stream-key). A guild channel additionally 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. +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. Mutation access requires everything read access does. A guild channel also requires the [STREAM](/http-api/permissions/) permission. The caller must hold a voice state in exactly the resolved channel and the connection identifier the key names. Fluxer rejects a caller that holds no such voice state with 403 `ACCESS_DENIED`, even when it owns the channel. @@ -52,7 +52,7 @@ Mutation access requires everything read access does. A guild channel also requi A caller can record a region and upload a preview for any voice connection it holds, whether or not it is streaming. ::: -A guild channel resolves before its guild does. A channel row that outlives its guild returns 404 `UNKNOWN_GUILD`. Where the guild record exists and Fluxer cannot resolve the membership state, the route returns 403 `ACCESS_DENIED`, and a caller that is not a member or that lacks [VIEW_CHANNEL](/http-api/permissions/) returns 403 `MISSING_PERMISSIONS`. An unsatisfied age restriction on a guild text, voice, or link channel returns 403 `NSFW_CONTENT_AGE_RESTRICTED`. +Fluxer resolves a guild channel before its guild. A channel row that outlives its guild returns 404 `UNKNOWN_GUILD`. Where the guild record exists and Fluxer cannot resolve the membership state, the route returns 403 `ACCESS_DENIED`, and a caller that is not a member or that lacks [VIEW_CHANNEL](/http-api/permissions/) returns 403 `MISSING_PERMISSIONS`. An unsatisfied age restriction on a guild text, voice, or link channel returns 403 `NSFW_CONTENT_AGE_RESTRICTED`. A private channel whose recipient set does not contain the caller returns 404 `UNKNOWN_CHANNEL`. An absent channel returns the same code, so recipient membership is never distinguishable from private channel existence. @@ -206,7 +206,7 @@ When the image should travel out of band, use [Create stream preview upload URL] Fluxer stores the accepted `content_type` verbatim and returns it from [Get stream preview](#get-stream-preview). When the field is absent, the stored media type is `image/jpeg`. -A `thumbnail` that is not canonical base64 returns 400 `INVALID_STREAM_THUMBNAIL_PAYLOAD`. Fluxer re-encodes the decoded bytes and rejects the value when the result is not what the client sent. The 2000000 character ceiling admits 1500000 decoded bytes, so an oversized JPEG reaches the byte check and returns `FILE_SIZE_TOO_LARGE`. The format check runs before the size check. An oversized payload that is not a JPEG returns `PREVIEW_MUST_BE_JPEG`. +A `thumbnail` that is not [canonical base64](/topics/uploads/#stream-previews) returns 400 `INVALID_STREAM_THUMBNAIL_PAYLOAD`. Fluxer re-encodes the decoded bytes and rejects the value when the result is not what the client sent. The 2000000 character ceiling admits 1500000 decoded bytes, so an oversized JPEG reaches the byte check and returns `FILE_SIZE_TOO_LARGE`. The format check runs before the size check. An oversized payload that is not a JPEG returns `PREVIEW_MUST_BE_JPEG`. ### Response diff --git a/fluxer_docs/src/content/docs/http-api/themes.mdx b/fluxer_docs/src/content/docs/http-api/themes.mdx index 18a266283..32b5431fd 100644 --- a/fluxer_docs/src/content/docs/http-api/themes.mdx +++ b/fluxer_docs/src/content/docs/http-api/themes.mdx @@ -37,13 +37,13 @@ The UTF-8 encoding of a submitted document cannot exceed 8388608 bytes, and the 3 Measured in characters of the submitted string -An account holds no theme quota. The route bucket is the only bound on how many themes an account creates, and no route here consults an [instance limit key](/http-api/instance/#limit-keys). +An account holds no theme quota. The route bucket is the only bound on how many themes an account creates, and no route here checks an [instance limit key](/http-api/instance/#limit-keys). ## Content screening The instance-wide content filter screens the submitted document before the route runs. It checks a `css` value of at least 3 characters against the instance phrase blocklist and the instance URL blocklist. A match on either returns 403 `CONTENT_BLOCKED`. A shorter value is never screened. -Fluxer scans a body under every content type except `multipart/form-data` and `application/x-www-form-urlencoded`. The filter also skips a body that does not parse as JSON. The route parses the body from the raw request text and reads no content type, so a JSON document sent under either form content type is stored without passing the blocklists. +Fluxer scans a body under every content type except `multipart/form-data` and `application/x-www-form-urlencoded`. The filter also skips a body that does not parse as JSON. The route parses the body from the raw request text and reads no content type, so a JSON document sent under either [form content type](/http-api/#request-body-formats) is stored without passing the blocklists. The screen precedes the rate limit bucket, the credential check, and the request schema, so a blocked document returns `CONTENT_BLOCKED` even when the request has no credential. diff --git a/fluxer_docs/src/content/docs/http-api/unfurl.mdx b/fluxer_docs/src/content/docs/http-api/unfurl.mdx index 08764f323..84da0cf3c 100644 --- a/fluxer_docs/src/content/docs/http-api/unfurl.mdx +++ b/fluxer_docs/src/content/docs/http-api/unfurl.mdx @@ -12,7 +12,7 @@ The route is user-only. Fluxer rejects a bot token and an OAuth2 bearer credenti ## Site resolvers -Fluxer walks a fixed chain of resolvers in the order below. A resolver runs only when the URL matches it, and the first resolver that produces at least one embed wins. Fluxer skips a resolver that produces nothing and a resolver whose own fetch fails, then tries the next match. An unavailable provider therefore degrades to the generic resolver. +Fluxer walks a fixed chain of resolvers in the order below. A resolver runs only when the URL matches it, and the first resolver that produces at least one embed wins. Fluxer skips a resolver that produces nothing and a resolver whose own fetch fails, then tries the next match. An unavailable provider therefore falls back to the generic resolver. | Resolver | Matched URL | Source | | --- | --- | --- | @@ -102,7 +102,7 @@ The route always asks for the explicit media classifier, so a media object the c The message path asks the same way, except in a channel that permits explicit media, where the classifier is disabled for every resolver. The same URL can resolve to differently flagged media there. -The message path also extracts its candidate URLs from message text. It drops anything inside a code span or code fence and anything wrapped in angle brackets. It skips a Fluxer invite, the invite host, the gift host, the web application host, the marketing host under `/channels/` and `/theme/`, and the instance's configured `unfurl_ignored_hosts`. It then unfurls at most the first five surviving URLs. One message has at most 10 embeds, and Fluxer scans every resolved URL and media URL for banned content before it attaches an embed. A scan that blocks the content deletes the message. +That path also extracts its candidate URLs from message text. It drops anything inside a code span or code fence and anything wrapped in angle brackets. It skips a Fluxer invite, the invite host, the gift host, the web application host, the marketing host under `/channels/` and `/theme/`, and the instance's configured `unfurl_ignored_hosts`. It then unfurls at most the first five surviving URLs. One message has at most 10 embeds, and Fluxer scans every resolved URL and media URL for banned content before it attaches an embed. A scan that blocks the content deletes the message. :::note[This route applies none of the message-path filtering] It resolves precisely the URL in the request body and returns the complete resolver output, with no URL extraction, no embed cap, and no banned-content scan. @@ -126,4 +126,4 @@ The operation fetches the supplied URL, can call a provider API for a matched si ### Rate limit -10 requests per minute for each authenticated user, on the `unfurl:debug` bucket. Fluxer charges the bucket before it checks the credential, and keys it on the caller's address when no valid credential is present. An unauthenticated caller therefore consumes it and receives 429 `RATE_LIMITED` rather than 401 once it is exhausted. +10 requests per minute for each authenticated user, on the `unfurl:debug` bucket. Fluxer charges the bucket before it checks the credential, and keys it on the caller's address when no valid credential is present. An unauthenticated caller therefore consumes it and receives 429 `RATE_LIMITED` once it is exhausted, not 401. diff --git a/fluxer_docs/src/content/docs/http-api/users.mdx b/fluxer_docs/src/content/docs/http-api/users.mdx index 1caadaa44..ccbf2b814 100644 --- a/fluxer_docs/src/content/docs/http-api/users.mdx +++ b/fluxer_docs/src/content/docs/http-api/users.mdx @@ -75,7 +75,7 @@ A system account never receives the deleted representation. An account only sche ## Reply mention preferences -The account-wide preference applied when another account replies to one of this account's messages. `mention_flags` is exactly one of the values below, and it is omitted from a serialised user while the stored value is `NO_PREFERENCE`. +The account-wide preference applied when another account replies to one of this account's messages. `mention_flags` is exactly one of the values below. It is omitted from a serialised user while the stored value is `NO_PREFERENCE`. | Value | Name | Description | | --- | --- | --- | @@ -87,7 +87,7 @@ A guild member has the same enumeration in its own `mention_flags`, where `NO_PR ## User object -The private representation of the current account, returned by [Get current user](/http-api/users/current-user/#get-current-user) and by every operation that mutates the account. It extends the [partial user object](#partial-user-object). +The private representation of the current account, returned by [Get current user](/http-api/users/current-user/#get-current-user) and by every operation that changes the account. It extends the [partial user object](#partial-user-object). ### Structure @@ -128,10 +128,10 @@ The private representation of the current account, returned by [Get current user | premium_lifetime_sequence3 | ?integer | The lifetime premium sequence, or null | | premium_grace_ends_at12 | ?ISO8601 timestamp | The end of the post-cancellation grace interval, or null when the account is not in grace | | premium_discriminator13 | boolean | Whether the current discriminator was selected under a premium entitlement | -| premium_badge_hidden3 | boolean | Whether the premium badge is withheld from the public profile | +| premium_badge_hidden3 | boolean | Whether the premium badge is hidden from the public profile | | premium_badge_masked3 | boolean | Whether a lifetime badge is presented as an ordinary subscription badge | -| premium_badge_timestamp_hidden3 | boolean | Whether the premium activation time is withheld from the public profile | -| premium_badge_sequence_hidden3 | boolean | Whether the lifetime sequence is withheld from the public profile | +| premium_badge_timestamp_hidden3 | boolean | Whether the premium activation time is hidden from the public profile | +| premium_badge_sequence_hidden3 | boolean | Whether the lifetime sequence is hidden from the public profile | | premium_purchase_disabled3 | boolean | Whether premium purchasing is disabled for the account | | premium_enabled_override3 | boolean | Whether an administrative override grants premium entitlements | | premium_perks_disabled3 | boolean | Whether premium entitlements are suspended for the account | @@ -151,11 +151,11 @@ The private representation of the current account, returned by [Get current user 1 An animated avatar hash has the `a_` prefix, and the prefix is removed while the account has no animated avatar entitlement -2 The array is empty for an account with no Admin access, and it is replaced wholesale by [Set user ACLs](/admin-api/users/#set-user-acls). It is independent of `is_staff` +2 The array is empty for an account with no Admin access, and it is replaced in full by [Set user ACLs](/admin-api/users/#set-user-acls). It is independent of `is_staff` 3 The field is neutralised for an OAuth2 bearer credential as described below -4 The value `premium` is present exactly when premium entitlements are currently active, regardless of the stored trait set, and every other stored trait is returned sorted +4 The value `premium` is present exactly when premium entitlements are active, regardless of the stored trait set, and every other stored trait is returned sorted 5 For an OAuth2 bearer credential without the `email` [scope](/http-api/oauth2/#oauth2-scopes), `email` is `null` @@ -163,11 +163,11 @@ The private representation of the current account, returned by [Get current user 7 The pair is present only while the account has the staff flag, and it is absent for every other account -8 The banner hash is withheld entirely while the account lacks the animated banner entitlement, which gates every profile banner +8 The banner hash is not returned at all while the account lacks the animated banner entitlement, which every profile banner requires 9 `mfa_enabled` is true exactly when at least one authenticator is configured. An account with no authenticator omits `authenticator_types` -10 The value is forced to `0` and `premium_since` is forced to `null` while premium entitlements are not currently active +10 The value is forced to `0` and `premium_since` is forced to `null` while premium entitlements are not active 11 `premium_until` is the later of the subscription end and any stacked gift extension. Fluxer reports it even when premium entitlements are no longer active @@ -183,7 +183,7 @@ The private representation of the current account, returned by [Get current user 17 A bot account is always permitted, and a user account is permitted only when its recorded date of birth places it at 18 years or older -18 The value is false while premium entitlements are not currently active, even when a dismissal was recorded earlier +18 The value is false while premium entitlements are not active, even when a dismissal was recorded earlier 19 Both values derive from the difference between the stored gift inventory sequence and the sequence the account has acknowledged, and they report `false` and `0` when no gift has ever been recorded @@ -285,7 +285,7 @@ Update semantics belong to the [user settings update object](/http-api/users/set | animate_emoji | boolean | Whether custom emoji animate | | animate_stickers | integer | [Sticker animation setting](#sticker-animation-settings) | | render_spoilers | integer | [Spoiler rendering setting](#spoiler-rendering-settings) | -| message_display_compact | boolean | Whether messages use compact presentation | +| message_display_compact | boolean | Whether messages use compact display | | friend_source_flags | integer | [Friend source flags](#friend-source-flags) | | incoming_call_flags | integer | [Incoming call flags](#incoming-call-flags) | | group_dm_add_permission_flags | integer | [Group DM add permission flags](#group-dm-add-permission-flags) | @@ -318,7 +318,7 @@ Update semantics belong to the [user settings update object](/http-api/users/set 6 The message is `fluxer.user.preferences.v1.SyncedPreferences`. The empty string means nothing has been synced yet. The decoded message cannot exceed 262144 bytes, the 256 KiB ceiling -7 The field is read-only here and is mutated by [Modify voice activity sharing](/http-api/users/settings/#modify-voice-activity-sharing) +7 The field is read-only here and is changed by [Modify voice activity sharing](/http-api/users/settings/#modify-voice-activity-sharing) ## Custom status object @@ -367,7 +367,7 @@ One entry of the saved, user-owned guild sidebar layout. Joining a guild prepend 1 The value `-1` identifies the uncategorised folder, which holds guilds not placed in a named folder. A stored layout that omits it has it prepended when the layout is written -2 A folder written without a colour is stored with `0`. The value is null on an uncategorised folder synthesised from a legacy guild ordering or created by a guild join +2 A folder written without a colour is stored with `0`. The value is null on an uncategorised folder built from a legacy guild ordering or created by a guild join 3 A folder written without an icon is stored with `folder` @@ -416,9 +416,9 @@ One entry of the saved, user-owned guild sidebar layout. Joining a guild prepend | Value | Name | Description | | --- | --- | --- | -| 0 | ALWAYS | Always reveal spoiler content | -| 1 | ON_CLICK | Reveal spoiler content after interaction | -| 2 | IF_MODERATOR | Reveal spoiler content for moderators | +| 0 | ALWAYS | Always show spoiler content | +| 1 | ON_CLICK | Show spoiler content after interaction | +| 2 | IF_MODERATOR | Show spoiler content for moderators | ## Time format settings @@ -504,7 +504,7 @@ The account-wide profile customisation inside a [full user profile object](#full 1 The value is forced to `null` when profile privacy restricts the viewer, which is reported by `profile_limited` -2 The banner hash is withheld while the account lacks the animated banner entitlement, which gates every profile banner. This withholding is independent of profile privacy +2 The banner hash is not returned while the account lacks the animated banner entitlement, which every profile banner requires. The hash is not returned regardless of profile privacy ## Guild member profile object @@ -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 additionally omitted while the target hides the activation time, and `premium_lifetime_sequence` while it hides the sequence +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 3 The collection is omitted unless its query parameter is true, and always when the target is the caller @@ -567,7 +567,7 @@ The profile read returned by [Get user profile](#get-user-profile) for one targe 6 The field is present only on a restricted read -A lifetime target that masks its badge reports `premium_type` of `1` rather than `2`. +A lifetime target that masks its badge reports `premium_type` of `1`, not `2`. ## Get user 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 161b8a775..a3998cef9 100644 --- a/fluxer_docs/src/content/docs/http-api/users/content.mdx +++ b/fluxer_docs/src/content/docs/http-api/users/content.mdx @@ -108,7 +108,7 @@ Fluxer omits an entry whose channel the caller cannot reach. An entry that needs | Status | Body | Condition | | --- | --- | --- | | 200 | array[[message](/http-api/messages/#message-object) object] | Readable mentions were returned | -| 403 | [error response](/http-api/#error-response) | A retained entry sits in a guild whose membership state cannot be resolved, returning `ACCESS_DENIED`, or in an age-restricted channel the account has not verified for, returning `NSFW_CONTENT_AGE_RESTRICTED` | +| 403 | [error response](/http-api/#error-response) | A retained entry is in a guild whose membership state cannot be resolved, returning `ACCESS_DENIED`, or in an age-restricted channel the account has not verified for, returning `NSFW_CONTENT_AGE_RESTRICTED` | ### Rate limit @@ -202,7 +202,7 @@ A guild configuring [message_history_cutoff](/http-api/guilds/#guild-object) sti | Status | Body | Condition | | --- | --- | --- | | 200 | array[[saved message](#saved-message-object) object] | Saved entries were returned | -| 403 | [error response](/http-api/#error-response) | A stored entry sits in a guild whose membership state cannot be resolved, returning `ACCESS_DENIED`, or in an age-restricted channel, returning `NSFW_CONTENT_AGE_RESTRICTED` | +| 403 | [error response](/http-api/#error-response) | A stored entry is in a guild whose membership state cannot be resolved, returning `ACCESS_DENIED`, or in an age-restricted channel, returning `NSFW_CONTENT_AGE_RESTRICTED` | ### Side effects @@ -274,7 +274,7 @@ No current access to the channel is required. ### Side effects -Fluxer issues the delete unconditionally, and [Saved Message Delete](/gateway/events/#saved-message-delete) reaches the caller's own sessions on every accepted request, including one naming a message the account never saved. The saved entry is removed and the message stays in its channel. +Fluxer always issues the delete, and [Saved Message Delete](/gateway/events/#saved-message-delete) reaches the caller's own sessions on every accepted request, including one naming a message the account never saved. The saved entry is removed and the message stays in its channel. ### Rate limit @@ -337,13 +337,13 @@ Neither scope includes the caller's personal notes channel, so this operation ca | 202 | empty | Deletion was accepted for asynchronous processing | | 400 | [error response](/http-api/#error-response) | The date range or context selection is invalid, or a supplied sudo proof is wrong and the request returns `INVALID_PASSWORD`, `PASSWORD_NOT_SET`, or `INVALID_MFA_CODE` | | 403 | [error response](/http-api/#error-response) | No accepted sudo proof is present and the request returns `SUDO_MODE_REQUIRED` | -| 500 | [error response](/http-api/#error-response) | Deletion could not be enqueued | +| 500 | [error response](/http-api/#error-response) | Deletion could not be queued | ### Side effects An account holding an MFA authenticator that proved sudo mode with MFA receives a fresh proof in the response header. A request that already had a valid proof gets that same token echoed back without an extended lifetime. -Fluxer enqueues the work with at most 5 attempts. As deletion progresses, each affected channel emits [Message Delete Bulk](/gateway/events/#message-delete-bulk) in batches of at most 100 message IDs, and the deleted messages' attachments are permanently removed. +Fluxer queues the work with at most 5 attempts. As deletion progresses, each affected channel emits [Message Delete Bulk](/gateway/events/#message-delete-bulk) in batches of at most 100 message IDs, and the deleted messages' attachments are permanently removed. On completion the system account sends the caller a direct message in the account locale reporting the total deleted message count and the number of channels touched. That message arrives through the ordinary [Message Create](/gateway/events/#message-create) Dispatch. @@ -367,7 +367,7 @@ The schedule targets every message the caller authored before the scheduled mome ### JSON body -The body is the [sudo verification object](/http-api/users/mfa/#sudo-verification-object). It has no other field, and an existing proof travels in the `X-Fluxer-Sudo-Mode-JWT` request header. +The body is the [sudo verification object](/http-api/users/mfa/#sudo-verification-object) and has no other field. An existing proof is in the `X-Fluxer-Sudo-Mode-JWT` request header. ### Response diff --git a/fluxer_docs/src/content/docs/http-api/users/current-user.mdx b/fluxer_docs/src/content/docs/http-api/users/current-user.mdx index da022cda6..1c5b2768f 100644 --- a/fluxer_docs/src/content/docs/http-api/users/current-user.mdx +++ b/fluxer_docs/src/content/docs/http-api/users/current-user.mdx @@ -12,7 +12,7 @@ Every route except [Get current user](#get-current-user) is user-only. Those rou ## Sudo verification -Sudo verification asks the account to prove itself again inside the request. Security-sensitive fields and destructive lifecycle operations require it. The accepted proof depends on the account state. +Security-sensitive fields and destructive lifecycle operations on this page require [sudo mode](/http-api/users/mfa/#sudo-mode), which states the token form, the header round trip, and the code each failure returns. The accepted proof depends on the account state. | Account state | Accepted proof | | --- | --- | @@ -20,13 +20,7 @@ Sudo verification asks the account to prove itself again inside the request. Sec | An authenticator configured | `mfa_method` of `totp` with `mfa_code`, or `mfa_method` of `webauthn` with `webauthn_response` and `webauthn_challenge` | | No password credential and no authenticator | None, the requirement is already satisfied | -Once an account configures an authenticator, its `password` stops working here. An already valid sudo token replaces any of those forms. - -A sudo token is an HS256 JSON Web Token valid for five minutes. It has the account ID as its subject and is rejected for any other account. A successful verification returns the token in the `X-Fluxer-Sudo-Mode-JWT` response header. Fluxer mints a fresh token only when an authenticator satisfied the requirement. A request presenting an already valid token gets that same token back without an extended lifetime. - -A request that does not satisfy the requirement returns 403 [`SUDO_MODE_REQUIRED`](/http-api/errors/). The body has `has_mfa` and `methods` as top-level members. An invalid authenticator code, backup code, or WebAuthn assertion returns [`INVALID_MFA_CODE`](/http-api/errors/) on the `mfa_code` path. An incorrect password returns [`INVALID_PASSWORD`](/http-api/errors/) on the `password` path. - -A client sends an existing token in the `X-Fluxer-Sudo-Mode-JWT` request header. Retain the response header value and send it back on each later operation in the same sudo window. +Once an account configures an authenticator, its `password` stops working here. ## Get current user @@ -60,7 +54,7 @@ Modifies the current account and returns the resulting [user](/http-api/users/#u ### Limitations -- Changing the username or discriminator to a different value, supplying `new_password`, or supplying `email_token` requires [sudo verification](#sudo-verification). +- Changing the username or discriminator to a different value, supplying `new_password`, or supplying `email_token` requires [sudo verification](/http-api/users/mfa/#sudo-mode). - A field supplied with the value the account already holds is not a change and does not raise that requirement. - An unclaimed account never raises it either. @@ -73,7 +67,7 @@ Modifies the current account and returns the resulting [user](/http-api/users/#u | global_name?3 | ?string | Display name after sanitisation (1-32 characters), or null to clear | | email?4 | string | Always rejected, because an address is applied through `email_token` | | new_password?5 | string | New password (8-256 characters) | -| password? | string | Current password (8-256 characters), used as [sudo verification](#sudo-verification) proof | +| password? | string | Current password (8-256 characters), used as [sudo verification](/http-api/users/mfa/#sudo-mode) proof | | avatar?6 | ?string | Base64-encoded avatar image, or null to clear | | banner?6 7 | ?string | Base64-encoded banner image, or null to clear | | bio?8 | ?string | Biography (1-320 characters), or null to clear | @@ -81,19 +75,19 @@ Modifies the current account and returns the resulting [user](/http-api/users/#u | accent_color? | ?integer | Packed 24-bit RGB colour (0-16777215), or null to clear | | timezone?10 11 | ?string | Supported IANA timezone identifier (1-128 characters), or null to clear | | timezone_privacy_flags?10 | integer | [Profile field privacy flags](/http-api/users/#profile-field-privacy-flags) applied to the profile timezone | -| premium_badge_hidden? | boolean | Whether to withhold the premium badge from the public profile | +| premium_badge_hidden? | boolean | Whether to hide the premium badge from the public profile | | premium_badge_masked? | boolean | Whether to present a lifetime badge as an ordinary subscription badge | -| premium_badge_timestamp_hidden? | boolean | Whether to withhold the premium activation time | -| premium_badge_sequence_hidden? | boolean | Whether to withhold the lifetime premium sequence | +| premium_badge_timestamp_hidden? | boolean | Whether to hide the premium activation time | +| premium_badge_sequence_hidden? | boolean | Whether to hide the lifetime premium sequence | | premium_enabled_override?12 | boolean | Whether a staff override grants premium entitlements | | has_dismissed_premium_onboarding?13 | boolean | Whether the premium onboarding flow has been dismissed | | has_unread_gift_inventory?14 | boolean | Whether the gift inventory still holds unread items | | mention_flags? | integer | [Reply mention preference](/http-api/users/#reply-mention-preferences), one of `0`, `1`, or `2` | | email_token?15 | string | Email token (1-256 characters) issued by [verify new email](/http-api/users/email-and-password/#verify-new-email) | -| mfa_method? | string | [Sudo verification](#sudo-verification) method, either `totp` or `webauthn` | -| mfa_code? | string | [Sudo verification](#sudo-verification) authenticator or backup code (1-32 characters) | -| webauthn_response? | [WebAuthn assertion](/http-api/users/mfa/#webauthn-assertion-object) object | [Sudo verification](#sudo-verification) WebAuthn assertion | -| webauthn_challenge? | string | [Sudo verification](#sudo-verification) WebAuthn challenge (1-256 characters) | +| mfa_method? | string | [Sudo verification](/http-api/users/mfa/#sudo-mode) method, either `totp` or `webauthn` | +| mfa_code? | string | [Sudo verification](/http-api/users/mfa/#sudo-mode) authenticator or backup code (1-32 characters) | +| webauthn_response? | [WebAuthn assertion](/http-api/users/mfa/#webauthn-assertion-object) object | [Sudo verification](/http-api/users/mfa/#sudo-mode) WebAuthn assertion | +| webauthn_challenge? | string | [Sudo verification](/http-api/users/mfa/#sudo-mode) WebAuthn challenge (1-256 characters) | 1 The value cannot be `everyone` or `here` and cannot contain `fluxer` or `system message`, on every instance including a self-hosted one @@ -125,6 +119,8 @@ Modifies the current account and returns the resulting [user](/http-api/users/#u 15 Supplying the field applies the verified address the token stands for, and the token is deleted once the update lands +An omitted field leaves its stored value untouched, and an explicit `null` clears a nullable field. `has_dismissed_premium_onboarding` acts only on `true` and `has_unread_gift_inventory` only on `false`, so neither can be reversed through this route. + To skip the body proof fields, send the sudo token in the `X-Fluxer-Sudo-Mode-JWT` request header. A body that has no `email_token` and nothing beyond `mfa_method`, `mfa_code`, `webauthn_response`, and `webauthn_challenge` returns the current account unchanged. That check runs before the required action barrier, so it succeeds even while an action is outstanding. Supplying `password` alone is an update with nothing to change, and it still emits [User Update](/gateway/events/#user-update). @@ -143,7 +139,7 @@ An animated avatar requires the animated avatar entitlement and is otherwise rej ### Content blocklists -The username, display name, biography, and pronouns are matched against the instance profile substring blocklist, and a match returns 403 [`CONTENT_BLOCKED`](/http-api/errors/). An account with the `STAFF` flag is exempt. The biography and pronouns are additionally scanned against the phrase and URL blocklists. +The username, display name, biography, and pronouns are matched against the instance profile substring blocklist, and a match returns 403 [`CONTENT_BLOCKED`](/http-api/errors/). An account with the `STAFF` flag is exempt. The biography and pronouns are also scanned against the phrase and URL blocklists. The global content filter applied to every POST, PUT, and PATCH JSON body runs the same pair of blocklists over the whole body before the handler. It reads `username`, `global_name`, `email`, `timezone`, and `discriminator` when supplied as a string, alongside the biography and pronouns. It skips `avatar`, `banner`, `password`, `new_password`, `email_token`, `mfa_code`, `webauthn_response`, `webauthn_challenge`, and every field whose name ends in `_flags`, and it ignores any value shorter than 3 characters. A match returns the same 403 `CONTENT_BLOCKED`, and no flag exempts an account from it. @@ -162,10 +158,6 @@ Each control is separate from the route bucket. A denial returns 400 `INVALID_FO Fluxer consumes the avatar control before it detects identical content, and clearing the avatar consumes nothing. A clearing request consumes the banner control, and so does a request whose value is unchanged. -:::note[Omission and null are distinct] -An omitted field leaves its stored value untouched, and an explicit `null` clears a nullable field. `has_dismissed_premium_onboarding` acts only on `true` and `has_unread_gift_inventory` only on `false`, so neither can be reversed through this route. -::: - :::caution[A password change rotates the session] Supplying `new_password` on a claimed account deletes every other authentication session and every outstanding password reset token, then replaces the session that issued the request. The replacement arrives as [Auth Session Change](/gateway/events/#auth-session-change). ::: @@ -190,7 +182,7 @@ A successful change emits [User Update](/gateway/events/#user-update) to the acc -Deletes every authorised IP record for the current account. Requires [sudo verification](#sudo-verification). Returns 204 with an empty body and emits no Gateway Dispatch. +Deletes every authorised IP record for the current account. Requires [sudo verification](/http-api/users/mfa/#sudo-mode). Returns 204 with an empty body and emits no Gateway Dispatch. A later sign-in from any address then needs fresh IP authorisation. @@ -213,9 +205,9 @@ The body is the [sudo verification object](/http-api/users/mfa/#sudo-verificatio -Disables the current account while preserving its data. Requires [sudo verification](#sudo-verification). +Disables the current account while preserving its data. Requires [sudo verification](/http-api/users/mfa/#sudo-mode). -The operation mints no sudo token and returns no `X-Fluxer-Sudo-Mode-JWT` header. +The route issues no sudo token and returns no `X-Fluxer-Sudo-Mode-JWT` response header. ### JSON body @@ -235,7 +227,7 @@ Disabling deletes every authentication session the account owns, including the o ### Side effects -Fluxer marks the account disabled and deletes every authentication session it owns. [User Update](/gateway/events/#user-update) is published only after that deletion. No client of the account is still connected to receive it. The disabled flag is not part of the public flag bitfield, so other accounts observe no change. +Fluxer marks the account disabled and deletes every authentication session it owns. [User Update](/gateway/events/#user-update) is published only after that deletion, so no client of the account is left to receive it. The disabled flag is not part of the public flag bitfield, so other accounts observe no change. ### Rate limit @@ -245,7 +237,7 @@ Fluxer marks the account disabled and deletes every authentication session it ow -Schedules deletion of the current account and deletes every authentication session it holds. Requires [sudo verification](#sudo-verification). +Schedules deletion of the current account and deletes every authentication session it holds. Requires [sudo verification](/http-api/users/mfa/#sudo-mode). ### Limitations @@ -272,7 +264,7 @@ Fluxer checks guild ownership before it accepts the deletion, so an owner of any ### Side effects -Fluxer marks the account for deletion at the configured deadline and enqueues it for erasure at that time. When the account has an email address, Fluxer sends a scheduled deletion email in the account locale. Every authentication session is deleted, and [User Update](/gateway/events/#user-update) is published only after that deletion. The self-deleted flag is not part of the public flag bitfield, so other accounts observe no change until erasure runs. +Fluxer marks the account for deletion at the configured deadline and queues it for erasure at that time. When the account has an email address, Fluxer sends a scheduled deletion email in the account locale. Every authentication session is deleted, and [User Update](/gateway/events/#user-update) is published only after that deletion. The self-deleted flag is not part of the public flag bitfield, so other accounts observe no change until erasure runs. ### Rate limit @@ -286,7 +278,7 @@ Records acceptance of the current terms of service and privacy policy. Returns t ### JSON body -The body is an empty object. An absent or empty body is read as an empty object, and any supplied property is discarded. Invalid JSON returns 400 `INVALID_FORM_BODY` with `INVALID_FORMAT` at the `body` path. Valid JSON that is not an object is rejected with 400. +The body is an empty object, and an absent or empty body is read as one. Any supplied property is discarded. Invalid JSON returns 400 `INVALID_FORM_BODY` with `INVALID_FORMAT` at the `body` path. Valid JSON that is not an object is rejected with 400. ### Response 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 95e5116b6..a64e2d778 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 @@ -6,7 +6,7 @@ description: Compiling the account's data into a downloadable ZIP archive. import RouteHeader from '@/components/RouteHeader.astro'; -A data harvest is a ZIP archive of the current account's data. Fluxer compiles it asynchronously, reports progress while it runs, and issues a temporary download URL once the archive is written. +A data harvest is a ZIP archive of the current account's data. Fluxer compiles it in the background, reports progress while it runs, and issues a temporary download URL once the archive is written. Every route except [Download data harvest archive](#download-data-harvest-archive) is user-only. Fluxer rejects a bot or OAuth2 bearer credential with 403 `ACCESS_DENIED`, and an account that has suspicious activity flags with 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`. No route here requires [sudo mode](/http-api/users/mfa/#sudo-mode). [Delete current user's messages](/http-api/users/content/#delete-current-users-messages) takes the same filter shape, destroys the messages, and requires it. @@ -154,7 +154,7 @@ The `inaccessible_only` scope selects messages in guilds the caller has left or 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. -Supplying both date bounds requires `start_date` strictly earlier than `end_date`. An equal or inverted pair is rejected at path `end_date`. +Supplying both date bounds requires `start_date` strictly earlier than `end_date`. An equal or reversed pair is rejected at path `end_date`. :::caution[A filtered archive still has every non-message section] The archive still contains the account document, the payment history document, the owned application document, the security document, and the account's avatar and banner. No read response returns the applied filter, so a caller keeps its own record of what it submitted. @@ -180,7 +180,7 @@ The operation takes no request body and always returns before the archive exists A harvest record is created with a new [snowflake](/snowflakes/), the request time as `created_at`, progress 0, the step `Queued`, and every other timestamp null. Creating a harvest removes no earlier record and enforces no ceiling on how many an account holds, so the route bucket is the only bound. -Fluxer prepares the archive asynchronously. It contains: +Fluxer prepares the archive in the background. It contains: - `user.json`, the account document - one `channels/{channel_id}/messages.json` file for each channel that contributed a message, with its messages ordered oldest first @@ -191,7 +191,7 @@ Fluxer prepares the archive asynchronously. It contains: Attachment metadata appears with its message and has the attachment ID, filename, size, content type, CDN URL, and pixel dimensions. The attachment files themselves are never included. -At most 100,000 authored messages are collected. Fluxer skips a message it cannot read, and the harvest still succeeds. When the archive completes, Fluxer sends one email containing a download URL if the account has an email address and the instance has email delivery enabled. That URL expires seven days after it was minted. +At most 100,000 authored messages are collected. Fluxer skips a message it cannot read, and the harvest still succeeds. When the archive completes, Fluxer sends one email containing a download URL if the account has an email address and the instance has email delivery enabled. That URL expires seven days after it was issued. ### Rate limit @@ -321,7 +321,7 @@ The signed `token` query parameter issued by [Get data harvest download URL](#ge The operation is active only while the instance has presigned harvest downloads disabled. That setting is enabled by default, so a default deployment answers every request here as 404 without inspecting the token. :::note[Every rejection looks identical] -A missing, malformed, forged, or expired token, a token for another harvest, a harvest that never completed, a harvest that recorded a failure, a harvest past its deadline, and a stale object key all return 404 with the plain text body `Not Found`. +A missing, malformed, forged, or expired token and a token for another harvest all return 404 with the plain text body `Not Found`. So do a harvest that never completed, one that recorded a failure, one past its deadline, and a stale object key. ::: ### Path parameters diff --git a/fluxer_docs/src/content/docs/http-api/users/email-and-password.mdx b/fluxer_docs/src/content/docs/http-api/users/email-and-password.mdx index e7396b1bd..9c6273a53 100644 --- a/fluxer_docs/src/content/docs/http-api/users/email-and-password.mdx +++ b/fluxer_docs/src/content/docs/http-api/users/email-and-password.mdx @@ -6,25 +6,25 @@ description: Ticketed replacement of the account email address and password. import RouteHeader from '@/components/RouteHeader.astro'; -Changing the email address or the password on an account takes several steps. Fluxer opens a ticket, emails a code, and every later step presents that ticket together with the codes and proofs it has collected. An account whose address the mail provider rejected uses [bounced email recovery](#bounced-email-recovery) instead. +Changing the email address or the password on an account takes several steps. Fluxer opens a ticket, emails a code, and every later step sends that ticket with the codes and proofs collected so far. An account whose address the mail provider rejected uses [bounced email recovery](#bounced-email-recovery) instead. -Every operation needs a non-bot user session. The email change routes admit a session with suspicious account state. Every password change route rejects that state with 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`. +Every operation needs a non-bot user session. The email change routes accept a session with suspicious account state. Every password change route rejects that state with 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`. ## Ticket and code contract -A ticket identifier is a version 4 UUID. Fluxer issues it when the flow starts, and every later step sends it as a plain string of 1 to 256 characters. Each ticket belongs to one account, and a step presented by another account fails with `INVALID_OR_EXPIRED_TICKET`. A completed ticket rejects every further step with `TICKET_ALREADY_COMPLETED`. Starting a flow again mints a fresh ticket and leaves an outstanding one usable. +A ticket identifier is a version 4 UUID. Fluxer issues it when the flow starts, and every later step sends it as a plain string of 1 to 256 characters. Each ticket belongs to one account, and a step sent by another account fails with `INVALID_OR_EXPIRED_TICKET`. A completed ticket rejects every further step with `TICKET_ALREADY_COMPLETED`. Starting a flow again issues a fresh ticket and leaves an outstanding one usable. -A verification code is eight characters drawn from the uppercase Latin alphabet and the decimal digits, written as two four-character groups separated by a hyphen. A code lives for 10 minutes from the moment it is sent. Fluxer trims a submitted value and the comparison is otherwise exact, so a value with any other stray character fails with `INVALID_VERIFICATION_CODE`. +A verification code is eight characters from the uppercase Latin alphabet and the decimal digits, written as two four-character groups separated by a hyphen. A code lives for 10 minutes from the moment it is sent. Fluxer trims a submitted value and the comparison is otherwise exact, so a value with any other stray character fails with `INVALID_VERIFICATION_CODE`. :::caution[A lowercase code never matches] -An issued code has only uppercase letters, digits, and one hyphen. A client that accepts free-form input uppercases the value and preserves the separator before submitting it. +A client that accepts free-form input uppercases the value and preserves the separator before submitting it. ::: Fluxer returns a proof, a version 4 UUID, from every step that establishes a fact about the ticket. The email change flow issues `original_proof` for the original address and requires it on [Request new email](#request-new-email) and [Verify new email](#verify-new-email). The password change flow issues `verification_proof` for the emailed code and requires it to complete the change. An email token is a version 4 UUID standing for one verified new address. It lives 30 minutes, belongs to the account it was issued to, and is deleted once consumed. A token presented by another account fails with `INVALID_EMAIL_TOKEN`, and one past its lifetime is deleted and fails with `EMAIL_TOKEN_EXPIRED`. -Fluxer refuses a resend for 30 seconds after the previous send of the same code, and that refusal is HTTP 429 with a `Retry-After` computed from the exact moment the next send becomes available. It is independent of the route bucket and of the longer email-send controls. +Fluxer refuses a resend for 30 seconds after the previous send of the same code. The refusal is HTTP 429 with a `Retry-After` computed from the exact moment the next send becomes available. This cooldown is separate from the route bucket and the email-send controls. :::danger[Tickets, codes, proofs, and email tokens are credentials] Each of these values can advance a security-sensitive account change. Keep them out of logs, analytics, URLs, and messages to unintended recipients. They are opaque, so preserve them exactly and derive nothing from their contents. @@ -45,7 +45,7 @@ The account-level codes `ACCESS_DENIED`, `ACCOUNT_SUSPICIOUS_ACTIVITY`, `SUDO_MO ## Email send controls -Each send-bearing operation passes through its own route bucket and, separately, through a longer email-send control keyed by the account. Exhausting either produces HTTP 429 even when the other has room. +Each send-bearing operation passes through its own route bucket and a longer email-send control keyed by the account. Exhausting either produces HTTP 429 even when the other has room. | Control | Allowance | Operations | | --- | --- | --- | @@ -56,7 +56,7 @@ Each send-bearing operation passes through its own route bucket and, separately, ## Email change ticket states -The email change ticket is pending-original, pending-new, or completed. A step legal in one state is rejected in every other with the transition code named below. +The email change ticket is pending-original, pending-new, or completed. A step allowed in one state is rejected in every other with the transition code named below. | Current state | Operation | Next state | | --- | --- | --- | @@ -78,7 +78,7 @@ Reaching the completed state issues an email token and leaves the account email ## Email change start object -The state of a freshly created email change ticket. +The state of a newly created email change ticket. ### Structure @@ -86,7 +86,7 @@ The state of a freshly created email change ticket. | --- | --- | --- | | ticket | string | The identifier every later step of this flow sends | | require_original1 | boolean | Whether the original address needs verification before a new address can be requested | -| original_email2 | ?string | The address currently on the account, or null when the account has none | +| original_email2 | ?string | The address on the account, or null when the account has none | | original_proof3 | ?string | The proof for the original address stage, or null while that stage is unverified | | original_code_expires_at4 | ?ISO8601 timestamp | The moment the code sent to the original address expires, 10 minutes after it was sent | | resend_available_at4 | ?ISO8601 timestamp | The earliest moment the original address code can be resent, 30 seconds after the last send | @@ -95,7 +95,7 @@ The state of a freshly created email change ticket. 2 Recorded on the ticket at creation, so a later change to the account email does not move it. [Request new email](#request-new-email) compares against this recorded value -3 Populated exactly when `require_original` is false. [Request new email](#request-new-email) and [Verify new email](#verify-new-email) require it +3 Set exactly when `require_original` is false. [Request new email](#request-new-email) and [Verify new email](#verify-new-email) require it 4 Null whenever `require_original` is false @@ -129,7 +129,7 @@ The state of a ticket with a candidate address bound to it and a code in flight 2 The trimmed form of the submitted address, which is what a later [Verify new email](#verify-new-email) writes -3 Always populated +3 Always set ## Original email verification object @@ -145,7 +145,7 @@ The proof that a ticket has cleared its original address stage. ## New email verification object -The email token a completed email change ticket hands back. +The email token a completed email change ticket returns. ### Structure @@ -157,7 +157,7 @@ The email token a completed email change ticket hands back. ## Password change start object -The state of a freshly created password change ticket. +The state of a newly created password change ticket. ### Structure @@ -167,7 +167,7 @@ The state of a freshly created password change ticket. | code_expires_at | ISO8601 timestamp | The moment the emailed code expires, 10 minutes after it was sent | | resend_available_at1 | ?ISO8601 timestamp | The earliest moment the code can be resent, 30 seconds after the last send | -1 Always populated +1 Always set ## Password verification object @@ -183,7 +183,7 @@ The proof that the emailed code was accepted. ## Password change completion object -The replacement session [Complete password change](#complete-password-change) hands back after the password is written. +The replacement session [Complete password change](#complete-password-change) returns after the password is written. ### Structure @@ -192,7 +192,7 @@ The replacement session [Complete password change](#complete-password-change) ha | token1 | string | The authentication token for the replacement session | | auth_session_id_hash | string | The base64url-encoded hash of the replacement authentication session | -1 The same operation destroys the token the client authenticated with, so adopt this value before issuing another request +1 The same operation destroys the token the client authenticated with, so use this value before issuing another request ## Start email change @@ -202,9 +202,9 @@ Creates an email change ticket. Returns an [email change start](#email-change-st Only an account that holds an email address or is unclaimed can start an email change. Every other account fails with `MUST_HAVE_EMAIL_TO_CHANGE_IT`. -Original address verification applies only when the account holds a verified email address. A bounce clears the verified state. An account that has done nothing since the bounce gets a ticket that skips the original stage. [Apply email change](#apply-email-change) marks the address verified again without clearing the bounced marker, so an account that has applied a change since the bounce gets a ticket that does require the stage. +Original address verification applies only when the account holds a verified email address. A bounce clears the verified state. An account that has done nothing since the bounce gets a ticket that skips the original stage. [Apply email change](#apply-email-change) marks the address verified again without clearing the bounced marker. An account that has applied a change since the bounce therefore gets a ticket that does require the stage. -Sending an original address code consumes the original address control. +Sending an original address code counts against the original address control. ### JSON body @@ -232,7 +232,7 @@ Sends a fresh verification code to the account's original address for an active A ticket that is no longer pending-original fails with `ORIGINAL_EMAIL_ALREADY_VERIFIED`. A ticket recording no original address fails with `NO_ORIGINAL_EMAIL_ON_RECORD`. -The send consumes the original address control, and the ticket enforces its own 30-second cooldown. +The send counts against the original address control, and the ticket applies its own 30-second cooldown. ### JSON body @@ -296,7 +296,7 @@ Fluxer trims the address and then checks it in order. Ownership is checked again when the resulting email token is applied, so passing here reserves nothing. Calling the operation again with a different address replaces the pending address on the same ticket. -The send consumes the new address control, and the ticket enforces its own 30-second cooldown. +The send counts against the new address control, and the ticket applies its own 30-second cooldown. ### JSON body @@ -323,7 +323,7 @@ An unclaimed account learns that its intended password is unusable before a code ### Side effects -The ticket stores the requested address, a fresh code, its send time, and its expiry, and stays pending-new. One verification email goes to the requested address. The account's own email address is untouched at this stage. +The ticket stores the requested address, a fresh code, its send time, and its expiry, and stays pending-new. One verification email goes to the requested address. The account email is unchanged at this stage. ### Rate limit @@ -337,7 +337,7 @@ Sends a fresh verification code to the address already bound to an active ticket A ticket with no requested address fails with `NO_NEW_EMAIL_REQUESTED`. -The send consumes the new address control, and the ticket enforces its own 30-second cooldown. +The send counts against the new address control, and the ticket applies its own 30-second cooldown. ### JSON body @@ -399,15 +399,15 @@ The ticket moves to the completed state and a 30-minute email token is issued fo Consumes an email token and writes its address to the account. Returns the updated [user](/http-api/users/#user-object) object on success. Emits a [User Update](/gateway/events/#user-update) Gateway event. -A claimed account proves [sudo mode](/http-api/users/mfa/#sudo-mode) before the route examines the token. An unclaimed account is exempt and applies the change with its session alone. The route accepts suspicious account state. +A claimed account proves [sudo mode](/http-api/users/mfa/#sudo-mode) before the route reads the token. An unclaimed account is exempt and applies the change with its session alone. The route accepts suspicious account state. -A token that belongs to another account fails with `INVALID_EMAIL_TOKEN`, and one past its 30-minute lifetime fails with `EMAIL_TOKEN_EXPIRED`. An expired token is deleted when rejected. Fluxer checks the address against existing account ownership again here, and an address another account has taken since fails with `EMAIL_ALREADY_IN_USE` without consuming the token. +A token that belongs to another account fails with `INVALID_EMAIL_TOKEN`, and one past its 30-minute lifetime fails with `EMAIL_TOKEN_EXPIRED`. An expired token is deleted when rejected. Fluxer checks the address against existing account ownership again here. An address another account has taken since fails with `EMAIL_ALREADY_IN_USE` without consuming the token. [Modify current user](/http-api/users/current-user/#modify-current-user) accepts the same token alongside an unrelated profile edit. The token is single use, so a retry after success fails with `INVALID_EMAIL_TOKEN`. ### JSON body -The body extends the [sudo verification object](/http-api/users/mfa/#sudo-verification-object) with `email_token`, and the sudo fields it merges in are repeated below. An existing sudo proof travels in the `X-Fluxer-Sudo-Mode-JWT` request header. +The body extends the [sudo verification object](/http-api/users/mfa/#sudo-verification-object) with `email_token`, and the sudo fields it merges in are repeated below. An existing sudo proof is sent in the `X-Fluxer-Sudo-Mode-JWT` request header. | Field | Type | Description | | --- | --- | --- | @@ -449,7 +449,7 @@ This operation replaces the email address only. Nothing on the path revokes a cr ### Side effects -Fluxer replaces the account email, marks it verified, and removes every email-clearable suspicious activity flag, which can empty `required_actions` and restore ordinary access. Fluxer evaluates the new address against the instance contact policy separately, and that check can add suspicious activity flags. The bounced marker is not cleared, so an account that has it recovers through [bounced email recovery](#bounced-email-recovery). +Fluxer replaces the account email, marks it verified, and removes every email-clearable suspicious activity flag, which can empty `required_actions` and restore ordinary access. The new address is also checked against the instance contact policy, and that check can add suspicious activity flags. The bounced marker is not cleared, so an account that has it recovers through [bounced email recovery](#bounced-email-recovery). For an ordinary change from an existing address, Fluxer sends a revert email to the original address so its holder can use [Revert an email change](/http-api/authentication/#revert-an-email-change). Fluxer deletes the email token, records the contact change in the account's contact change log, and emits [User Update](/gateway/events/#user-update). No public member field changes, so no [Guild Member Update](/gateway/events/#guild-member-update) follows. @@ -459,13 +459,13 @@ For an ordinary change from an existing address, Fluxer sends a revert email to ## Bounced email recovery -An account whose stored address the mail provider rejected has the bounced marker, reported as `email_bounced` on the [user object](/http-api/users/#user-object). The bounce also clears the verified email state and imposes a required action. The operations below bind a replacement address and finish without the sudo-gated apply step. +An account whose stored address the mail provider rejected has the bounced marker, reported as `email_bounced` on the [user object](/http-api/users/#user-object). The bounce also clears the verified email state and adds a required action. The operations below bind a replacement address and finish without the sudo-gated apply step. Every one of them requires the bounced marker. An account without it is rejected with 403 `ACCESS_DENIED`, and an account with no address at all is rejected with `MUST_HAVE_EMAIL_TO_CHANGE_IT`. -The flow also requires the account to hold no verified address, which is the state a bounce leaves. [Apply email change](#apply-email-change) marks an address verified without clearing the bounced marker. An account that took the ordinary flow after its bounce has both states, gets `ORIGINAL_EMAIL_MUST_BE_VERIFIED_FIRST` here, and changes its address through the ordinary flow. +The flow also requires the account to hold no verified address, which is the state a bounce leaves. [Apply email change](#apply-email-change) marks an address verified without clearing the bounced marker. An account that took the ordinary flow after its bounce has both states, so it gets `ORIGINAL_EMAIL_MUST_BE_VERIFIED_FIRST` here and changes its address through the ordinary flow. -[Ticket and code contract](#ticket-and-code-contract) governs its codes, cooldown, and new address send control. +[Ticket and code contract](#ticket-and-code-contract) applies to its codes, cooldown, and new address send control. ## Request replacement email for bounced address @@ -473,12 +473,12 @@ The flow also requires the account to hold no verified address, which is the sta Starts the recovery flow. Returns a [new email request](#new-email-request-object) object on success. -This operation creates a ticket and binds the replacement address in the same call. The route accepts suspicious account state. Fluxer checks the address exactly as [Request new email](#request-new-email) checks it, so it passes DNS eligibility, belongs to no other account, and differs from the bounced address. +This operation creates a ticket and binds the replacement address in the same call. The route accepts suspicious account state. Fluxer checks the address exactly as [Request new email](#request-new-email) does: it must pass DNS eligibility, belong to no other account, and differ from the bounced address. -The send consumes the new address control. +The send counts against the new address control. :::caution[A failed attempt still sends mail] -When the account does hold a verified address the request fails with `ORIGINAL_EMAIL_MUST_BE_VERIFIED_FIRST`. The failure lands only after Fluxer has created the ticket, sent one code to the address the account currently holds, and counted one send against the original address control. +When the account does hold a verified address the request fails with `ORIGINAL_EMAIL_MUST_BE_VERIFIED_FIRST`. The failure comes only after Fluxer has created the ticket, sent one code to the address the account holds, and counted one send against the original address control. ::: ### JSON body @@ -496,7 +496,7 @@ When the account does hold a verified address the request fails with `ORIGINAL_E ### Side effects -A ticket is created pending-new with the replacement address bound to it, and one verification email goes to that address. No account field changes. Retain the returned `ticket`, because the remaining steps take no other identifier. +A ticket is created pending-new with the replacement address bound to it, and one verification email goes to that address. No account field changes. Keep the returned `ticket`, because the remaining steps take no other identifier. ### Rate limit @@ -510,7 +510,7 @@ Sends a fresh verification code to the replacement address bound to an active re A ticket with no replacement address fails with `NO_NEW_EMAIL_REQUESTED`. -The send consumes the new address control, and the ticket enforces its own 30-second cooldown. +The send counts against the new address control, and the ticket applies its own 30-second cooldown. ### JSON body @@ -557,7 +557,7 @@ No sudo verification and no email token step applies. ### Side effects -Fluxer replaces the account email, marks it verified, clears the bounced marker, and removes every email-clearable suspicious activity flag, which can empty `required_actions` and restore ordinary access. It marks the ticket completed and deletes its email token immediately. The contact change is recorded in the account's contact change log. +Fluxer replaces the account email, marks it verified, clears the bounced marker, and removes every email-clearable suspicious activity flag. That can empty `required_actions` and restore ordinary access. Fluxer marks the ticket completed and deletes its email token immediately. The contact change is recorded in the account's contact change log. No revert email is sent. @@ -573,7 +573,7 @@ Creates a password change ticket and sends a verification code to the account em An account holding no email address fails with `MUST_HAVE_EMAIL_TO_CHANGE_IT`. -The send consumes the password change start control. +The send counts against the password change start control. ### JSON body @@ -599,7 +599,7 @@ A ticket is created in its pending state and one verification email goes to the Sends a fresh verification code for an active password change ticket. Requires the account to still hold an email address. Returns 204 with an empty body. -The send consumes the password change resend control, and the ticket enforces its own 30-second cooldown. +The send counts against the password change resend control, and the ticket applies its own 30-second cooldown. ### JSON body @@ -678,7 +678,7 @@ Completing a password change deletes every authentication session for the accoun ### Side effects -The password and its change timestamp are replaced and the ticket is marked completed. Every outstanding password reset token is deleted. Fluxer deletes and terminates every other authentication session on the Gateway first, then emits [Auth Session Change](/gateway/events/#auth-session-change), then deletes and terminates the caller's own session. The returned replacement is then the account's only session. OAuth2 access tokens and refresh tokens are not revoked. +The password and its change timestamp are replaced and the ticket is marked completed. Every outstanding password reset token is deleted. Fluxer deletes and ends every other authentication session on the Gateway first, then emits [Auth Session Change](/gateway/events/#auth-session-change), then deletes and ends the caller's own session. The returned replacement is the account's only session. OAuth2 access tokens and refresh tokens are not revoked. ### Rate limit diff --git a/fluxer_docs/src/content/docs/http-api/users/gifts.mdx b/fluxer_docs/src/content/docs/http-api/users/gifts.mdx index 86c50b7a9..5f976eeb6 100644 --- a/fluxer_docs/src/content/docs/http-api/users/gifts.mdx +++ b/fluxer_docs/src/content/docs/http-api/users/gifts.mdx @@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro'; The gift inventory lists the premium gift codes the current account created, with the redemption state of each. The [Gifts resource](/http-api/gifts/) has public gift lookup and redemption, and the [Billing resource](/http-api/billing/) has gift purchase. -A self-hosted deployment answers this route with 404 `NOT_FOUND`, as [deployment availability](/http-api/deployment-availability/) describes. +A self-hosted deployment returns 404 `NOT_FOUND` here, as [deployment availability](/http-api/deployment-availability/) describes. A code enters this inventory only through a completed gift checkout with the payment provider. The Admin API records a code it issues against the system account, so that code never appears in any account's inventory. @@ -40,7 +40,7 @@ One gift code created by the current account, with its redemption state. A compl 2 One of `days`, `weeks`, `months`, or `years`. A purchased code has only `months` or `years`, because a term dividing evenly into years is normalised to years -3 A purchased code always has `1`, because the only gift products are one month and one year. The value `0` denotes a lifetime grant, and no current operation issues one +3 A purchased code always has `1`, because the only gift products are one month and one year. The value `0` means a lifetime grant, and no current operation issues one 4 A code issued while repairing a lost checkout notification has the checkout time, so the value can predate the moment the code appeared 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 056f8951f..66ffb893a 100644 --- a/fluxer_docs/src/content/docs/http-api/users/mfa.mdx +++ b/fluxer_docs/src/content/docs/http-api/users/mfa.mdx @@ -6,7 +6,7 @@ description: TOTP, backup codes, WebAuthn credentials, and sudo mode. 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 stands in for a TOTP code, and [sudo mode](#sudo-mode) reuses these factors to guard every security-sensitive operation across the API. +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. @@ -21,7 +21,7 @@ Fluxer validates a TOTP code as a six-digit HMAC-SHA-1 one-time password over a The client generates the secret, presents it to the user, and submits it with a code derived from it when calling [Enable TOTP MFA](#enable-totp-mfa). Fluxer never generates or returns a TOTP secret. :::caution[A TOTP code can pass twice] -Fluxer rejects a second presentation of an accepted code for 30 seconds. The skew keeps a code valid up to 90 seconds, so it passes again once those 30 seconds lapse. Wait for the next time step. +Fluxer rejects an accepted code for 30 seconds if it is sent again. The skew keeps a code valid up to 90 seconds, so it passes again once those 30 seconds lapse. Wait for the next time step. ::: ## Backup code contract @@ -34,7 +34,7 @@ Enabling TOTP issues 10 codes without deleting anything first. An account that a 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. -Every entry point requires the account to hold a TOTP secret. An account whose only authenticator is a WebAuthn credential can still regenerate backup codes, but holds no secret, so none of the three entry points accepts one. +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. :::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`. @@ -46,13 +46,13 @@ Keep the TOTP secret and every backup code out of logs, analytics, URLs, and oth ## Backup codes challenge -An account that lost its saved backup codes reads the set back through an emailed challenge. [Start MFA backup codes challenge](#start-mfa-backup-codes-challenge) opens a ticket and emails a code. [Verify MFA backup codes challenge code](#verify-mfa-backup-codes-challenge-code) exchanges that code for the current set and a proof. [Regenerate MFA backup codes](#regenerate-mfa-backup-codes) then presents the ticket and the proof to replace the set. [Resend MFA backup codes challenge code](#resend-mfa-backup-codes-challenge-code) sends a fresh code at any point before verification. +An account that lost its saved backup codes reads the set back through an emailed challenge. [Start MFA backup codes challenge](#start-mfa-backup-codes-challenge) opens a ticket and emails a code. [Verify MFA backup codes challenge code](#verify-mfa-backup-codes-challenge-code) exchanges that code for the current set and a proof. [Regenerate MFA backup codes](#regenerate-mfa-backup-codes) then presents the ticket and the proof to replace the set. [Resend MFA backup codes challenge code](#resend-mfa-backup-codes-challenge-code) sends a fresh code any time before verification. [Sudo mode](#sudo-mode) applies to none of the four, so a user who no longer has the authenticator app still reaches the codes through the email address. The account needs a verified email address and TOTP enabled. Only [Start MFA backup codes challenge](#start-mfa-backup-codes-challenge) checks the address, and [Regenerate MFA backup codes](#regenerate-mfa-backup-codes) checks TOTP a second time. -A ticket is a version 4 UUID and lives 30 minutes from its last write. Starting the challenge, resending the code, and verifying the code each write the ticket. Regeneration reads it and writes nothing, so the window runs from the verification that preceded it. A ticket Fluxer no longer holds, or one opened by another account, fails with `INVALID_OR_EXPIRED_TICKET`. +A ticket is a version 4 UUID and is valid for 30 minutes from its last write. Starting the challenge, resending the code, and verifying the code each write the ticket. Regeneration reads it and writes nothing, so the window runs from the verification that preceded it. A ticket Fluxer no longer holds, or one opened by another account, fails with `INVALID_OR_EXPIRED_TICKET`. -A verification code is eight characters drawn from the uppercase Latin alphabet and the decimal digits, written as two four-character groups separated by a hyphen. A code lives 10 minutes from the moment it is sent, and each send replaces the code the previous send issued. +A verification code is eight characters drawn from the uppercase Latin alphabet and the decimal digits, written as two four-character groups separated by a hyphen. A code is valid for 10 minutes from the moment it is sent, and each send replaces the code the previous send issued. :::note[A lowercase code with no hyphen still matches] Fluxer trims the submitted value, uppercases it, and removes every hyphen before it compares. A wrong value fails with `INVALID_VERIFICATION_CODE` on the path `code`. @@ -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, and that refusal is HTTP 429 with a `Retry-After` computed from the exact 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 two 15-minute send controls. ## Sudo mode @@ -80,7 +80,7 @@ Sudo mode is a short-lived proof that the human in front of the session is still The sudo token is an HS256 JSON Web Token with a lifetime of exactly five minutes. Its subject is the account [snowflake](/snowflakes/), and verification rejects a token whose subject is any other account. The token is opaque, and it is not bound to the session that obtained it. Fluxer generates one only when the account has at least one MFA authenticator and the request proved identity with MFA. :::caution[The echoed header repeats the request value] -When Fluxer generates no fresh token, it echoes the request's `X-Fluxer-Sudo-Mode-JWT` value back unchanged and never re-signs it, so the window never slides. Fluxer echoes the value even when it fails verification. +When Fluxer issues no fresh token it echoes the request's `X-Fluxer-Sudo-Mode-JWT` value back unchanged and never re-signs it, so the five-minute window never slides. Fluxer echoes the value even when it fails verification. ::: The accepted proof depends on the authenticators the account has configured. @@ -88,7 +88,7 @@ The accepted proof depends on the authenticators the account has configured. | Account state | Accepted proof | | --- | --- | | No authenticator | `password` | -| Any authenticator | `mfa_method` with its matching material, because `password` is no longer accepted | +| Any authenticator | `mfa_method` with its matching fields, because `password` is no longer accepted | | Neither an authenticator nor a password credential | Nothing, and eligible sudo operations pass | The `totp` method reads `mfa_code` as a current authenticator code or an unconsumed backup code and requires the account to hold a TOTP secret. The `webauthn` method reads `webauthn_response` and `webauthn_challenge` together and requires a registered credential. @@ -97,7 +97,7 @@ A request that has no accepted proof fails with 403 `SUDO_MODE_REQUIRED`. Its bo The `totp` method also consumes a per-account allowance of 10 multi-factor attempts in 15 minutes, shared by every sudo-gated operation on every resource. Fluxer charges the allowance before it checks the code, so a wrong code and a correct code both draw on it. A correct code resets the counter to zero. While it is exhausted, a correct code returns the same `INVALID_MFA_CODE` entry as a wrong one. The `webauthn` method draws on no allowance. The login MFA allowances on [HTTP authentication](/http-api/authentication/) are counted separately. -Fluxer returns an issued or echoed token in the `X-Fluxer-Sudo-Mode-JWT` response header. A client retains it and presents it through the same header on every later sudo-gated operation. +Fluxer returns an issued or echoed token in the `X-Fluxer-Sudo-Mode-JWT` response header. A client keeps it and sends it in the same header on every later sudo-gated operation. ## Sudo mode methods object @@ -175,7 +175,7 @@ A backup code is two four-character groups separated by a hyphen. Each group is ## MFA backup codes challenge object -The state of a freshly created backup codes challenge ticket. +The state of a newly created backup codes challenge ticket. ### Structure @@ -187,7 +187,7 @@ The state of a freshly created backup codes challenge ticket. ## MFA backup codes verification object -The backup codes and the proof a verified challenge hands back. +The backup codes and the proof a verified challenge returns. ### Structure @@ -215,7 +215,7 @@ Fluxer verifies every assertion against the relying party identifier and the all 1 Null until the credential completes a login, MFA, or sudo assertion for the first time -The credential's public key, signature counter, and reported transports are never returned. +Fluxer never returns the credential's public key, signature counter, or reported transports. ## WebAuthn credential descriptor object @@ -325,7 +325,7 @@ Fluxer accepts additional credential property fields. ## WebAuthn authentication options object -Fluxer returns this object from every operation that issues a WebAuthn authentication challenge: 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 two login option operations on [HTTP authentication](/http-api/authentication/). ### Structure @@ -341,7 +341,7 @@ Fluxer returns this object from every operation that issues a WebAuthn authentic 2 Every operation emits the fixed value `60000`, a standard PublicKeyCredential request option -3 Sudo options list every credential currently registered to the account. The field is absent only on the discoverable login route +3 Sudo options list every credential registered to the account. The field is absent only on the discoverable login route 4 Sudo options and MFA login completion request `discouraged`. Discoverable login requests and verifies `required` @@ -457,7 +457,7 @@ Fluxer checks sudo mode first, so an account that already holds a WebAuthn crede ### JSON body -The body extends the [sudo verification object](#sudo-verification-object) with the fields below, and an existing sudo proof travels in the `X-Fluxer-Sudo-Mode-JWT` request header. +The body extends the [sudo verification object](#sudo-verification-object) with the fields below, and an existing sudo proof is sent in the `X-Fluxer-Sudo-Mode-JWT` request header. | Field | Type | Description | | --- | --- | --- | @@ -490,11 +490,11 @@ TOTP is enabled and 10 backup codes are issued. Any backup code the account alre Disables TOTP for the current account and returns 204 with an empty body. [Sudo mode](#sudo-mode) is required in addition to this route's `code` field. Emits a [User Update](/gateway/events/#user-update) Gateway event. -The account needs TOTP already enabled and is otherwise refused with 400 `TWO_FACTOR_REQUIRED`. A verified email is not required, so an account whose address later became unverified can still remove its authenticator. +TOTP must already be enabled, or the request is refused with 400 `TWO_FACTOR_REQUIRED`. A verified email is not required, so an account whose address later became unverified can still remove its authenticator. ### JSON body -The body extends the [sudo verification object](#sudo-verification-object) with the field below, and an existing sudo proof travels in the `X-Fluxer-Sudo-Mode-JWT` request header. +The body extends the [sudo verification object](#sudo-verification-object) with the field below, and an existing sudo proof is sent in the `X-Fluxer-Sudo-Mode-JWT` request header. | Field | Type | Description | | --- | --- | --- | @@ -516,7 +516,7 @@ Disabling TOTP invalidates every backup code on the account, including the code ### Side effects -TOTP is disabled and every backup code stops working. The TOTP authenticator type is removed, along with the unassigned legacy authenticator value `1` if the account still had it. The resulting authenticator types are also applied to every bot account owned by the user. [User Update](/gateway/events/#user-update) reaches the user's sessions and each owned bot whose authenticator types changed. +TOTP is disabled and every backup code stops working. Fluxer removes the TOTP authenticator type, along with the unassigned legacy authenticator value `1` when the account still held it. The resulting authenticator types are also applied to every bot account owned by the user. [User Update](/gateway/events/#user-update) goes to the user's sessions and each owned bot whose authenticator types changed. ### Rate limit @@ -530,7 +530,7 @@ Returns an [MFA backup codes](#mfa-backup-codes-object) object, replacing the se Neither a verified email nor an authenticator is required, so an account with no MFA at all can prove sudo mode with its password and use this operation. -A backup code presented as `mfa_code` to satisfy sudo mode is consumed. With `regenerate` false it comes back with `consumed` set to true. With `regenerate` true it is deleted with the rest of the previous set and does not appear. +A backup code sent as `mfa_code` to satisfy sudo mode is consumed. With `regenerate` false it comes back with `consumed` set to true. With `regenerate` true it is deleted with the rest of the previous set and does not appear. :::caution[Regeneration replaces the previous set immediately] Every code from the previous set is invalid as soon as an operation with `regenerate` set to true returns. The deletion and the insertion are separate writes, so the account holds no backup code at all between them. @@ -538,7 +538,7 @@ Every code from the previous set is invalid as soon as an operation with `regene ### JSON body -The body extends the [sudo verification object](#sudo-verification-object) with the field below, and an existing sudo proof travels in the `X-Fluxer-Sudo-Mode-JWT` request header. +The body extends the [sudo verification object](#sudo-verification-object) with the field below, and an existing sudo proof is sent in the `X-Fluxer-Sudo-Mode-JWT` request header. | Field | Type | Description | | --- | --- | --- | @@ -666,7 +666,7 @@ The ticket stores a fresh proof and its code is cleared. The response holds ever Replaces the account's backup codes with 10 new ones and returns an [MFA backup codes](#mfa-backup-codes-object) object. Sudo mode is not required. -The ticket and its proof are the only authorisation. The account needs TOTP enabled and is otherwise refused with 400 `TWO_FACTOR_REQUIRED`. +The ticket and its proof are the only authorisation. Without TOTP enabled the request is refused with 400 `TWO_FACTOR_REQUIRED`. A ticket holding no proof returns `INVALID_OR_EXPIRED_TICKET` on the path `verification_proof`, and a proof that does not match returns `INVALID_PROOF_TOKEN`. @@ -727,7 +727,7 @@ This operation sets no `X-Fluxer-Sudo-Mode-JWT` response header. ### JSON body -The body is a [sudo verification object](#sudo-verification-object). An existing sudo proof travels in the `X-Fluxer-Sudo-Mode-JWT` request header. +The body is a [sudo verification object](#sudo-verification-object). An existing sudo proof is sent in the `X-Fluxer-Sudo-Mode-JWT` request header. ### Response @@ -799,7 +799,7 @@ A verified email is not required. ### JSON body -The body extends the [sudo verification object](#sudo-verification-object) with the field below, and an existing sudo proof travels in the `X-Fluxer-Sudo-Mode-JWT` request header. +The body extends the [sudo verification object](#sudo-verification-object) with the field below, and an existing sudo proof is sent in the `X-Fluxer-Sudo-Mode-JWT` request header. | Field | Type | Description | | --- | --- | --- | @@ -837,7 +837,7 @@ A verified email is not required. ### JSON body -The body is a [sudo verification object](#sudo-verification-object). An existing sudo proof travels in the `X-Fluxer-Sudo-Mode-JWT` request header. +The body is a [sudo verification object](#sudo-verification-object). An existing sudo proof is sent in the `X-Fluxer-Sudo-Mode-JWT` request header. :::caution[Removing the final credential removes an authenticator] Deleting the account's last WebAuthn credential removes the WebAuthn authenticator type. Without TOTP the account then has no MFA authenticator, and sudo mode falls back to the account password. 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 b61027c85..be706222d 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 @@ -6,7 +6,7 @@ description: Proving control of a phone number over outbound SMS or an inbound c import RouteHeader from '@/components/RouteHeader.astro'; -Phone verification proves that the current account controls a phone number. The account either receives a one-time code over outbound SMS, or texts an issued challenge code to a Fluxer number. Success sets the account's verified phone state, which the phone [required actions](/http-api/users/#required-actions) and the `VERY_HIGH` guild [verification level](/http-api/guilds/#verification-levels) consult. +Phone verification proves that the current account controls a phone number. The account either receives a one-time code over outbound SMS, or texts an issued challenge code to a Fluxer number. Success sets the account's verified phone state, which the phone [required actions](/http-api/users/#required-actions) and the `VERY_HIGH` guild [verification level](/http-api/guilds/#verification-levels) check. Every route here needs a non-bot user session, and each one admits a session with suspicious account state. @@ -73,7 +73,7 @@ Keep phone numbers, verification codes, and challenge codes out of logs, analyti | Order | Condition | Verdict | | --- | --- | --- | -| 1 | The lookup could not be performed1 | Refused with `INVALID_PHONE_NUMBER` | +| 1 | The lookup could not be done1 | Refused with `INVALID_PHONE_NUMBER` | | 2 | The provider reports the number as invalid | Refused with `INVALID_PHONE_NUMBER` | | 3 | The line type is `fixedVoip` or `nonFixedVoip` | Inbound, reason `voip` | | 4 | The number starts with `+1` and its numbering plan area is Canadian | Inbound, reason `canadian` | @@ -235,7 +235,7 @@ Further controls bound outbound delivery to 3 sends per 6 hours for each account | 403 | [error response](/http-api/#error-response) | The account is not eligible and the request returns `PHONE_ADD_NOT_ELIGIBLE` | | 4291 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Risk controls, a provider throttle, or an account or per-number send control denies the request, which returns `PHONE_RATE_LIMIT_EXCEEDED` | -1 A phone denial uses the rate limit envelope with `code` set to `PHONE_RATE_LIMIT_EXCEEDED` rather than the `RATE_LIMITED` an ordinary route denial returns +1 A phone denial uses the rate limit envelope with `code` set to `PHONE_RATE_LIMIT_EXCEEDED`. An ordinary route denial returns `RATE_LIMITED` On the 429, `X-RateLimit-Scope` reports `shared` when the per-number control or a number-scoped provider throttle produced the denial. @@ -304,7 +304,7 @@ A number can complete verification twice, and each success refreshes that record | 403 | [error response](/http-api/#error-response) | The account is not eligible and the request returns `PHONE_ADD_NOT_ELIGIBLE` | | 4291 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | A provider throttle denies the request, which returns `PHONE_RATE_LIMIT_EXCEEDED` | -1 A phone denial uses the rate limit envelope with `code` set to `PHONE_RATE_LIMIT_EXCEEDED` rather than the `RATE_LIMITED` an ordinary route denial returns +1 A phone denial uses the rate limit envelope with `code` set to `PHONE_RATE_LIMIT_EXCEEDED`. An ordinary route denial returns `RATE_LIMITED` On the 429, `X-RateLimit-Scope` reports `shared` when a number-scoped provider throttle produced the denial. @@ -353,7 +353,7 @@ The body is an empty object. Fluxer reads an absent or empty body as an empty ob | Status | Body | Condition | | --- | --- | --- | -| 200 | [user](/http-api/users/#user-object) object | The escape ran, wholly or up to the per-request limit | +| 200 | [user](/http-api/users/#user-object) object | The escape ran, fully or up to the per-request limit | | 400 | [error response](/http-api/#error-response) | The escape is unavailable and the request returns `PHONE_GATE_ESCAPE_UNAVAILABLE` | ### Side effects 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 548116ee5..7771b5363 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 @@ -78,9 +78,9 @@ Fluxer omits an open entry whose channel no longer lists the caller among its re Opens a direct message with one account, or creates a group DM. Returns a [channel](/http-api/channels/#channel-object) object on success. Emits [Channel Create](/gateway/events/#channel-create) and [Message Create](/gateway/events/#message-create) Gateway events. -One body field selects the channel type. `recipient_id` opens a direct message. `recipients` always creates a group DM whatever its length, so a single-element array creates a two-participant group DM, and an empty array creates a group DM whose only participant is the caller. +One body field selects the channel type. `recipient_id` opens a direct message. `recipients` always creates a group DM whatever its length. A single-element array creates a two-participant group DM, and an empty array creates a group DM whose only participant is the caller. -A request supplying `recipients` additionally consumes the `user:group_dm:create` bucket and is subject to the CAPTCHA check below, whatever the resulting participant count. +A request supplying `recipients` also consumes the `user:group_dm:create` bucket and is subject to the CAPTCHA check below, whatever the resulting participant count. Instance policy can disable private channel creation entirely, in which case every request returns 400 `DIRECT_MESSAGES_DISABLED` after the body has been validated and the group DM bucket and CAPTCHA have been applied. @@ -93,7 +93,7 @@ A request naming the caller as its own target returns 400 `CANNOT_DM_YOURSELF` a Fluxer reads no block state, no friendship, and no shared guild on this route. A target that resolves is admitted, so the caller opens a direct message with an account that has blocked them. `CANNOT_SEND_MESSAGES_TO_USER` is reachable from [Create message](/http-api/messages/#create-message) alone, which applies the recipient's direct message policy on every send. :::caution[Blocking removes no channel from the blocked account] -Blocking refuses no creation and closes no channel. The blocked account still opens the pair's direct message through this route and receives 200. Fluxer refuses the send alone, with 400 `CANNOT_SEND_MESSAGES_TO_USER`. +Blocking closes no existing channel. The blocked account still opens the pair's direct message through this route and receives 200. Only the send is refused, with 400 `CANNOT_SEND_MESSAGES_TO_USER`. ::: :::caution[A SPAMMER caller receives a channel nobody sees] @@ -113,7 +113,7 @@ Fluxer then evaluates each other recipient independently and collects every fail An ordinary caller never adds a recipient across a block in either direction, because blocking deletes the friendship both ways and a friend request crosses no block. :::note[Group DM admission reads no block state] -Fluxer holds a bot caller to no friendship, so a recipient that has blocked the bot is added when the two share a guild and the recipient's [group DM add permission flags](/http-api/users/#group-dm-add-permission-flags) admit it. +Fluxer holds a bot caller to no friendship. A recipient that has blocked the bot is added when the two share a guild and the recipient's [group DM add permission flags](/http-api/users/#group-dm-add-permission-flags) admit it. ::: ### Request headers @@ -131,7 +131,7 @@ Fluxer holds a bot caller to no friendship, so a recipient that has blocked the | Field | Type | Description | | --- | --- | --- | -| recipient_id?1 | snowflake | The sole recipient of a direct message | +| recipient_id?1 | snowflake | The only recipient of a direct message | | recipients?1 | array[snowflake] | The other participants of a group DM, excluding the caller (max 49) | 1 Exactly one of the two is supplied. Supplying both or neither fails validation with the entry path `root` @@ -147,7 +147,7 @@ Fluxer holds a bot caller to no friendship, so a recipient that has blocked the ### Side effects -A newly created direct message opens for the caller only and emits [Channel Create](/gateway/events/#channel-create) to the caller. The recipient's side stays closed until they open it themselves, or until the pair becomes friends through [Accept request or block user](/http-api/users/relationships/#accept-request-or-block-user), which opens both sides. Reopening an existing direct message emits `CHANNEL_CREATE` to the caller unconditionally. +A newly created direct message opens for the caller only and emits [Channel Create](/gateway/events/#channel-create) to the caller. The recipient's side stays closed until they open it themselves, or until the pair becomes friends through [Accept request or block user](/http-api/users/relationships/#accept-request-or-block-user), which opens both sides. Reopening an existing direct message always emits `CHANNEL_CREATE` to the caller. Creating a group DM makes the caller its owner, adds the complete participant set, and emits [Channel Create](/gateway/events/#channel-create) to every participant. It creates one [RECIPIENT_ADD](/http-api/messages/#message-types) system message authored by the caller for each other recipient and emits [Message Create](/gateway/events/#message-create) for each to every participant. Initial creation emits no [Channel Recipient Add](/gateway/events/#channel-recipient-add), which is reserved for a recipient added later. A group DM created from an empty `recipients` array produces no system message. @@ -155,7 +155,7 @@ Creating a group DM makes the caller its owner, adds the complete participant se 40 requests per 10 seconds for each authenticated user, on the `user:channels` bucket, shared with [List private channels](#list-private-channels), [Pin private channel](#pin-private-channel), and [Unpin private channel](#unpin-private-channel). -A request supplying `recipients` additionally draws on the `user:group_dm:create` bucket of 10 requests per hour for each authenticated user, which is exempt from the global HTTP limit. +A request supplying `recipients` also draws on the `user:group_dm:create` bucket of 10 requests per hour for each authenticated user, which is exempt from the global HTTP limit. ## Preload private channel messages 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 cf5213a6c..5eb7c32f0 100644 --- a/fluxer_docs/src/content/docs/http-api/users/relationships.mdx +++ b/fluxer_docs/src/content/docs/http-api/users/relationships.mdx @@ -14,7 +14,7 @@ The routes here are user-only. A bot or OAuth2 bearer credential receives 403 `A A friendship is a FRIEND record on both accounts. A pending request is `OUTGOING_REQUEST` on the sender and `INCOMING_REQUEST` on the recipient. A block exists only on the account that created it, so the blocked account observes nothing. ::: -A deleted account has the deleted flag with no pending deletion timestamp. An account inside a scheduled deletion window has a timestamp, so it stays a valid friend request target. +A deleted account has the deleted flag with no pending deletion timestamp, and is not a valid friend request target. An account inside a scheduled deletion window has a timestamp, so it stays a valid target. ## Relationship object @@ -86,7 +86,7 @@ A bulk ignore result reports how many incoming friend requests one call removed. ## Relationship limit -The `max_relationships` limit governs the combined number of friendship, block, and pending request records one account holds. Fluxer resolves it from the account's traits and premium state, so the value can be higher for a premium account. The fallback is 1000 when the deployment resolves no rule. The registry entry is listed under [limit keys](/http-api/instance/#limit-keys). +The `max_relationships` limit controls the combined number of friendship, block, and pending request records one account holds. Fluxer resolves it from the account's traits and premium state, so the value can be higher for a premium account. The fallback is 1000 when the deployment resolves no rule. The registry entry is listed under [limit keys](/http-api/instance/#limit-keys). Fluxer refuses the write once the stored record count reaches the resolved value. It checks the caller and the target independently, and a request fails when either has reached that value. @@ -162,7 +162,7 @@ Admission is ordered. The deployment direct message policy runs first, so an ord The caller's own ID returns 400 `CANNOT_SEND_FRIEND_REQUEST_TO_SELF`. An unresolved target returns 404 `UNKNOWN_USER`. A deleted target returns 400 `FRIEND_REQUEST_BLOCKED`. -Fluxer consults the current relationship state next. A pending request from the target is accepted. An existing friendship or outgoing request is returned unchanged. The rules that follow apply to a genuinely new request alone, so they never refuse an existing friend. +Fluxer checks the current relationship state next. A pending request from the target is accepted. An existing friendship or outgoing request is returned unchanged. The rules that follow apply to a genuinely new request alone, so they never refuse an existing friend. The remaining rules reject an unclaimed caller with 400 `UNCLAIMED_ACCOUNT_CANNOT_SEND_FRIEND_REQUESTS` and an unverified email with 403 `FRIEND_REQUEST_EMAIL_VERIFICATION_REQUIRED`. A bot target is rejected with 400 `FRIEND_REQUEST_BLOCKED` unless it has `FRIENDLY_BOT`. A target with the internal app store reviewer flag is rejected with the same code. @@ -171,7 +171,7 @@ A target the caller has blocked returns 400 `CANNOT_SEND_FRIEND_REQUEST_TO_BLOCK 1 The friend source evaluation is skipped when the target has no stored settings, which admits the request as though the target permitted requests from anyone :::caution[A flagged caller receives an indistinguishable success] -A caller with the `SPAMMER` [public user flag](/http-api/users/#public-user-flags), or one whose send trips the deployment's direct contact spam policy, gets a one-sided request. Fluxer writes only the caller's `OUTGOING_REQUEST` record, and [Relationship Add](/gateway/events/#relationship-add) reaches the caller alone. The response is an ordinary `OUTGOING_REQUEST` object. +A caller with the `SPAMMER` [public user flag](/http-api/users/#public-user-flags), or one whose send trips the deployment's direct contact spam policy, gets a one-sided request. Fluxer writes only the caller's `OUTGOING_REQUEST` record. [Relationship Add](/gateway/events/#relationship-add) reaches the caller alone. The response is an ordinary `OUTGOING_REQUEST` object. ::: 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. @@ -194,7 +194,7 @@ The body can be omitted, in which case it is treated as an empty object. | --- | --- | --- | | staff_force_accept?1 | boolean | Whether the friendship is created immediately without a pending request | -1 Honoured only when the caller has the `STAFF` flag, and a caller without it is treated as though the member were absent. The stored flag governs, so hidden staff status still qualifies +1 Honoured only when the caller has the `STAFF` flag, and a caller without it is treated as though the member were absent. Fluxer reads the stored flag, so hidden staff status still qualifies ### Response @@ -261,7 +261,7 @@ The body can be omitted, in which case it is treated as an empty object and acce Acceptance replaces the pending records with FRIEND records on both accounts and emits [Relationship Update](/gateway/events/#relationship-update) to each. It then opens the pair's direct message channel for both accounts, creating the channel when none exists, and emits [Channel Create](/gateway/events/#channel-create) to each account whose open state changed. No other relationship operation opens one. -Blocking replaces the caller's existing friendship or pending request with a BLOCKED record. Removing a friendship or an outgoing request also removes the target's reciprocal record and emits [Relationship Remove](/gateway/events/#relationship-remove) to the target. Removing an incoming request touches the caller's record alone. [Relationship Add](/gateway/events/#relationship-add) then reaches the caller for the block. Blocking an already blocked target changes nothing. Blocking never closes or deletes the pair's direct message channel. +Blocking replaces the caller's existing friendship or pending request with a BLOCKED record. Removing a friendship or an outgoing request also removes the target's reciprocal record, and [Relationship Remove](/gateway/events/#relationship-remove) reaches the target. Removing an incoming request touches the caller's record alone. [Relationship Add](/gateway/events/#relationship-add) then reaches the caller for the block. Blocking an already blocked target changes nothing. Blocking never closes or deletes the pair's direct message channel. ### Rate limit 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 ffd23d172..e34c9f533 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 @@ -4,13 +4,13 @@ title: User settings Protobuf description: The client preference snapshot in the user settings object. --- -A synced preferences snapshot holds the client settings an account shares between its devices. It travels as one base64-encoded `fluxer.user.preferences.v1.SyncedPreferences` message in the `synced_preferences` field of the [user settings object](/http-api/users/#user-settings-object). Fluxer stores it and interprets no field on this page. +A synced preferences snapshot holds the client settings an account shares between its devices. It is one base64-encoded `fluxer.user.preferences.v1.SyncedPreferences` message in the `synced_preferences` field of the [user settings object](/http-api/users/#user-settings-object). Fluxer stores it and interprets no field on this page. ## Reading and writing the snapshot Decode the string before reading any preference, and encode a valid `SyncedPreferences` message when writing one. [Modify current user settings](/http-api/users/settings/#modify-current-user-settings) takes the complete snapshot every time. -The empty string in a response means nothing is stored. Sending null clears the stored snapshot, and so does sending the empty string. A stored snapshot reaches the account's other sessions through [User Settings Update](/gateway/events/#user-settings-update). +The empty string in a response means nothing is stored. Sending null or the empty string clears it. A stored snapshot reaches the account's other sessions through [User Settings Update](/gateway/events/#user-settings-update). Fluxer decodes every submitted snapshot and re-encodes it in canonical form before storing it, so a read can return a different string from the one submitted. Known fields are emitted in ascending field number order, and an unrecognised field number is preserved and re-emitted after them. Enums here are open, and an unassigned numeric value survives the round trip. When every known field holds its zero value and no unrecognised field is present, the snapshot encodes to zero bytes and is stored as the empty string. @@ -20,7 +20,7 @@ A submission may use either the standard or the URL-safe base64 alphabet, with o Types here are the declared Protobuf types. A field whose declared type is an enumeration appears as `int32`, its wire representation, and the Description column links the enumeration. -A field without the optional marker always decodes and holds its Protobuf zero value: false for a bool, 0 for a numeric type, the empty string, an empty repeated field, or an empty map. A field with the optional marker tracks presence and is absent until a value is stored. Every singular message field tracks presence the same way, so a preference group is absent from the snapshot until a client writes to it. +A field without the optional marker always decodes and holds its Protobuf zero value. That is false for a bool, 0 for a numeric type, the empty string, an empty repeated field, or an empty map. A field with the optional marker tracks presence and is absent until a value is stored. Every singular message field tracks presence the same way, so a preference group is absent from the snapshot until a client writes to it. Fluxer applies no bound to any individual field of the message, so every length, range, and enumeration membership below describes what the first-party client writes and reads. @@ -92,7 +92,7 @@ The `SyncedPreferences` message is the root of the snapshot. Every field is a pr ## Accessibility settings object -The `accessibility` field has display, motion, message, media, voice, and interaction presentation preferences. Almost every field is optional, so distinguish an absent field from a stored zero value before applying a default. +The `accessibility` field has display, motion, message, media, voice, and interaction preferences. Almost every field is optional, so distinguish an absent field from a stored zero value before applying a default. ### Structure @@ -171,9 +171,9 @@ The `accessibility` field has display, motion, message, media, voice, and intera 5 The same enumeration as `animate_stickers` on the [user settings object](/http-api/users/#user-settings-object). This field is the mobile-local replacement -6 The complete CSS of the account's synced custom theme, stored inline. A client that has opted out of syncing its theme applies a local one instead and re-emits this value unchanged, so it does not clobber the devices that do sync +6 The complete CSS of the account's synced custom theme, stored inline. A client that has opted out of syncing its theme applies a local one instead and re-emits this value unchanged, so it does not overwrite the value on the devices that do sync -7 A multiplier, where 1 is unscaled. The first-party client keeps its zoom level in browser storage and neither reads nor writes this field +7 A multiplier, where 1 is unscaled. The first-party client keeps its zoom level in browser storage and does not read or write this field 8 A multiplier, where 1 is the unmodified speaking rate @@ -202,7 +202,7 @@ Field numbers 42 and 43 are reserved, together with the names `attachment_media_ | Value | Name | Description | | --- | --- | --- | | 0 | HDR_DISPLAY_MODE_UNSPECIFIED | No explicit mode is selected | -| 1 | HDR_DISPLAY_MODE_FULL | Display HDR media without constraining its range | +| 1 | HDR_DISPLAY_MODE_FULL | Display HDR media without limiting its range | | 2 | HDR_DISPLAY_MODE_STANDARD | Display HDR media using the standard presentation | ## Accessibility overrides object @@ -282,7 +282,7 @@ The `memes_picker` field records meme usage, favourites, and collapsed picker ca ## Emoji state object -The `emoji` field stores the selected emoji skin tone. +The `emoji` field holds the selected skin tone. ### Structure @@ -294,7 +294,7 @@ The `emoji` field stores the selected emoji skin tone. ## Emoji and sticker layout settings object -The `emoji_sticker_layout` field controls how the emoji and sticker pickers lay their contents out. +The `emoji_sticker_layout` field controls how the emoji and sticker pickers lay out their contents. ### Structure @@ -372,7 +372,7 @@ One descriptor addresses one encoding of one favourited GIF. It mirrors the [GIF ## Favourites state object -The `favorites` field stores favourite channels, the categories they are grouped into, and the presentation state of that grouping. These categories are private to the favourites view. +The `favorites` field stores favourite channels, the categories they are grouped into, and the display state of that grouping. These categories are private to the favourites view. ### Structure @@ -428,13 +428,13 @@ The `recent_mentions` field controls which mentions appear in the recent mention | include_roles?1 | bool | Whether to include role mentions | | include_guilds?1 2 | bool | Whether to include mentions in a guild channel | -1 The first-party client defaults every filter to true and omits the field while it holds that default, so a client that reads an absent value as the Protobuf zero filters out mentions the account expects to see +1 The first-party client defaults every filter to true and omits the field while it holds that default. A client that reads an absent value as the Protobuf zero then filters out mentions the account expects to see 2 False keeps direct message mentions and drops every mention whose channel belongs to a guild ## Sidebar preferences object -The `sidebar` field stores direct message sidebar presentation state. +The `sidebar` field stores direct message sidebar display state. ### Structure @@ -564,7 +564,7 @@ The `whats_new` field stores the most recently dismissed update entry. ## Privacy preferences object -The `privacy` field stores client privacy behaviour. Fluxer reads none of it, and none of it changes the privacy fields of the [user settings object](/http-api/users/#user-settings-object), which govern what Fluxer itself discloses about the account. +The `privacy` field stores client privacy behaviour. Fluxer reads none of it, and none of it changes the privacy fields of the [user settings object](/http-api/users/#user-settings-object), which control what Fluxer itself discloses about the account. ### Structure @@ -578,7 +578,7 @@ The `privacy` field stores client privacy behaviour. Fluxer reads none of it, an ## Local user spam overrides object -The `local_spam_overrides` field stores client-local classifications that override the [SPAMMER public user flag](/http-api/users/#public-user-flags) in presentation only. Fluxer neither reads these lists nor changes any flag because of them. +The `local_spam_overrides` field stores client-local classifications that override the [SPAMMER public user flag](/http-api/users/#public-user-flags) in display only. Fluxer neither reads these lists nor changes any flag because of them. ### Structure @@ -839,7 +839,7 @@ One combination describes the key, modifiers, and buttons that trigger a keybind 1 Resolves to Meta on macOS and to Control on every other platform, so one stored combination expresses the platform-native accelerator -2 Distinct from `enabled` on the [custom keybind](#custom-keybind-object) that owns the combination, and with the opposite default, because an absent value here reads as active +2 An absent value here reads as active, the opposite of the default for `enabled` on the [custom keybind](#custom-keybind-object) that owns the combination ## Chat input settings object 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 3c54fe960..48b91f647 100644 --- a/fluxer_docs/src/content/docs/http-api/users/settings.mdx +++ b/fluxer_docs/src/content/docs/http-api/users/settings.mdx @@ -86,9 +86,9 @@ Notification settings stored for one channel. | mute_config | ?[guild mute configuration](#guild-mute-configuration-object) object | The mute configuration of the channel, or null when none is stored | | unread_badges1 2 | ?integer | [Notification level](#notification-levels) for unread badges, or null to inherit | -1 The value controls client presentation and does not affect notification delivery +1 The value controls client display and does not affect notification delivery -2 Every response has the member, with null standing in for an unset level +2 Every response has the member, with null meaning no level is set ## Channel notification override input object @@ -128,11 +128,11 @@ Notification settings for one guild, or for every private channel when `guild_id 2 An override map holding no entry is serialised as null -3 The value controls client presentation and does not affect notification delivery +3 The value controls client display and does not affect notification delivery 4 The value increases on every stored write, including one that changes no other member, so a client can discard a stale [User Guild Settings Update](/gateway/events/#user-guild-settings-update) -5 Every response has the member, with null standing in for an unset level +5 Every response has the member, with null meaning no level is set ### Example @@ -209,7 +209,7 @@ Two further members are accepted and ignored. `guild_positions` is an array of a | animate_emoji? | boolean | Whether custom emoji animate | | animate_stickers? | integer | [Sticker animation setting](/http-api/users/#sticker-animation-settings) | | render_spoilers? | integer | [Spoiler rendering setting](/http-api/users/#spoiler-rendering-settings) | -| message_display_compact? | boolean | Whether messages use compact presentation | +| message_display_compact? | boolean | Whether messages use compact display | | flags?4 | integer | [Friend source flags](/http-api/users/#friend-source-flags) | | friend_source_flags?4 | integer | [Friend source flags](/http-api/users/#friend-source-flags) | | incoming_call_flags?5 | integer | [Incoming call flags](/http-api/users/#incoming-call-flags) | @@ -306,7 +306,7 @@ A custom status as submitted with a settings update. 2 The value is in the future, and a value at or before the current time is rejected -3 `emoji_name` is ignored, and not validated, when `emoji_id` has a value. An `emoji_id` of null leaves `emoji_name` in place, and it is then exactly one Unicode emoji +3 `emoji_name` is ignored without validation when `emoji_id` has a value. An `emoji_id` of null leaves `emoji_name` in place, and it is then exactly one Unicode emoji A custom emoji ID that does not resolve fails with `CUSTOM_EMOJI_NOT_FOUND` against `custom_status.emoji_id`. Fluxer drops the resolved emoji for an account without the global expression entitlement, so the request succeeds with text alone. @@ -360,7 +360,7 @@ An account with an outstanding required action can still call this route. An acc Modifies the account-wide settings and returns the complete [user settings](/http-api/users/#user-settings-object) object. Emits a [User Settings Update](/gateway/events/#user-settings-update) Gateway event. -An account with no stored settings record returns 404 `UNKNOWN_USER`. A locale change additionally emits a [User Update](/gateway/events/#user-update) Gateway event. +An account with no stored settings record returns 404 `UNKNOWN_USER`. A locale change also emits a [User Update](/gateway/events/#user-update) Gateway event. An unresolvable custom status emoji returns 400 `INVALID_FORM_BODY` with `CUSTOM_EMOJI_NOT_FOUND` against `custom_status.emoji_id`. The trusted domain, age restriction, and synced preferences decisions return 400 `VALIDATION_ERROR` with their own codes in `errors`. @@ -379,11 +379,11 @@ The body uses the [user settings update object](#user-settings-update-object). ### Side effects -Fluxer emits [User Settings Update](/gateway/events/#user-settings-update) to the caller's own sessions. A value-identical patch still performs the write and produces the Dispatch. A locale change also applies to email and notification localisation and emits [User Update](/gateway/events/#user-update). The user object has no locale member. +Fluxer emits [User Settings Update](/gateway/events/#user-settings-update) to the caller's own sessions. A value-identical patch still does the write and produces the Dispatch. A locale change also applies to email and notification localisation and emits [User Update](/gateway/events/#user-update). The user object has no locale member. Every [User Settings Update](/gateway/events/#user-settings-update) republishes the caller's current [Presence Update](/gateway/events/#presence-update) to eligible sessions, so a custom status change becomes visible without a further request. -A status of invisible forces every one of the caller's sessions to invisible. A later status of online, idle, or dnd releases them, and it does so only when at least one session is currently invisible. +A status of invisible forces every one of the caller's sessions to invisible. A later status of online, idle, or dnd releases them only when at least one session is currently invisible. ### Rate limit @@ -397,7 +397,7 @@ Changes the default voice activity sharing value, applies it to the caller's sid A missing account or settings record returns 404 `UNKNOWN_USER`. -This operation owns `default_share_voice_activity`, which is read-only in the [user settings object](/http-api/users/#user-settings-object). +Only this route writes `default_share_voice_activity`, which is read-only in the [user settings object](/http-api/users/#user-settings-object). ### JSON body @@ -421,7 +421,7 @@ Outside the window the cooldown restarts on every accepted request, including on ### Side effects -An accepted request writes `default_share_voice_activity` and then walks every friendship. A friendship whose caller-side value already equals the submitted value is skipped, and its stored `version` does not advance. Every friendship, skipped or rewritten, produces one [Relationship Update](/gateway/events/#relationship-update) to the caller and one to the friend, so re-sending the current value still fans the full set out. The friend's Dispatch is emitted only when the reciprocal friendship exists. +An accepted request writes `default_share_voice_activity` and then walks every friendship. A friendship whose caller-side value already equals the submitted value is skipped, and its stored `version` does not advance. Every friendship, skipped or rewritten, produces one [Relationship Update](/gateway/events/#relationship-update) to the caller and one to the friend, so re-sending the current value still sends the full set out. The friend's Dispatch is emitted only when the reciprocal friendship exists. It then records the change time in `last_voice_activity_sharing_change_at` and emits [User Update](/gateway/events/#user-update) to the caller. @@ -433,7 +433,7 @@ It then records the change time in `last_voice_activity_sharing_change_at` and e -Modifies and returns the [user guild settings](#user-guild-settings-object) object that governs direct messages and group DMs. Emits a [User Guild Settings Update](/gateway/events/#user-guild-settings-update) Gateway event on every success. +Modifies and returns the [user guild settings](#user-guild-settings-object) object that controls direct messages and group DMs. Emits a [User Guild Settings Update](/gateway/events/#user-guild-settings-update) Gateway event on every success. Fluxer creates the settings with the documented defaults when they are absent. @@ -449,7 +449,7 @@ The body uses the [guild settings update fields](#guild-settings-update-fields). ### Side effects -The private channel settings are returned with `guild_id` null. Fluxer performs the write and emits the Dispatch to the caller's own sessions even when the request changes nothing, and `version` advances on that write. No other account receives a Dispatch. +The private channel settings are returned with `guild_id` null. Fluxer does the write and emits the Dispatch to the caller's own sessions even when the request changes nothing, and `version` advances on that write. No other account receives a Dispatch. ### Rate limit @@ -489,7 +489,7 @@ The body uses the [guild settings update fields](#guild-settings-update-fields). ### Side effects -Fluxer performs the write and emits the Dispatch to the caller's own sessions even when the request changes nothing, and `version` advances on that write. No guild member other than the caller receives a Dispatch. +Fluxer does the write and emits the Dispatch to the caller's own sessions even when the request changes nothing, and `version` advances on that write. No guild member other than the caller receives a Dispatch. ### Rate limit diff --git a/fluxer_docs/src/content/docs/http-api/webhooks.mdx b/fluxer_docs/src/content/docs/http-api/webhooks.mdx index 651b4a303..53e3cdc2e 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, and [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 three 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. @@ -169,11 +169,11 @@ Every entry supplied in a JSON body is dropped, and it counts towards neither th ## Message references -A webhook execution accepts the [message reference input](/http-api/messages/#message-reference-input-object) object defined by the Messages resource. The constraints below are specific to webhooks. +A webhook execution accepts the [message reference input](/http-api/messages/#message-reference-input-object) object. The constraints below are specific to webhooks. The `channel_id` field is required for a forward reference and must equal the webhook's own channel. A value naming any other channel returns 404 `UNKNOWN_MESSAGE`. A reply reference always resolves in the webhook's own channel and ignores this field. -The referenced message must exist in the webhook's channel, and a missing message returns 404 `UNKNOWN_MESSAGE`. A reply reference additionally requires the referenced message to be an ordinary or reply message. Any other type returns 400 `INVALID_FORM_BODY` with the validation code `CANNOT_REPLY_TO_SYSTEM_MESSAGE`. +The referenced message must exist in the webhook's channel, and a missing message returns 404 `UNKNOWN_MESSAGE`. A reply reference also requires the referenced message to be an ordinary or reply message. Any other type returns 400 `INVALID_FORM_BODY` with the validation code `CANNOT_REPLY_TO_SYSTEM_MESSAGE`. A forward reference must not accompany content, embeds, or attachments, and one that does returns 400 `INVALID_FORM_BODY` with the validation code `FORWARD_MESSAGES_CANNOT_CONTAIN_CONTENT`. A missing `channel_id` or `message_id` on a forward reference returns the same status with `FORWARD_REFERENCE_REQUIRES_CHANNEL_AND_MESSAGE`. @@ -264,7 +264,7 @@ No per-embed title, description, footer, or field maximum applies to a Slack val The Slack schema bounds only `username`, which must be 1 through 80 characters and rejects a longer value with `WEBHOOK_NAME_LENGTH_INVALID`. Fluxer applies no bound to any other Slack string or to either array. -A conversion yielding neither content nor an embed returns 400 `CANNOT_SEND_EMPTY_MESSAGE`. Content longer than the effective maximum returns 400 `INVALID_FORM_BODY` with the validation code `CONTENT_EXCEEDS_MAX_LENGTH`, and more converted embeds than the resolved ceiling returns the same status with `TOO_MANY_EMBEDS`. Fluxer discards an attachment that produces no embed property. +A conversion yielding neither content nor an embed returns 400 `CANNOT_SEND_EMPTY_MESSAGE`. Content longer than the effective maximum returns 400 `INVALID_FORM_BODY` with the validation code `CONTENT_EXCEEDS_MAX_LENGTH`. More converted embeds than the resolved ceiling returns the same status with `TOO_MANY_EMBEDS`. Fluxer discards an attachment that produces no embed property. ## GitHub callback objects @@ -307,7 +307,7 @@ These objects define the body [Execute GitHub webhook](#execute-github-webhook) A top-level `pull_request` field selects the wording of an `issue_comment` rendering, which names a pull request when the field is present and an issue when it is absent. -Every GitHub string in these objects accepts at most 152,133 characters unless a narrower bound is stated, and every field typed as an absolute URL must parse as an `http` or `https` URL of at most 2,048 characters. A field marked as accepted and validated still has its declared type and bound, so a malformed value rejects the callback with 400 `INVALID_FORM_BODY`. +Every GitHub string in these objects accepts at most 152,133 characters unless a narrower bound is stated. Every field typed as an absolute URL must parse as an `http` or `https` URL of at most 2,048 characters. A field marked as accepted and validated still has its declared type and bound, so a malformed value rejects the callback with 400 `INVALID_FORM_BODY`. Every rendered embed title is truncated to 70 characters, except the ordinary push, check run, and check suite titles, which are truncated to 256. Every rendered embed description is truncated to 350 characters, except the forced push description, which is the fixed compare link and is neither decoded nor truncated. Fluxer decodes HTML entities and trims each value before truncating it. @@ -355,7 +355,7 @@ Every rendered embed title is truncated to 70 characters, except the ordinary pu | message2 | string | Commit message | | author | [GitHub author](#github-author-object) object | Commit author | -1 Renderings abbreviate the identifier to its first seven characters +1 Renderings shorten the identifier to its first seven characters 2 Every `This reverts commit .` sentence with a 40-character identifier is rewritten to a Markdown link to the reverted commit @@ -427,7 +427,7 @@ No other key is read. An unlisted key, including a `pull_request` marker that Gi | html_url | string | Absolute review URL | | state1 | string | Review state | -1 The field is accepted and validated but does not contribute to the rendered message, so an approval, a change request, and a comment review all render the same embed +1 The field is accepted and validated but does not change the rendered message, so an approval, a change request, and a comment review all render the same embed ### GitHub check pull request object @@ -492,7 +492,7 @@ The object exists only inside the `pull_requests` arrays of the [GitHub check su ## GitHub event types -The value of the `X-GitHub-Event` request header selects the rendering. An event type outside this registry, and an absent header, are both acknowledged without creating a message. Every listed type additionally requires repository. +The value of the `X-GitHub-Event` request header selects the rendering. An event type outside this registry, and an absent header, are both acknowledged without creating a message. Every listed type also requires repository. | Value | Description | | --- | --- | @@ -526,7 +526,7 @@ The value of the `X-GitHub-Event` request header selects the rendering. An event 5 A conclusion of `skipped` on the relevant suite suppresses the message -An ordinary push requires compare and at least one commit, and renders one line for each commit. The line set is not capped by count, and the assembled description is truncated to the ordinary 350-character ceiling. +An ordinary push requires compare and at least one commit, and renders one line for each commit. The line set is not capped by count, and the description built from them is truncated to the ordinary 350-character ceiling. ## Instatus callback objects @@ -725,7 +725,7 @@ Fluxer normalises an incident, maintenance, or component status by uppercasing i -Returns an array of [webhook objects](#webhook-object) in a guild. The caller must be a member of the guild and hold the [MANAGE_WEBHOOKS](/http-api/permissions/) permission at guild level. +Returns an array of [webhook objects](#webhook-object) in a guild. The caller must be a guild member holding [MANAGE_WEBHOOKS](/http-api/permissions/) at guild level. ### Path parameters @@ -739,7 +739,7 @@ Returns an array of [webhook objects](#webhook-object) in a guild. The caller mu | --- | --- | --- | | 2001 | array[[webhook](#webhook-object) object] | Webhooks were returned, and an empty array is returned when the guild has none | | 400 | [error response](/http-api/#error-response) | Path parameter is not a valid snowflake | -| 4002 | [error response](/http-api/#error-response) | The caller holds `MANAGE_WEBHOOKS` without an enrolled authenticator in an elevated-MFA guild | +| 4002 | [error response](/http-api/#error-response) | The caller holds `MANAGE_WEBHOOKS` with no enrolled authenticator in an elevated-MFA guild | | 4033 | [error response](/http-api/#error-response) | Credential is a bearer token | | 4033 | [error response](/http-api/#error-response) | The account has an outstanding required action | | 4033 | [error response](/http-api/#error-response) | The guild is unavailable | @@ -776,7 +776,7 @@ Returns an array of [webhook objects](#webhook-object) in a guild text or voice | --- | --- | --- | | 200 | array[[webhook](#webhook-object) object] | Webhooks were returned, and an empty array is returned when the channel has none | | 400 | [error response](/http-api/#error-response) | Path parameter is not a valid snowflake | -| 4001 | [error response](/http-api/#error-response) | The caller holds `MANAGE_WEBHOOKS` without an enrolled authenticator in an elevated-MFA guild | +| 4001 | [error response](/http-api/#error-response) | The caller holds `MANAGE_WEBHOOKS` with no enrolled authenticator in an elevated-MFA guild | | 4032 | [error response](/http-api/#error-response) | Credential is a bearer token | | 4032 | [error response](/http-api/#error-response) | The account has an outstanding required action | | 4032 | [error response](/http-api/#error-response) | The guild is unavailable | @@ -802,9 +802,9 @@ Creates a webhook in a guild text or voice channel and returns the new [webhook Creation emits a [Webhooks Update](/gateway/events/#webhooks-update). -Fluxer checks the request in a fixed order: the content filter, the rate limit, the credential, the body schema, channel access and `MANAGE_WEBHOOKS`, the guild allowance, the channel allowance, the name scan, and only then the avatar itself. +Fluxer checks the request in a fixed order: the content filter, the rate limit, the credential, the body schema, channel access and `MANAGE_WEBHOOKS`, the guild allowance, the channel allowance, and the name scan. The avatar itself is checked last. -The guild allowance is the resolved [max_webhooks_per_guild](/http-api/instance/#limit-keys) value for the guild, defaulting to 1000, and the channel allowance is the resolved [max_webhooks_per_channel](/http-api/instance/#limit-keys) value, defaulting to 15. +The guild allowance is the resolved [max_webhooks_per_guild](/http-api/instance/#limit-keys) value for the guild, defaulting to 1000. The channel allowance is the resolved [max_webhooks_per_channel](/http-api/instance/#limit-keys) value, defaulting to 15. ### Path parameters @@ -821,7 +821,7 @@ The guild allowance is the resolved [max_webhooks_per_guild](/http-api/instance/ 1 A leading data URI header is stripped at the first comma before decoding, and the base64 payload is 1 through 13981016 characters -The decoded bytes must be at most the resolved [avatar_max_size](/http-api/instance/#limit-keys) value, which is the 10 MiB ceiling of 10485760 bytes by default. The decoded image must be an accepted avatar upload format, and an animated AVIF is rejected. +The decoded bytes must be at most the resolved [avatar_max_size](/http-api/instance/#limit-keys) value, which is the 10 MiB ceiling of 10485760 bytes by default. The image must be an accepted avatar upload format, and an animated AVIF is rejected. ### Response @@ -830,7 +830,7 @@ The decoded bytes must be at most the resolved [avatar_max_size](/http-api/insta | 200 | [webhook](#webhook-object) object | Webhook was created | | 4001 | [error response](/http-api/#error-response) | Body or avatar is invalid | | 4002 | [error response](/http-api/#error-response) | The guild or channel webhook allowance is already reached | -| 400 | [error response](/http-api/#error-response) | The caller holds `MANAGE_WEBHOOKS` without an enrolled authenticator in an elevated-MFA guild | +| 400 | [error response](/http-api/#error-response) | The caller holds `MANAGE_WEBHOOKS` with no enrolled authenticator in an elevated-MFA guild | | 403 | [error response](/http-api/#error-response) | Credential is a bearer token | | 403 | [error response](/http-api/#error-response) | The account has an outstanding required action | | 403 | [error response](/http-api/#error-response) | The guild is unavailable | @@ -860,9 +860,9 @@ The decoded bytes must be at most the resolved [avatar_max_size](/http-api/insta ### Side effects -The operation consumes one guild and channel webhook slot, generates a 64-character execution token, and stores the optional avatar. It records a `WEBHOOK_CREATE` guild audit entry that names the created webhook, has the target channel in its metadata, and has the supplied reason. A failure to write that entry does not fail the request, and the webhook still exists. +A successful call consumes one guild and channel webhook slot, generates a 64-character execution token, and stores the optional avatar. It records a `WEBHOOK_CREATE` guild audit entry naming the created webhook, with the target channel in its metadata and the supplied reason. A failure to write that entry does not fail the request, and the webhook still exists. -It emits [Webhooks Update](/gateway/events/#webhooks-update) with the guild and channel IDs to guild sessions that can view the channel. A failed creation leaves no webhook and consumes no slot. +Guild sessions that can view the channel receive [Webhooks Update](/gateway/events/#webhooks-update) with the guild and channel IDs. A failed creation leaves no webhook and consumes no slot. ### Rate limit @@ -886,7 +886,7 @@ Returns a [webhook object](#webhook-object). The caller must be a member of the | --- | --- | --- | | 200 | [webhook](#webhook-object) object | Webhook was returned | | 400 | [error response](/http-api/#error-response) | Path parameter is not a valid snowflake | -| 4001 | [error response](/http-api/#error-response) | The caller holds `MANAGE_WEBHOOKS` without an enrolled authenticator in an elevated-MFA guild | +| 4001 | [error response](/http-api/#error-response) | The caller holds `MANAGE_WEBHOOKS` with no enrolled authenticator in an elevated-MFA guild | | 4032 | [error response](/http-api/#error-response) | Credential is a bearer token | | 4032 | [error response](/http-api/#error-response) | The account has an outstanding required action | | 4032 | [error response](/http-api/#error-response) | The caller is not a member of the webhook's guild or lacks `MANAGE_WEBHOOKS` in the webhook's channel | @@ -909,7 +909,7 @@ Updates a webhook and returns the modified [webhook object](#webhook-object). Em ### Limitations - The caller is a member of the webhook's guild and holds [MANAGE_WEBHOOKS](/http-api/permissions/) at guild level and in the webhook's current channel. -- Moving the webhook additionally requires channel access and that permission in the destination channel, which must belong to the same guild. +- Moving the webhook also requires channel access and that permission in the destination channel, which must belong to the same guild. - The operation accepts an audit reason. ### Path parameters @@ -931,7 +931,7 @@ Updates a webhook and returns the modified [webhook object](#webhook-object). Em 2 A destination equal to the current channel is a no-op, and any other destination is subject to the destination channel's own [max_webhooks_per_channel](/http-api/instance/#limit-keys) allowance :::caution[A move notifies the source channel only] -The Dispatch is emitted once and names the webhook's channel as it stood before the update. Sessions watching the destination channel receive nothing, so a client tracking a channel's webhook set refreshes it itself. +One Dispatch is emitted, naming the webhook's channel as it stood before the update. Sessions watching the destination channel receive nothing, so a client tracking a channel's webhook set refreshes it itself. ::: The returned webhook object has the destination channel. @@ -943,7 +943,7 @@ The returned webhook object has the destination channel. | 200 | [webhook](#webhook-object) object | Webhook was updated | | 400 | [error response](/http-api/#error-response) | Body or avatar is invalid | | 4001 | [error response](/http-api/#error-response) | The destination channel already holds its maximum webhooks | -| 400 | [error response](/http-api/#error-response) | The caller holds `MANAGE_WEBHOOKS` without an enrolled authenticator in an elevated-MFA guild | +| 400 | [error response](/http-api/#error-response) | The caller holds `MANAGE_WEBHOOKS` with no enrolled authenticator in an elevated-MFA guild | | 403 | [error response](/http-api/#error-response) | Credential is a bearer token | | 403 | [error response](/http-api/#error-response) | The account has an outstanding required action | | 403 | [error response](/http-api/#error-response) | The caller is not a member of the webhook's guild | @@ -973,7 +973,7 @@ The returned webhook object has the destination channel. ### Side effects -The operation replaces the supplied fields, uploading a new avatar when one is supplied and clearing the stored hash when avatar is null. It records a `WEBHOOK_UPDATE` guild audit entry containing the previous and next webhook snapshots, the webhook's channel in its metadata, and the supplied audit reason. +An update replaces the supplied fields, uploading a new avatar when one is supplied and clearing the stored hash when avatar is null. It records a `WEBHOOK_UPDATE` guild audit entry containing the previous and next webhook snapshots, the webhook's channel in its metadata, and the supplied audit reason. It emits [Webhooks Update](/gateway/events/#webhooks-update) with the guild ID and the webhook's previous channel ID to guild sessions that can view that channel. @@ -985,7 +985,7 @@ It emits [Webhooks Update](/gateway/events/#webhooks-update) with the guild ID a -Permanently deletes a webhook and returns 204 with an empty body on success. The caller must be a member of the webhook's guild and hold the [MANAGE_WEBHOOKS](/http-api/permissions/) permission at guild level and in the webhook's channel. The operation accepts an audit reason. +Permanently deletes a webhook and returns 204 with an empty body on success. The caller must be a member of the webhook's guild with [MANAGE_WEBHOOKS](/http-api/permissions/) at guild level and in the webhook's channel. The operation accepts an audit reason. Deletion emits a [Webhooks Update](/gateway/events/#webhooks-update). @@ -1005,7 +1005,7 @@ No operation restores the webhook or reissues its token. Messages it already cre | --- | --- | --- | | 204 | empty | Webhook was deleted | | 400 | [error response](/http-api/#error-response) | Path parameter is not a valid snowflake | -| 4001 | [error response](/http-api/#error-response) | The caller holds `MANAGE_WEBHOOKS` without an enrolled authenticator in an elevated-MFA guild | +| 4001 | [error response](/http-api/#error-response) | The caller holds `MANAGE_WEBHOOKS` with no enrolled authenticator in an elevated-MFA guild | | 4032 | [error response](/http-api/#error-response) | Credential is a bearer token | | 4032 | [error response](/http-api/#error-response) | The account has an outstanding required action | | 4032 | [error response](/http-api/#error-response) | The caller is not a member of the webhook's guild or lacks `MANAGE_WEBHOOKS` in the webhook's channel | @@ -1075,7 +1075,7 @@ A successful update emits a [Webhooks Update](/gateway/events/#webhooks-update). | name? | string | Replacement webhook name (1-80 characters) | | avatar?1 | ?base64 string | Base64-encoded replacement avatar, or null to remove the current avatar | -1 An omitted avatar leaves the stored hash unchanged and an explicit null clears it, exactly as in the authenticated form. The same encoding, size, and format rules as [create webhook](#create-webhook) apply +1 An omitted avatar leaves the stored hash unchanged and an explicit null clears it, as in [update webhook](#update-webhook). The same encoding, size, and format rules as [create webhook](#create-webhook) apply :::note[The token body is strict] Unlike [update webhook](#update-webhook), this body rejects any field it does not define. Sending `channel_id` or any other unknown key returns 400, so a token alone cannot move a webhook between channels. @@ -1179,7 +1179,7 @@ The JSON representation of the request body is a [webhook message body](#webhook A multipart execution uploads every direct file as the webhook's creating account, and as the deleted user account when that creator no longer exists. The upload requires `VIEW_CHANNEL`, `SEND_MESSAGES`, and `ATTACH_FILES` in the webhook's channel, so a body with a direct file returns 403 `MISSING_PERMISSIONS` when the uploading account no longer holds them. A body with no direct file is unaffected. -An attachment metadata entry whose `id` matches a supplied file index supplies that file's filename, title, description, flags, duration, and waveform. An entry whose `id` matches no supplied file and that has a `filename` returns 400 `INVALID_FORM_BODY` with the validation code `NO_FILE_FOR_ATTACHMENT_METADATA`. Two entries claiming the same file index return `DUPLICATE_ATTACHMENT_IDS_NOT_ALLOWED`. When the payload supplies no attachment metadata at all, one entry is synthesised for each file from its index and its own filename. +An attachment metadata entry whose `id` matches a supplied file index supplies that file's filename, title, description, flags, duration, and waveform. An entry whose `id` matches no supplied file and that has a `filename` returns 400 `INVALID_FORM_BODY` with the validation code `NO_FILE_FOR_ATTACHMENT_METADATA`. Two entries claiming the same file index return `DUPLICATE_ATTACHMENT_IDS_NOT_ALLOWED`. When the payload supplies no attachment metadata at all, one entry is built for each file from its index and its own filename. ### Response @@ -1206,7 +1206,7 @@ An attachment metadata entry whose `id` matches a supplied file index supplies t | Payload the webhook message schema rejects | `INVALID_MESSAGE_DATA` | | Embed or attachment count ceiling exceeded | `TOO_MANY_EMBEDS` or `TOO_MANY_FILES` | | Forward reference omits `channel_id` or `message_id` | `FORWARD_REFERENCE_REQUIRES_CHANNEL_AND_MESSAGE` | -| Forward reference accompanies content, embeds, or attachments | `FORWARD_MESSAGES_CANNOT_CONTAIN_CONTENT` | +| Forward reference comes with content, embeds, or attachments | `FORWARD_MESSAGES_CANNOT_CONTAIN_CONTENT` | | Reply reference names a system message | `CANNOT_REPLY_TO_SYSTEM_MESSAGE` | ### Side effects @@ -1306,7 +1306,7 @@ Any other type returns 400 `CANNOT_MODIFY_SYSTEM_WEBHOOK`, and a forward, which ### Side effects -The operation replaces the supplied fields, and a content change marks the message edited. An edit does not re-extract mentions. The stored mention lists are kept, and `allowed_mentions` is accepted and not read. Supplying embeds replaces the complete embed collection and revalidates every attachment reference the embeds make. The operation emits [Message Update](/gateway/events/#message-update) to sessions that can read the channel. +The operation replaces the supplied fields, and a content change marks the message edited. An edit does not re-extract mentions, so the stored mention lists are kept and `allowed_mentions` is accepted and ignored. Supplying embeds replaces the complete embed collection and rechecks every attachment reference the embeds make. It emits [Message Update](/gateway/events/#message-update) to sessions that can read the channel. ### Rate limit @@ -1395,7 +1395,7 @@ A callback that renders nothing, and one whose message creation fails, both leav ### Side effects -A renderable event creates one message with exactly one embed, authored under the fixed name `GitHub` with the bundled GitHub avatar, and emits [Message Create](/gateway/events/#message-create) to sessions that can read the channel. Mention parsing is disabled for the created message, so no mention in a commit message, issue title, or comment body notifies anyone. +A renderable event creates one message with exactly one embed, authored under the fixed name `GitHub` with the bundled GitHub avatar. Sessions that can read the channel receive [Message Create](/gateway/events/#message-create). Mention parsing is disabled for the created message, so no mention in a commit message, issue title, or comment body notifies anyone. An unrecognised event type, a recognised type whose required fields or action are absent, and a repeated non-empty delivery identifier all create nothing and emit no Dispatch. @@ -1435,17 +1435,17 @@ The body is a [Slack callback](#slack-callback-object) object. | 404 | [error response](/http-api/#error-response) | Webhook and token pair does not exist, returning `UNKNOWN_WEBHOOK` | | 404 | [error response](/http-api/#error-response) | The target channel is missing or is not a guild text or voice channel, returning `UNKNOWN_CHANNEL` | -1 The [error code](/http-api/errors/) is `CANNOT_SEND_EMPTY_MESSAGE` for a conversion that yields nothing, `CONTENT_EXCEEDS_MAX_LENGTH` for over-length content, and `INVALID_FORM_BODY` with the validation code `TOO_MANY_EMBEDS` for the embed ceiling +1 The [error code](/http-api/errors/) is `CANNOT_SEND_EMPTY_MESSAGE` for a conversion that yields nothing, `CONTENT_EXCEEDS_MAX_LENGTH` for content that is too long, and `INVALID_FORM_BODY` with the validation code `TOO_MANY_EMBEDS` for the embed ceiling :::note[The Slack callback answers in HTML] -The success body is the literal string `ok` under the `text/html` content type. It has no representation of the created message. +The success body is the literal string `ok` under the `text/html` content type, with no representation of the created message. ::: ### Side effects -The converted callback creates one webhook-authored message with the converted content and embeds, and emits [Message Create](/gateway/events/#message-create) to sessions that can read the channel. +Conversion creates one webhook-authored message with the converted content and embeds, and emits [Message Create](/gateway/events/#message-create) to sessions that can read the channel. -A supplied username replaces the author name on that message. A supplied `icon_url` that parses as an absolute URL is fetched through the media boundary and stored as the message avatar, and the stored webhook name and avatar do not change. When the URL cannot be fetched, the callback still succeeds and the message has no avatar override. +A supplied username replaces the author name on that message. A supplied `icon_url` that parses as an absolute URL is fetched through the media boundary and stored as the message avatar. The stored webhook name and avatar do not change. When the URL cannot be fetched, the callback still succeeds and the message has no avatar override. The webhook execution default applies, and every mention in the converted content is suppressed. The callback has no nonce, so a repeated callback creates a second message. diff --git a/fluxer_docs/src/content/docs/media-proxy/overview.md b/fluxer_docs/src/content/docs/media-proxy/overview.md index f825068dd..4bd96abc2 100644 --- a/fluxer_docs/src/content/docs/media-proxy/overview.md +++ b/fluxer_docs/src/content/docs/media-proxy/overview.md @@ -48,7 +48,7 @@ One Media Proxy process serves exactly one mode. The mode is fixed at startup an | Mode | Serves | | --- | --- | | `mp` | Attachments, signed external media, themes, entrance sounds, and every image asset route | -| `static` | Every read path as a raw object read from the static bucket, with no transformation and no SVG rasterisation1 | +| `static` | Every read route as a raw object read from the static bucket, with no transformation and no SVG rasterisation1 | | `upload` | The [upload relay](/media-proxy/upload-relay/), plus every `mp` read route from the same buckets | 1 A `static` mode endpoint also strips `X-Robots-Tag` from every response and sends no `Content-Disposition` @@ -59,7 +59,7 @@ Which published base URL serves which mode is a deployment choice. The reference ## Methods -Every read route accepts `GET` and `HEAD`. HEAD returns the same status and representation headers as GET with an empty body, and a `Range` on HEAD still selects 206 or 416. Any other method on a read path returns 405. +Every read route accepts `GET` and `HEAD`. HEAD returns the same status and representation headers as GET with an empty body, and a `Range` on HEAD still selects 206 or 416. Any other method on a read route returns 405. The relay path accepts `PUT`. Any other method there returns 405 with an `Allow` header. An unknown path returns 404. @@ -71,7 +71,7 @@ That answer uses the declared type and the filename alone, so an origin that mis ## Request headers -Only `Range` affects the representation a public read returns. `X-Forwarded-For` decides which address the [media access allowlist](#access-restrictions) evaluates, so it can turn a 200 into a 403 without changing the representation. Every other request header, including `Authorization`, `Cookie`, `Accept`, `If-Range`, `If-None-Match`, and `Origin`, is ignored on a public read route. +Only `Range` affects the representation a public read returns. `X-Forwarded-For` decides which address the [media access allowlist](#access-restrictions) evaluates, so it can turn a 200 into a 403 without changing the representation. Every other request header, including `Authorization`, `Cookie`, `Accept`, `Origin`, `If-Range`, `If-None-Match`, `If-Modified-Since`, `If-Match`, and `If-Unmodified-Since`, is ignored on a public read route. ### Common request headers @@ -84,8 +84,6 @@ Only `Range` affects the representation a public read returns. `X-Forwarded-For` 2 Read only when the allowlist gate is enabled and the peer address is a configured trusted proxy. A value from any other peer is ignored -The Media Proxy reads no `If-None-Match`, `If-Modified-Since`, `If-Range`, `If-Match`, or `If-Unmodified-Since`, and it sends no `ETag` or `Last-Modified` on a read route, so a cache revalidates a representation by fetching it again. - ## Access restrictions An operator MAY gate public reads on a CDN edge address allowlist. The gate is disabled by default. When it is enabled, the process fetches the Bunny edge address list at startup and refreshes it every 3600 seconds by default, and a process whose first fetch fails does not start. @@ -106,7 +104,7 @@ A Boolean is true only for case-insensitive `true` or the exact value `1`. Every ## Byte ranges -This contract governs every range the Media Proxy resolves itself. A range is recognised only when its unit is the exact lowercase `bytes`, so `bytes=0-99` selects an interval and `BYTES=0-99` is ignored. Surrounding spaces and tabs are trimmed. A range containing a comma names multiple ranges and is ignored. A malformed range is ignored and produces the complete representation. `HEAD` applies the same contract as `GET`. +This contract controls every range the Media Proxy resolves itself. A range is recognised only when its unit is the exact lowercase `bytes`, so `bytes=0-99` selects an interval and `BYTES=0-99` is ignored. Surrounding spaces and tabs are trimmed. A range containing a comma names multiple ranges and is ignored. A malformed range is ignored and produces the complete representation. `HEAD` applies the same contract as `GET`. | Value | Description | | --- | --- | @@ -118,13 +116,13 @@ A reversed range, a zero-length suffix, a start outside the representation, or a ### Ranges on the signed external route -The route forwards a range to the origin only when no transformation is requested. It sends the range verbatim when the value after `bytes=` is non-empty and every byte of it is an ASCII graphic character, so a multiple range reaches the origin and the origin decides how to answer it. A value with a space anywhere is dropped, and no range is sent. The route relays the origin partial response with the origin `Content-Range` unchanged. +The route forwards a range to the origin only when no transformation is requested. It sends the range verbatim when the value after `bytes=` is non-empty and every byte of it is an ASCII graphic character. A multiple range therefore reaches the origin, and the origin decides how to answer it. A value with a space anywhere is dropped, and no range is sent. The route relays the origin partial response with the origin `Content-Range` unchanged. -A transforming request forwards no range to the origin and applies the client range to the transformed bytes, so it still returns 206 or 416. On a non-transforming request, an origin 200 is relayed as that 200 when its declared type is trustworthy and the response is not SVG by declared type, filename, or leading bytes. The relayed 200 does not reapply the client range. Fluxer rasterises an SVG response and applies the client range to the rasterised bytes. +A transforming request forwards no range to the origin, and the client range applies to the transformed bytes, so it still returns 206 or 416. On a non-transforming request, an origin 200 is relayed as that 200 when its declared type is trustworthy and the response is not SVG by declared type, filename, or leading bytes. The relayed 200 does not reapply the client range. Fluxer rasterises an SVG response and applies the client range to the rasterised bytes. A trustworthy type is a normalised `image/`, `video/`, or `audio/` type other than `application/octet-stream`. An absent or empty `Content-Type`, `text/plain`, `application/pdf`, and `application/zip` are all untrustworthy. Fluxer buffers the body of a 200 under an untrustworthy type and applies the client range to those bytes, so that read returns 206. -The route fetches twice in exactly one case. When an origin answers a forwarded range with 206 under a declared SVG media type, Fluxer discards that partial response, fetches the whole object again without a range, rasterises it, and applies the client range to the rasterised bytes. +The route fetches twice in exactly one case. When an origin answers a forwarded range with 206 under a declared SVG media type, Fluxer discards that partial response and fetches the whole object again without a range. It rasterises the object and applies the client range to the rasterised bytes. :::caution[A mislabelled SVG reaches the client as bytes] The re-fetch tests the declared media type alone. An origin that answers a forwarded range with SVG bytes under another type produces a 206 of raw SVG under that type. @@ -143,7 +141,7 @@ Disposition follows that declared type, so SVG mislabelled as an image or video | Accept-Ranges | string | The literal `bytes` on every media representation | | Access-Control-Allow-Origin | string | The literal `*` on media responses | | Cache-Control4 | string | The browser cache policy for the representation | -| CDN-Cache-Control | string | The corresponding shared cache policy | +| CDN-Cache-Control | string | The matching shared cache policy | | Vary | string | The literal `Accept-Encoding` | | X-Content-Type-Options | string | The literal `nosniff` | | X-Robots-Tag5 | string | The indexing policy | @@ -157,13 +155,13 @@ Disposition follows that declared type, so SVG mislabelled as an image or video 4 An audio or video representation appends `no-transform`. A route-produced error uses `no-store` instead -5 The literal `noindex, nofollow, nosnippet, noimageindex, notranslate, max-snippet:0, max-image-preview:none, max-video-preview:0`. Present in `mp` and `upload` mode. A `static` mode endpoint removes it +5 The literal `noindex, nofollow, nosnippet, noimageindex, notranslate, max-snippet:0, max-image-preview:none, max-video-preview:0`. Present in `mp` and `upload` mode, and a `static` mode endpoint removes it 6 Absent from a media access allowlist rejection -Every successful media representation uses `Cache-Control: public, max-age=31536000` and `CDN-Cache-Control: public, max-age=31536000`. An audio or video representation adds `no-transform` to the browser-facing policy only. No response repeats its policy in an `Expires` header. [Cache policies](/media-proxy/responses-and-limits/#cache-policies) lists the responses that have no policy at all. +Every successful media representation uses `Cache-Control: public, max-age=31536000` and `CDN-Cache-Control: public, max-age=31536000`. An audio or video representation adds `no-transform` to `Cache-Control` only. No response repeats its policy in an `Expires` header. [Cache policies](/media-proxy/responses-and-limits/#cache-policies) lists every cache policy this surface sets, including the responses that set none. -The upload relay is the only route that returns an `ETag`, and it relays the object storage value for the stored object. +The upload relay is the only route that returns an `ETag`, and it relays the object storage value for the stored object. No read route sends an `ETag` or a `Last-Modified`, so a cache revalidates a representation by fetching it again. :::note[External media is cached for a year too] The signed path is derived from the target URL, so the bytes behind one unchanged target are cached for a year at both layers. @@ -185,7 +183,7 @@ Disposition follows the resolved media type. An image other than SVG and a video The disposition filename comes from the route. An attachment or signed external read uses the filename in the path or target URL. An image asset uses the path hash with any `a_` prefix stripped, followed by the canonical name of the path extension, so `/avatars/1/a_abcd1234.jpg` is offered as `abcd1234.jpeg`. -When `download` resolves to true and the served media type has a canonical extension the filename does not already use, the filename keeps its stem and takes that extension, so a PNG transformation of `holiday.jpg` is offered as `holiday.png`. When a filename is not safe as a quoted ASCII value, Fluxer sends a sanitised quoted fallback and an RFC 5987 `filename*` parameter. +When `download` resolves to true and the served media type has a canonical extension the filename does not already use, the filename keeps its stem and takes that extension. A PNG transformation of `holiday.jpg` is offered as `holiday.png`. When a filename is not safe as a quoted ASCII value, Fluxer sends a sanitised quoted fallback and an RFC 5987 `filename*` parameter. :::caution[A scriptable document is never inline] The attachment, image asset, and signed external routes rasterise SVG to WebP, so a browser does not execute the document in the Media Proxy origin. 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 3b44dd6c1..b2296168e 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 @@ -37,7 +37,7 @@ Every error a route produces uses `Cache-Control: no-store` and the standard [se ### Handling contract -The set of reason phrases is open. A client MUST branch on the HTTP status and MUST NOT parse, compare, or pattern-match the body, because a phrase can be reworded and a new condition can introduce one without a version change. A client MUST NOT expect a JSON body on a failure, and MUST NOT expect a failure to name the key, parameter, or field that caused it. +The set of reason phrases is open. A client MUST branch on the HTTP status and MUST NOT parse, compare, or pattern-match the body. A phrase can be reworded and a new condition can introduce one without a version change. A client MUST NOT expect a JSON body on a failure, and MUST NOT expect a failure to name the key, parameter, or field that caused it. ## Status registry @@ -88,7 +88,7 @@ Buffered external bodies share one endpoint budget of 500 MiB for every [work ad Decoded images are limited to 16,384 pixels on either edge and 268,435,456 pixels in total. Animated input is limited to 20,000 frames and 1,073,741,824 decoded pixels across all frames. No configuration changes these bounds. [Transformations](/media-proxy/transformations/#transformation-limits) defines the resulting failure statuses. -The upload relay limits a body to the smaller of the capability's declared maximum and the endpoint's configured body limit, which 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. +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. @@ -139,7 +139,7 @@ Every route-produced error response uses `Cache-Control: no-store`, and a succes ## Range response headers -A complete media response has `Accept-Ranges: bytes`, the representation `Content-Type`, and an exact `Content-Length`1. A 206 additionally sets `Content-Range` to the selected interval over the complete size and `Content-Length` to the selected byte count. A 416 has `Content-Range: bytes */{size}` and `Accept-Ranges: bytes` with an empty body. +A complete media response has `Accept-Ranges: bytes`, the representation `Content-Type`, and an exact `Content-Length`1. A 206 also sets `Content-Range` to the selected interval over the complete size and `Content-Length` to the selected byte count. A 416 has `Content-Range: bytes */{size}` and `Accept-Ranges: bytes` with an empty body. 1 A streamed signed external response omits `Content-Length` when the origin declared none diff --git a/fluxer_docs/src/content/docs/media-proxy/routes.mdx b/fluxer_docs/src/content/docs/media-proxy/routes.mdx index b384517bf..cf4c52e57 100644 --- a/fluxer_docs/src/content/docs/media-proxy/routes.mdx +++ b/fluxer_docs/src/content/docs/media-proxy/routes.mdx @@ -10,7 +10,7 @@ A Media Proxy read route resolves one path to one file and returns it. These pat ## Shared route contract -Every route here accepts `GET` and `HEAD`, reads no `Authorization` header, and has no request-count rate limit. HEAD returns the GET status and representation headers with an empty body, including 206 and 416. +[Methods](/media-proxy/overview/#methods) and [Authorisation](/media-proxy/overview/#authorisation) define the contract every route here uses. No route here has a request-count rate limit. [Selector parsing](/media-proxy/overview/#selector-parsing) defines query decoding. [Byte ranges](/media-proxy/overview/#byte-ranges) defines which ranges are recognised and which headers a 206 or 416 has. @@ -58,9 +58,9 @@ The path has no signature and no expiry, so anyone holding the URL reads the obj 1 Read only on this route, and an empty or unparsable value is ignored. Only lossless animated WebP output uses a value above 6 -A transformation begins when `width`, `height`, `format`, or `quality` is present, or when `animated` resolves to true. `download` and `effort` select none on their own. +[When a transformation runs](/media-proxy/transformations/#when-a-transformation-runs) defines the trigger. -Without a transformation Fluxer streams the object from the store and forwards the range to it. With a transformation it reads the complete object into memory first, and the range then applies to the transformed bytes. +Without a transformation Fluxer streams the object from the store and forwards the range to it. With one it reads the complete object into memory first, and the range applies to the transformed bytes. An SVG attachment with a transformation parameter is rasterised, and `format` and `quality` select the output as they do for any other source, defaulting to WebP. Without a transformation parameter, only an `mp` endpoint rasterises it, always as lossless WebP. An `upload` endpoint returns the stored SVG bytes. @@ -77,7 +77,7 @@ A video source requires an explicit image `format`. Another transformation param | 416 | empty | The range is unsatisfiable | | 504 | Gateway timeout | Transformation capacity was unavailable or the transformation exceeded its deadline | -The [common responses](#common-responses) also apply. +[Common responses](#common-responses) also apply. ## Get signed external media @@ -107,7 +107,7 @@ A field holds a signed path only when its source URL is external. A URL already A client MUST preserve both components exactly as issued and MUST NOT decode or reconstruct either one. The signature covers the target component alone, so adding or changing a query parameter does not invalidate it. A path with no `/` after `/external/` returns 400. :::caution[The signed path is the only authorisation] -Anyone holding a signed path fetches that external target through Fluxer for as long as the deployment secret is unchanged. The path has no expiry, user binding, or individual revocation. +Anyone holding a signed path fetches that external target through Fluxer for as long as the deployment secret is unchanged, with no expiry, user binding, or individual revocation. ::: ### Target validation @@ -118,7 +118,7 @@ The target must resolve to a public address. A hostname must be at most 253 byte A hostname whose lookup fails or resolves to no address returns 400. -The route follows at most five redirects. A further redirect, a repeated URL, or a redirect without a `Location` header returns 502. A redirect to a rejected target returns 400. +A redirect beyond the [five-redirect bound](/media-proxy/responses-and-limits/#request-and-media-limits), a repeated URL, or a redirect without a `Location` header returns 502. A redirect to a rejected target returns 400. ### Query parameters @@ -137,9 +137,9 @@ A transformation begins when `width`, `height`, `format`, or `quality` is presen A non-transforming request forwards the client range to the origin under the filter [byte ranges](/media-proxy/overview/#byte-ranges) defines, and relays an origin 206 with its `Content-Range` and `Content-Length` unchanged. An origin that ignores the range answers 200, and Fluxer streams that whole body through when the origin names an `image/`, `video/`, or `audio/` media type other than `image/svg+xml`. Fluxer buffers a transforming request, an SVG body, and any body whose declared media type is empty, `application/octet-stream`, or outside those categories. A buffered response has the range applied to the bytes the route finally serves, so a client range over a buffered origin 200 produces 206. -Buffering reserves the whole body from a process-wide external buffer budget of 500 MiB for each configured transform slot and queue slot, plus 512 KiB. A reservation the budget cannot cover returns 503. +A body the [external buffer budget](/media-proxy/responses-and-limits/#request-and-media-limits) cannot cover returns 503. -Fluxer answers a `HEAD` with no transformation and no range from an origin `HEAD` when that origin returns 200, declares a `Content-Length` of at most 500 MiB, and names a non-SVG media type. Any other `HEAD` runs the `GET` path and returns its headers with an empty body. +This is the one route whose `HEAD` can answer differently from its `GET`. A `HEAD` with no transformation and no range is served from an origin `HEAD` when that origin returns 200, declares a `Content-Length` of at most 500 MiB, and names a non-SVG media type. Any other `HEAD` runs the `GET` path and returns its headers with an empty body. [Methods](/media-proxy/overview/#methods) defines how that origin `HEAD` resolves the media type. A video target requires an explicit image `format` to produce a thumbnail. Without one, Fluxer returns the original bytes unchanged, and it does the same for a target that is neither an image nor a video. @@ -159,7 +159,7 @@ A video target requires an explicit image `format` to produce a thumbnail. Witho 1 A forwarded range relays the origin `Content-Range` and `Content-Length` unchanged and omits either header the origin did not send, so a chunked origin 206 produces a 206 with no `Content-Length`. A local range over buffered or transformed bytes always has both -An origin status of 400, 401, 403, 404, 405, 406, 408, 409, 410, 411, 412, 413, 414, 415, 416, 428, or 429 reaches the client unchanged as an upstream fetch failure. The origin body and headers are not returned. Any other unsuccessful origin status becomes 502. The [common responses](#common-responses) also apply. +An origin status of 400, 401, 403, 404, 405, 406, 408, 409, 410, 411, 412, 413, 414, 415, 416, 428, or 429 reaches the client unchanged as an upstream fetch failure, and the origin body and headers are not returned. Any other unsuccessful origin status becomes 502. [Common responses](#common-responses) also apply. ### Side effects @@ -190,7 +190,7 @@ The route selects no representation and accepts a `Range`. | 400 | Bad request | The decoded theme key is invalid | | 416 | empty | The range is unsatisfiable | -The [common responses](#common-responses) also apply. A path that does not end in `.css` is not a theme path and falls through to 404. +[Common responses](#common-responses) also apply. A path that does not end in `.css` is not a theme path and falls through to 404. ## Get entrance sound @@ -219,11 +219,11 @@ The route selects no representation, accepts a `Range`, and sends no `Content-Di | 206 | Selected audio bytes | One range is satisfiable | | 416 | empty | The range is unsatisfiable | -The [common responses](#common-responses) also apply. +[Common responses](#common-responses) also apply. ## Image asset contract -An image asset is a stored picture Fluxer serves at a requested size and format, such as an avatar, a guild icon, or an emoji. The asset hash comes from the resource object, and the path combines the owning resource, that hash, and a file extension. On an emoji or sticker path the filename is the identifier itself. The grammar, query parameters, and responses below govern every image asset route. [Asset size selection](/media-proxy/transformations/#asset-size-selection) defines every size class. +An image asset is a stored picture Fluxer serves at a requested size and format, such as an avatar, a guild icon, or an emoji. The asset hash comes from the resource object, and the path combines the owning resource, that hash, and a file extension. On an emoji or sticker path the filename is the identifier itself. The grammar, query parameters, and responses below apply to every image asset route. [Asset size selection](/media-proxy/transformations/#asset-size-selection) defines every size class. A client builds the complete URL from the Media Proxy [base URL](/media-proxy/overview/#base-urls), the path template for the asset class, the hash, and a file extension. @@ -273,7 +273,7 @@ An owner segment of the literal `.` or `..` parses as an asset path but produces 1 A failed transcode whose stored media type begins with `image/` and is not `image/avif`, `image/heic`, `image/heif`, or SVG instead returns 200 with the original stored bytes and media type but without `Content-Disposition` -The [common responses](#common-responses) also apply. [Transformations](/media-proxy/transformations/) defines geometry, animation, output formats, and original representation selection. +[Common responses](#common-responses) also apply. [Transformations](/media-proxy/transformations/) defines geometry, animation, output formats, and original representation selection. ## Get user avatar @@ -286,12 +286,12 @@ Returns the user avatar under the icon size class. | Field | Type | Description | | --- | --- | --- | | user_id | string | The owning user [snowflake](/snowflakes/) paired with the avatar hash | -| hash | string | The avatar hash, optionally prefixed with `a_` to default to animated output | +| hash | string | The avatar hash, under the [image asset contract](#image-asset-contract) | | ext | string | The path extension, which selects the output format | ### Response -The operation uses the shared [image asset response](#image-asset-response). +The [image asset response](#image-asset-response) applies. ## Get guild icon @@ -304,12 +304,12 @@ Returns the guild icon under the icon size class. | Field | Type | Description | | --- | --- | --- | | guild_id | string | The owning guild [snowflake](/snowflakes/) paired with the icon hash | -| hash | string | The icon hash, optionally prefixed with `a_` to default to animated output | +| hash | string | The icon hash, under the [image asset contract](#image-asset-contract) | | ext | string | The path extension, which selects the output format | ### Response -The operation uses the shared [image asset response](#image-asset-response). +The [image asset response](#image-asset-response) applies. ## Get instance branding @@ -322,12 +322,12 @@ Returns the instance branding image written by [Create branding asset](/admin-ap | Field | Type | Description | | --- | --- | --- | | entity_id | string | The instance branding entity key supplied by the branding URL | -| hash | string | The branding hash, optionally prefixed with `a_` to default to animated output | +| hash | string | The branding hash, under the [image asset contract](#image-asset-contract) | | ext | string | The path extension, which selects the output format | ### Response -The operation uses the shared [image asset response](#image-asset-response). +The [image asset response](#image-asset-response) applies. ## Get owner banner @@ -340,12 +340,12 @@ Returns the owner banner under the banner size class. | Field | Type | Description | | --- | --- | --- | | owner_id | string | The owning user or guild [snowflake](/snowflakes/) paired with the banner hash | -| hash | string | The banner hash, optionally prefixed with `a_` to default to animated output | +| hash | string | The banner hash, under the [image asset contract](#image-asset-contract) | | ext | string | The path extension, which selects the output format | ### Response -The operation uses the shared [image asset response](#image-asset-response). +The [image asset response](#image-asset-response) applies. ## Get guild splash @@ -358,12 +358,12 @@ Returns the guild splash under the banner size class. | Field | Type | Description | | --- | --- | --- | | guild_id | string | The owning guild [snowflake](/snowflakes/) paired with the splash hash | -| hash | string | The splash hash, optionally prefixed with `a_` to default to animated output | +| hash | string | The splash hash, under the [image asset contract](#image-asset-contract) | | ext | string | The path extension, which selects the output format | ### Response -The operation uses the shared [image asset response](#image-asset-response). +The [image asset response](#image-asset-response) applies. ## Get guild embed splash @@ -376,12 +376,12 @@ Returns the guild embed splash under the banner size class. | Field | Type | Description | | --- | --- | --- | | guild_id | string | The owning guild [snowflake](/snowflakes/) paired with the embed splash hash | -| hash | string | The embed splash hash, optionally prefixed with `a_` to default to animated output | +| hash | string | The embed splash hash, under the [image asset contract](#image-asset-contract) | | ext | string | The path extension, which selects the output format | ### Response -The operation uses the shared [image asset response](#image-asset-response). +The [image asset response](#image-asset-response) applies. ## Get guild member avatar @@ -395,12 +395,12 @@ Returns the guild member avatar under the icon size class. | --- | --- | --- | | guild_id | string | The guild [snowflake](/snowflakes/) owning the member asset | | user_id | string | The member user [snowflake](/snowflakes/) paired with the member avatar hash | -| hash | string | The member avatar hash, optionally prefixed with `a_` to default to animated output | +| hash | string | The member avatar hash, under the [image asset contract](#image-asset-contract) | | ext | string | The path extension, which selects the output format | ### Response -The operation uses the shared [image asset response](#image-asset-response). +The [image asset response](#image-asset-response) applies. ## Get guild member banner @@ -414,12 +414,12 @@ Returns the guild member banner under the banner size class. | --- | --- | --- | | guild_id | string | The guild [snowflake](/snowflakes/) owning the member asset | | user_id | string | The member user [snowflake](/snowflakes/) paired with the member banner hash | -| hash | string | The member banner hash, optionally prefixed with `a_` to default to animated output | +| hash | string | The member banner hash, under the [image asset contract](#image-asset-contract) | | ext | string | The path extension, which selects the output format | ### Response -The operation uses the shared [image asset response](#image-asset-response). +The [image asset response](#image-asset-response) applies. ## Get emoji image @@ -442,7 +442,7 @@ Fluxer issues emoji paths without an `a_` prefix, so an emoji request defaults t ### Response -The operation uses the shared [image asset response](#image-asset-response). +The [image asset response](#image-asset-response) applies. ## Get sticker image @@ -463,7 +463,7 @@ Fluxer issues sticker paths without an `a_` prefix, so a sticker request default ### Response -The operation uses the shared [image asset response](#image-asset-response). +The [image asset response](#image-asset-response) applies. ## Get static object @@ -490,7 +490,7 @@ The complete request path is the storage key after percent-decoding. The route a | 400 | Bad request | The decoded key is invalid | | 416 | empty | The range is unsatisfiable | -The [common responses](#common-responses) also apply. +[Common responses](#common-responses) also apply. ## Operator and internal endpoints diff --git a/fluxer_docs/src/content/docs/media-proxy/transformations.md b/fluxer_docs/src/content/docs/media-proxy/transformations.md index 1990bc951..d5085f004 100644 --- a/fluxer_docs/src/content/docs/media-proxy/transformations.md +++ b/fluxer_docs/src/content/docs/media-proxy/transformations.md @@ -33,7 +33,7 @@ The Media Proxy canonicalises nothing and issues no redirect, so two spellings o | effort?5 | integer | The WebP encoder effort for an attachment request | | download? | boolean | Whether the response uses attachment disposition | -1 An empty, unparsable, zero, or over-bound value returns 400. Any integer from 1 through 16,384 is accepted +1 An empty, unparsable, zero, or too large value returns 400. Any integer from 1 through 16,384 is accepted 2 No value is rejected. An absent or unparsable value selects 128, a parsable value snaps to the ladder, and the result is then clamped by [asset size selection](#asset-size-selection) @@ -57,7 +57,7 @@ Transformations never enlarge an image. `width` alone scales proportionally to the requested width, and `height` alone scales proportionally to the requested height. A fit inside a rectangle uses the smaller of the two ratios, so a dimension requested larger than the source still shrinks when the other requested dimension is smaller than the source. -A cover crop scales a still image to cover the requested rectangle and crops it centrally. The Media Proxy applies one to an attachment or signed external request that supplies both `width` and `height`, and to every emoji or sticker asset. Every other image asset fits inside the selected square and preserves its full aspect ratio. +A cover crop scales a still image to cover the requested rectangle and crops it centrally. It applies to an attachment or signed external request that supplies both `width` and `height`, and to every emoji or sticker asset. Every other image asset fits inside the selected square and preserves its full aspect ratio. :::note[An animated transformation fits the whole frame] The Media Proxy downgrades a cover crop to a plain fit whenever it opens the decoder for every page. An animated emoji or sticker is fitted inside its square. @@ -158,13 +158,13 @@ The Media Proxy returns the original bytes when the source already has the selec An `effort` value forces encoding, and a `quality` value forces encoding for every source except GIF. With neither `width` nor `height`, an animated attachment or signed external request for the source's own GIF, WebP, or APNG format bypasses both tests and can still reuse the original animation. -The Media Proxy derives the response `Content-Type` from the content when the stored media type is empty, is case-insensitively `application/octet-stream`, or is outside the set `image/jpeg`, `image/png`, `image/webp`, `image/gif`, `image/apng`, `image/avif`, `image/heic`, `image/heif`, `image/jxl`, and `image/svg+xml`. An original response can therefore use a different media type from the stored metadata. A stored media type from that set is trusted even when it disagrees with the bytes and is served unchanged. +The response `Content-Type` comes from the content when the stored media type is empty, is case-insensitively `application/octet-stream`, or is outside the set `image/jpeg`, `image/png`, `image/webp`, `image/gif`, `image/apng`, `image/avif`, `image/heic`, `image/heif`, `image/jxl`, and `image/svg+xml`. An original response can therefore use a different media type from the stored metadata. A stored media type from that set is trusted even when it disagrees with the bytes and is served unchanged. ## Transformation limits -Proxied or stored media is limited to 500 MiB. A stored object above that bound returns 413, and so does an external origin that declares or delivers more. The same bound applies to a transforming request and to every buffer it produces. +A stored object above the [500 MiB media bound](/media-proxy/responses-and-limits/#request-and-media-limits) returns 413, and so does an external origin that declares or delivers more. The same bound applies to a transforming request and to every buffer it produces. -Decoded images are limited to 16,384 pixels on either edge and 268,435,456 pixels in total. Animated input is additionally limited to 20,000 decoded frames and 1,073,741,824 decoded pixels across all frames. These bounds are fixed. Exceeding a decoded image or animation limit fails the transformation. +Decoded images are limited to 16,384 pixels on either edge and 268,435,456 pixels in total. Animated input is also limited to 20,000 decoded frames and 1,073,741,824 decoded pixels across all frames. These bounds are fixed. Exceeding a decoded image or animation limit fails the transformation. Animated WebP and animated APNG output is bounded again at encode time, and exceeding one of those bounds truncates the output. The encoder stops adding frames after 20,000 frames, or once the accumulated frame delays reach 30,000 ms of playback, and emits the frames it already has. An operator can configure the frame cap from 1 through 100,000 and the playback cap from 100 through 600,000 ms. Animated GIF output has neither cap on either of its paths, so the decode limits above are its only bound. diff --git a/fluxer_docs/src/content/docs/operator/configuration.mdx b/fluxer_docs/src/content/docs/operator/configuration.mdx index 9914b711d..6da7a5220 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, so read it when you want to change something. +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. The installer in [Get started](/operator/get-started/) writes `.env` for you and fills in every secret. [Upgrading](/operator/upgrading/) covers moving between releases. @@ -25,7 +25,7 @@ POSTGRES_PASSWORD=ab$$cd POSTGRES_PASSWORD='ab$cd' ``` -Double quotes do not escape it. `docker compose config` prints a literal `$` back as `$$`, so a value that reads `ab$$cd` in that output is the correct one. The secrets the installer generates are hex or base64 and hold no `$`, so this reaches an instance through a password, an API key or an SMTP secret pasted in by hand. +Double quotes do not escape it. `docker compose config` prints a literal `$` back as `$$`. A value that reads `ab$$cd` in that output is the correct one. The secrets the installer generates are hex or base64 and hold no `$`, so this reaches an instance through a password, an API key or an SMTP secret pasted in by hand. A container's environment is fixed when the container is created, and `api` and `worker` cache their configuration at first load. Either way a change needs the process restarted, which `docker compose up -d` does by recreating the service. @@ -175,7 +175,7 @@ Nothing in the stack reads `X-Forwarded-Proto` or `X-Forwarded-Host`. Every abso It ships commented out. When it is unset, Compose builds those names from `FLUXER_PUBLIC_SCHEME` and `FLUXER_DOMAIN`, and each service puts `FLUXER_PUBLIC_PORT` back into the endpoints it derives. -A non-default port therefore needs nothing here. `FLUXER_PUBLIC_PORT` is where the port of the public address goes, and the rest of the stack follows it. The comment block under the first three lines of `.env.example` says what each layout needs. +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. @@ -305,7 +305,7 @@ No default. Which Compose files are loaded. Read by Docker Compose itself. Set i ## Client IP -The edge resolves one client address and rewrites `X-Forwarded-For` to it on every upstream hop, so no service reads what a visitor sent. When the peer is outside `FLUXER_EDGE_TRUSTED_PROXIES`, the edge uses the peer address and discards the header. When the peer is inside it, the edge takes the rightmost header entry that is not itself trusted, so a proxy that appends still delivers the real caller. A client whose own address is inside the list resolves to the proxy, which is why [Trusted proxies](/operator/reverse-proxy/#trusted-proxies) tells a LAN or VPN deployment to narrow it. +The edge resolves one client address per request and rewrites `X-Forwarded-For` to it on every upstream hop, so no service reads what a visitor sent. When the peer is outside `FLUXER_EDGE_TRUSTED_PROXIES`, the edge uses the peer address and discards the header. When the peer is inside the list, the edge takes the rightmost header entry that is not itself trusted, so a proxy that appends still delivers the real caller. A client whose own address is inside the list resolves to the proxy, which is why [Trusted proxies](/operator/reverse-proxy/#trusted-proxies) tells a LAN or VPN deployment to narrow it. All are optional. @@ -341,7 +341,7 @@ These three pick which container images Compose pulls. All are optional. | FLUXER_REGISTRY | `ghcr.io/${FLUXER_REGISTRY_OWNER}` | | FLUXER_IMAGE_TAG | `v1` | -`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 every Fluxer image uses and falls back to `v1`. +`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. @@ -693,7 +693,7 @@ Default `7881`. The TCP media port. Read by Compose only. Compose publishes it o Default `7882`. The UDP media port, published and passed to LiveKit as `rtc.udp_port` the same way. -LiveKit media does not traverse the edge. Compose publishes both media ports directly, so both must stay open in the host firewall and be forwarded to the host when it sits behind NAT. Compose points LiveKit at the webhook target `http://api:8080/webhooks/livekit` and configures no TURN server. A client that cannot use UDP falls back to ICE-TCP on the TCP port. +LiveKit media does not go through the edge. Compose publishes both media ports directly, so both must stay open in the host firewall and be forwarded to the host when it sits behind NAT. Compose points LiveKit at the webhook target `http://api:8080/webhooks/livekit` and configures no TURN server. A client that cannot use UDP falls back to ICE-TCP on the TCP port. 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. @@ -735,7 +735,7 @@ Default `60000`. Grace before culling a LiveKit-only participant. Milliseconds. 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. -The same two conditions turn on a DNS check at registration. The check runs when email is on by the rule above, so 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 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. #### `FLUXER_EMAIL_ENABLED` @@ -974,7 +974,7 @@ Default empty. The ipinfo key. Paired with `FLUXER_RISK_INTEGRATION_ENABLED`. #### `FLUXER_ACCOUNT_POLICY_DSL` -No default. The account risk policy. JSON. Malformed JSON fails startup, and a well-formed policy with unknown keys surfaces at use. +No default. The account risk policy. JSON. Malformed JSON fails startup, and an unknown key in a well-formed policy surfaces when the policy runs. #### `FLUXER_RISK_TOR_BLOCK_ALL_RELAYS` @@ -1040,7 +1040,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 list is empty out of the box, so the pre-screen does nothing until an operator fills it 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. +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. ## Limits @@ -1516,7 +1516,7 @@ The Gateway entrypoint derives the scheduler counts before the BEAM starts. It r 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 Gateway protocol version is `1`. The Gateway answers any other value in `?v=` with 101, then a close frame reading `Invalid API version`. +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. The Gateway reads every `FLUXER_GATEWAY_` name straight from the environment, so all of them work. @@ -1596,9 +1596,9 @@ Every name below goes in `.env`. `app-proxy` reads its environment at container `FLUXER_CSP_EXTRA_DEFAULT_SRC`, `FLUXER_CSP_EXTRA_CONNECT_SRC`, `FLUXER_CSP_EXTRA_IMG_SRC`, `FLUXER_CSP_EXTRA_MEDIA_SRC`, `FLUXER_CSP_EXTRA_FONT_SRC`, `FLUXER_CSP_EXTRA_SCRIPT_SRC`, `FLUXER_CSP_EXTRA_STYLE_SRC`, `FLUXER_CSP_EXTRA_FRAME_SRC`, `FLUXER_CSP_EXTRA_WORKER_SRC`, and `FLUXER_CSP_EXTRA_MANIFEST_SRC` take one or more sources separated by commas, spaces, tabs, or newlines. Blank entries and sources the directive already lists are dropped. `FLUXER_CSP_REPORT_URI` sets a single `report-uri` value. -`object-src`, `base-uri`, and `frame-ancestors` are fixed and have no override. `app-proxy` reads the discovery document and adds the static CDN endpoint, the media endpoint, and the origins of the configured branding images, so a stack on one hostname needs no extra sources. The usual reason to set one is a voice server on another hostname, which needs its WebSocket origin in `FLUXER_CSP_EXTRA_CONNECT_SRC`. [Voice media does not use the proxy](/operator/reverse-proxy/#voice-media-does-not-use-the-proxy) has that line in place. +`object-src`, `base-uri`, and `frame-ancestors` are fixed and have no override. `app-proxy` reads the discovery document and adds the static CDN endpoint, the media endpoint, and the origins of the configured branding images. A stack on one hostname therefore needs no extra sources. The usual reason to set one is a voice server on another hostname, which needs its WebSocket origin in `FLUXER_CSP_EXTRA_CONNECT_SRC`. [Voice media does not use the proxy](/operator/reverse-proxy/#voice-media-does-not-use-the-proxy) has that line in place. -A front proxy must not add a Content-Security-Policy of its own. +A front proxy must not add a [Content-Security-Policy of its own](/operator/reverse-proxy/#send-no-content-security-policy-of-its-own). ## Keys in .env.example that no service reads @@ -1759,7 +1759,7 @@ The edge and LiveKit are the only services that publish ports. The edge publishe `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. -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. `FLUXER_SVC_SHARD_COUNT` is fixed at `1` in the shipped stack. +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`. ## Resources @@ -1767,7 +1767,7 @@ Every service has a memory limit and four also have a memory reservation, all un A limit is a ceiling. The limits below sum to 16.75 GiB and the stack does not need a host that large, because a container costs what it touches. -`deploy.resources.reservations.memory` becomes the container's cgroup v2 `memory.low`, which biases kernel reclaim toward other containers under host pressure. It reserves nothing on its own. +`deploy.resources.reservations.memory` becomes the container's cgroup v2 `memory.low`, which biases kernel reclaim towards other containers under host pressure. It reserves nothing on its own. All are optional. @@ -1943,7 +1943,7 @@ 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. The worker rebuilds both queues from the users table within a day, and every lock has a TTL. +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. 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). @@ -1999,7 +1999,7 @@ At 4 GB the api heap ceiling is 396 MB and the worker heap ceiling is 332 MB. Th ## Routing -The edge is the only HTTP entry point, and every route is served from the one public hostname. It rewrites each path before handing it to an upstream. [What the single port routes](/operator/reverse-proxy/#what-the-single-port-routes) has that path table, and [What every proxy must do](/operator/reverse-proxy/#what-every-proxy-must-do) has the requirements for anything in front of the edge. +The edge is the only HTTP entry point, and every route is served from the one public hostname. It rewrites each path before handing it to an upstream. [What the single port routes](/operator/reverse-proxy/#what-the-single-port-routes) has the path table, and [What every proxy must do](/operator/reverse-proxy/#what-every-proxy-must-do) has the requirements for anything in front of the edge. None of the variables on this page change that routing. The `Caddyfile` is a bind mount, so an edit to it takes `docker compose restart edge`. diff --git a/fluxer_docs/src/content/docs/operator/get-started.mdx b/fluxer_docs/src/content/docs/operator/get-started.mdx index 64aad8f61..2a88ddca7 100644 --- a/fluxer_docs/src/content/docs/operator/get-started.mdx +++ b/fluxer_docs/src/content/docs/operator/get-started.mdx @@ -77,9 +77,9 @@ Open these ports on the host: | 7881 | tcp | LiveKit media, published by the LiveKit container | | 7882 | udp | LiveKit media, published by the LiveKit container | -A machine behind a home router needs 80, 443 and both LiveKit ports forwarded to it. The certificate is issued from the public internet, and voice media arrives on the LiveKit ports directly. Running your own reverse proxy changes only the first two, which the proxy holds while the stack binds a plain HTTP port on the loopback. 7881 and 7882 have to reach the host either way, because voice media never goes through a proxy. +A machine behind a home router needs 80, 443 and both LiveKit ports forwarded to it. The certificate is issued from the public internet, and voice media arrives on the LiveKit ports directly. Running your own reverse proxy changes only the first two. The proxy holds them, and the stack binds a plain HTTP port on the loopback. 7881 and 7882 have to reach the host either way, because voice media never goes through a proxy. -On another port, open that one in place of 443 and read [Serving on another port](#serving-on-another-port). +For another port, open that one instead of 443 and read [Serving on another port](#serving-on-another-port). On a cloud VM, open them in the provider's firewall or security group. On a Linux host running firewalld: @@ -151,7 +151,7 @@ FLUXER_HTTPS_PORT=8443 docker compose up -d ``` -`FLUXER_PUBLIC_PORT` is where the port of the public address goes, and every endpoint the services advertise follows it. It does not move what the host publishes, so `FLUXER_HTTPS_PORT` has to name the same port. Move `FLUXER_PUBLIC_SCHEME` with them when the new port serves plain HTTP, and set `FLUXER_HTTP_PORT` in place of `FLUXER_HTTPS_PORT`. `.env.example` ships beside `.env` and writes out both complete recipes. +`FLUXER_PUBLIC_PORT` is the port of the public address, and every endpoint the services advertise follows it. It does not move what the host publishes, so `FLUXER_HTTPS_PORT` has to name the same port. Move `FLUXER_PUBLIC_SCHEME` with them when the new port serves plain HTTP, and set `FLUXER_HTTP_PORT` instead of `FLUXER_HTTPS_PORT`. `.env.example` ships beside `.env` and writes out both complete recipes. Host 80 stays published on the `https` recipe and still answers the ACME challenge. Let's Encrypt only ever connects to the public 80 or 443, so the certificate is issued if a router in front forwards public 80 to this host. Serve your own certificate from the `Caddyfile` when it cannot. @@ -180,7 +180,7 @@ Run `--dry-run` first to see what a set of flags does. The PowerShell script tak ### The script is the reference -Every step in the script has a comment beside it holding the command that does that step alone and the reason it exists: where each stack file comes from, how `.env` is built from `.env.example`, what each `CHANGE_ME` takes, and how the VAPID pair is derived. Read it at [https://fluxer.dev/install.sh](https://fluxer.dev/install.sh), or the Windows script at [https://fluxer.dev/install.ps1](https://fluxer.dev/install.ps1). +Every step in the script has a comment beside it with the command that does that step alone and why it exists: where each stack file comes from, how `.env` is built from `.env.example`, what each `CHANGE_ME` takes, and how the VAPID pair is derived. Read it at [https://fluxer.dev/install.sh](https://fluxer.dev/install.sh), or the Windows script at [https://fluxer.dev/install.ps1](https://fluxer.dev/install.ps1). ## Step 5: Check that it works @@ -224,13 +224,13 @@ The wizard runs in two halves. The first is a welcome, a theme choice, an admin Create the owner account with an email address at a domain you control. The first registration that supplies one receives the wildcard admin ACL, which is full access to the admin dashboard, unless registration is set to hold new accounts for approval. Finishing the wizard grants that same ACL to whichever account completes it, when that account holds none. -Once email is on, the address goes through a DNS check before the account exists. Email counts as on when the switch is set and the provider is `smtp` with a complete SMTP configuration, so setting `FLUXER_EMAIL_ENABLED=true` on its own does not turn the check on. Its domain has to publish an `MX` record, or an `A` or `AAAA` record as a fallback. A name that resolves on your own network alone, such as a `.lan`, `.internal` or `home.arpa` name, publishes neither, and the account form answers `That email domain cannot receive mail.` whatever the address looks like. Use a domain with public records, or leave email off. +Once email is on, the address goes through a DNS check before the account exists. Email counts as on when the switch is set and the provider is `smtp` with a complete SMTP configuration, so `FLUXER_EMAIL_ENABLED=true` on its own does not turn the check on. The address domain has to publish an `MX` record, or an `A` or `AAAA` record as a fallback. A name that resolves on your own network alone, such as a `.lan`, `.internal` or `home.arpa` name, publishes neither, and the account form answers `That email domain cannot receive mail.` whatever the address looks like. Use a domain with public records, or leave email off. `.env.example` ships `FLUXER_EMAIL_ENABLED=false`, which marks every address verified at creation and sends no mail at all. A forgotten owner password therefore has no email reset. Record it, and register a passkey or a second admin account before you open registration. Then sign in to the admin dashboard at `https://chat.example.com/admin` with the account holding the wildcard ACL. The **Instance Config** page has everything the wizard asked, plus registration mode, approvals and integration keys. **Limit Config** holds the instance limits published to clients. **Voice Regions** and **Voice Servers** come seeded, so voice needs no setup there. -The desktop client opens the hosted web app for its release channel, so reach your own instance in a browser. See [Issue #1088](https://github.com/fluxerapp/fluxer/issues/1088) before you assume we don't care. It's being actively worked on. +The desktop client opens the hosted web app for its release channel, so reach your own instance in a browser. [Issue #1088](https://github.com/fluxerapp/fluxer/issues/1088) tracks that, and is being worked on. [Configuration](/operator/configuration/) lists every runtime setting and the environment variable each one overrides. [Deployment availability](/http-api/deployment-availability/) lists the routes that exist only on the hosted deployment. @@ -269,12 +269,12 @@ In PowerShell write `${PWD}` in place of `$PWD`. You are done when `backups` holds a dump and a tarball and every service reads `running` again. -`sh install.sh --update` takes both before every upgrade. [What the backup covers](/operator/upgrading/#what-the-backup-covers) puts the dump on a nightly timer, and [Restore a backup](/operator/upgrading/#restore-a-backup) puts either artifact back. +`sh install.sh --update` takes both artifacts before every upgrade. [What the backup covers](/operator/upgrading/#what-the-backup-covers) puts the dump on a nightly timer, and [Restore a backup](/operator/upgrading/#restore-a-backup) puts either artifact back. ### Remove the instance :::danger[This deletes every account, message and upload] -`-v` deletes every named volume. Every account, message, upload, search index and certificate the instance holds is gone, and no `docker compose up -d` brings any of it back. Take the copies above first. To upgrade, the command is `sh install.sh --update`, below. +`-v` deletes every named volume. Every account, message, upload, search index and certificate the instance holds is gone, and no `docker compose up -d` brings any of it back. Take the copies above first. To upgrade instead, run `sh install.sh --update`, below. ::: Take the instance down and delete its data: diff --git a/fluxer_docs/src/content/docs/operator/reverse-proxy.mdx b/fluxer_docs/src/content/docs/operator/reverse-proxy.mdx index beb989405..7a2c6ad9b 100644 --- a/fluxer_docs/src/content/docs/operator/reverse-proxy.mdx +++ b/fluxer_docs/src/content/docs/operator/reverse-proxy.mdx @@ -8,7 +8,7 @@ description: Serving a Fluxer instance from nginx, Caddy, Traefik, HAProxy, Apac 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. ::: -A reverse proxy takes the HTTPS connection from a browser and passes each request on to Fluxer. Fluxer runs behind any proxy that terminates TLS and forwards WebSocket upgrades. +Fluxer runs behind any proxy that terminates TLS and forwards WebSocket upgrades. The stack ships Caddy and runs it by default on `80` and `443`. One extra Compose file stops that and publishes a single plain HTTP port instead. That port already routes every path to the right internal service, so the proxy in front needs one routing rule: send everything to it. @@ -60,7 +60,7 @@ With `FLUXER_PUBLIC_SCHEME=https` the admin CSRF cookie is `__Host-csrf_token`, #### Forward WebSocket upgrades -The client connection is on `/gateway` and voice signalling is on `/livekit/*`. Both open as WebSocket upgrades. Check that the handshake completes through whatever is in front of the instance, with your own hostname in place of the one below: +The client connection is on `/gateway` and voice signalling is on `/livekit/*`. Both open as WebSocket upgrades. Check that the handshake completes through whatever is in front of the instance, with your own hostname in place of `chat.example.com`: ```bash curl -sS -i --http1.1 --max-time 5 \ @@ -87,7 +87,7 @@ The Gateway socket stays open for the life of a client session, so give idle and #### Pass `Sec-Fetch-Site` through untouched -The browser sends it, and the admin dashboard refuses a mutating request with a cross-site value. +Browsers send it, and the admin dashboard refuses a mutating request with a cross-site value. #### Send no `Content-Security-Policy` of its own @@ -115,7 +115,7 @@ FLUXER_PUBLIC_PORT=8443 Your proxy holds the public port, and the overlay pins the edge to plain HTTP on `8080` whatever that port is, so nothing else in `.env` moves with it. -Leave `FLUXER_PUBLIC_ORIGIN` commented out. It states the same address as one string, and an `.env` that sets it without the port `FLUXER_PUBLIC_PORT` names puts part of the instance on `chat.example.com:8443` and part on `chat.example.com`, where the web app sends no `Authorization` header across the gap. +Leave `FLUXER_PUBLIC_ORIGIN` commented out. It states the same address as one string, and an `.env` that sets it without the port `FLUXER_PUBLIC_PORT` names puts part of the instance on `chat.example.com:8443` and part on `chat.example.com`, where the web app sends no `Authorization` header across the gap. [The public origin](/operator/configuration/#the-public-origin) has the rest. ## nginx @@ -348,11 +348,11 @@ networks: name: fluxer_fluxer ``` -Cloudflare appends the visitor address to any `X-Forwarded-For` the visitor sent, so the header can arrive with a forged value in front of the real one. The edge reads the rightmost address that is not in `FLUXER_EDGE_TRUSTED_PROXIES`, which is the one Cloudflare appended. A Transform Rule that sets `X-Forwarded-For` to `cf.connecting_ip` removes the ambiguity outright. +Cloudflare appends the visitor address to any `X-Forwarded-For` the visitor sent, so the header can arrive with a forged value in front of the real one. The edge reads the rightmost address that is not in `FLUXER_EDGE_TRUSTED_PROXIES`, which is the one Cloudflare appended. A Transform Rule that sets `X-Forwarded-For` to `cf.connecting_ip` removes the ambiguity. Cloudflare caps a request body at 100 MB on Free and Pro, 200 MB on Business, and 500 MB on Enterprise, and answers 413 above the cap. The cap applies to tunnel traffic, and no `cloudflared` setting raises it. Keep the [`max_attachment_file_size`](/http-api/instance/#limit-keys) limit below the cap for your plan, in the admin dashboard under limit configuration. It defaults to 26214400 bytes for a non-premium account and 524288000 bytes for a premium one, which is above every cap below Enterprise. -A tunnel has the web app, API, Gateway, admin dashboard, media routes, and LiveKit signalling. It has no LiveKit media. +A tunnel has the web app, API, Gateway, admin dashboard, media routes, and LiveKit signalling, but no LiveKit media. ## Nginx Proxy Manager @@ -376,7 +376,7 @@ proxy_read_timeout 3600s; proxy_send_timeout 3600s; ``` -Nginx Proxy Manager appends to `X-Forwarded-For`, and the visitor's own value arrives in front of the real address. The edge takes the rightmost entry that is not in `FLUXER_EDGE_TRUSTED_PROXIES`, which is the one Nginx Proxy Manager appended. Users on a LAN or a VPN inside the default `private_ranges` are the exception and resolve to the forged entry, so narrow `FLUXER_EDGE_TRUSTED_PROXIES` to the proxy's address in that layout. +Nginx Proxy Manager appends to `X-Forwarded-For`, so the visitor's own value arrives in front of the real address. The edge takes the rightmost entry that is not in `FLUXER_EDGE_TRUSTED_PROXIES`, which is the one Nginx Proxy Manager appended. Users on a LAN or a VPN inside the default `private_ranges` are the exception and resolve to the forged entry, so narrow `FLUXER_EDGE_TRUSTED_PROXIES` to the proxy's address in that layout. ## What the single port routes @@ -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 themselves 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 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. `/_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`. @@ -462,7 +462,7 @@ FLUXER_EDGE_TRUSTED_PROXIES=private_ranges 203.0.113.10/32 FLUXER_EDGE_TRUSTED_PROXIES=private_ranges 100.101.102.103/32 ``` -The edge believes a trusted peer without question. Anyone who can open a TCP connection from a trusted address can set `X-Forwarded-For` to any value and be recorded as that address, which defeats bans and rate limits and pollutes abuse detection. Keep the list down to the one address your proxy arrives from. That is also the only correct list on a LAN or VPN deployment, where a visitor whose own address falls inside a wider list is recorded under the proxy's address. +The edge accepts whatever a trusted peer sends. Anyone who can open a TCP connection from a trusted address can set `X-Forwarded-For` to any value and be recorded as that address, which defeats bans and rate limits and pollutes abuse detection. Keep the list down to the one address your proxy arrives from. That is also the only correct list on a LAN or VPN deployment, where a visitor whose own address falls inside a wider list is recorded under the proxy's address. Find the address the edge sees a host-side proxy arrive from: diff --git a/fluxer_docs/src/content/docs/operator/upgrading.mdx b/fluxer_docs/src/content/docs/operator/upgrading.mdx index ff62e2b04..d93c19974 100644 --- a/fluxer_docs/src/content/docs/operator/upgrading.mdx +++ b/fluxer_docs/src/content/docs/operator/upgrading.mdx @@ -21,7 +21,7 @@ Read the release notes between the version you run and the one you are moving to ## Match the images to the stack files -The images come from `FLUXER_IMAGE_TAG` in `.env`. The stack files come from a git ref, which is a branch or a tag in the Fluxer repository. The installer derives that ref from the image tag: `main` when the tag is `v1` or `latest`, and the tag string itself for anything else. `--ref` overrides the derivation. `docker compose pull` never updates a stack file, and refreshing a stack file never moves an image. +The images come from `FLUXER_IMAGE_TAG` in `.env`. The stack files come from a git ref, which is a branch or a tag in the Fluxer repository. The installer derives that ref from the image tag: `main` for `v1` or `latest`, and the tag string itself for anything else. `--ref` overrides the derivation. `docker compose pull` never updates a stack file, and refreshing a stack file never moves an image. The repository holds no ref by the name of a pinned image tag. A release tags each image on its own, as `fluxer-api@2026.813.205040`, so pass `--ref` with that tag or with the commit it points at. Without that override the download fails with exit 4. @@ -63,9 +63,9 @@ To write it by hand, run the generator in a shell: printf '\nFLUXER_ERLANG_COOKIE=%s\n' "$(openssl rand -hex 32)" >> .env ``` -Run that as a command. Pasting `FLUXER_ERLANG_COOKIE=$(openssl rand -hex 32)` into `.env` as text stores those characters as the value, because Compose reads a line literally and runs nothing in it. The leading newline keeps the key on its own line. An editor can leave the last line of `.env` without a newline, and the two would otherwise join. +Pasting `FLUXER_ERLANG_COOKIE=$(openssl rand -hex 32)` into `.env` as text stores those characters as the value, because Compose reads a line literally and runs nothing in it. The leading newline keeps the key on its own line. An editor can leave the last line of `.env` without a newline, and the two would otherwise join. -`api` and `media-proxy` now refuse to start unless `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64` decodes to at least 32 bytes. The line has been in `.env.example` for a while as `CHANGE_ME`, which decodes to 6 bytes, and nothing read it before, so an instance can be running today with the placeholder. `--update` replaces an absent or `CHANGE_ME` value with a generated one before it reads anything. Both services read the same value from one `.env` line. A `CHANGE_ME` value stops `api` at boot with `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 must decode to at least 32 bytes`, and an empty one with `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 is required for the API`. +`api` and `media-proxy` now refuse to start unless `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64` decodes to at least 32 bytes. `.env.example` has shipped it as `CHANGE_ME`, which decodes to 6 bytes, and nothing read it before, so an instance can be running today with the placeholder. `--update` replaces an absent or `CHANGE_ME` value with a generated one before it reads anything. Both services read the same value from one `.env` line. A `CHANGE_ME` value stops `api` at boot with `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 must decode to at least 32 bytes`, and an empty one with `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 is required for the API`. To write it by hand, run the generator in a shell the same way: @@ -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, and it belongs 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 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. `.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. @@ -200,8 +200,6 @@ Each upgrade writes one record directory, named `record-` and a UTC stamp, under 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 dump costs no downtime. The uploads copy stops the stack, and the script starts the stack again on the old images before it goes any further. - 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. 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: @@ -210,7 +208,7 @@ Put a dump on a timer as well. On Linux and macOS, this crontab line writes one 15 3 * * * cd /srv/fluxer && docker compose exec -T postgres pg_dump -U fluxer -d fluxer --format=custom > "backups/fluxer-$(date -u +\%Y\%m\%dT\%H\%M\%SZ).dump" && find backups -name 'fluxer-*.dump' -mtime +14 -delete ``` -`/srv/fluxer` stands for the directory holding `.env`. Write it as an absolute path, because cron does not expand `~`. An unescaped `%` in a crontab is a newline, which is why every one of them has a backslash. +`/srv/fluxer` stands for the directory holding `.env`. Write it as an absolute path, because cron does not expand `~`. An unescaped `%` in a crontab is a newline, which is why every one above has a backslash. ## Run a data store outside the stack @@ -244,13 +242,13 @@ sh install.sh --rollback On Windows it is `.\install.ps1 -Rollback`. It takes the newest record, puts back the images and the stack files it holds, recreates, restarts `edge`, and verifies. `--dry-run` prints that plan too. -A rollback never pulls, so it needs the old images still on the host. Run [Reclaim disk](#reclaim-disk) only after an upgrade satisfies you. +A rollback never pulls, so it needs the old images still on the host. Run [Reclaim disk](#reclaim-disk) only once the upgrade is known good. -The database does not move. `api`, `worker`, `users-shard` and `messages-shard` apply schema work in place while they start, and an older image does not undo it. Across a release that changed the schema, the dump in the record is the only way back, and putting it back is a decision you make. +The database does not move. `api`, `worker`, `users-shard` and `messages-shard` apply schema work in place while they start, and an older image does not undo it. Across a release that changed the schema, the dump in the record is the only way back, and a rollback never puts it back for you. ## Restore a backup -Both artifacts go back with the stack stopped, so nothing writes while they are replaced. Name your own record directory in place of the one below. +Both artifacts go back with the stack stopped, so nothing writes while they are replaced. Name your own record directory instead of the one below. The database restores from the custom-format dump: @@ -285,7 +283,7 @@ docker run --rm -v fluxer_seaweedfs-data:/data \ docker compose up -d ``` -`tar` extracts over whatever is already on the volume, so the `find` empties it first. In PowerShell write `${PWD}` in place of `$PWD`. Success is an existing attachment URL answering 200 again. The `fluxer_` prefix is the Compose project name, which `docker-compose.yml` sets to `fluxer`. +`tar` extracts over whatever is already on the volume, so the `find` empties it first. In PowerShell write `${PWD}` instead of `$PWD`. Success is an existing attachment URL answering 200 again. The `fluxer_` prefix is the Compose project name, which `docker-compose.yml` sets to `fluxer`. ## Move to a new Postgres major version @@ -321,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 inside `docker-compose.yml` and move only when an upgrade refreshes that file. +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. ## Change the domain or the passkey relying party @@ -341,7 +339,7 @@ Each upgrade leaves the previous images behind, which adds up to a few gigabytes docker image prune -f ``` -On the default moving tag, the pull moves `v1` onto the new images and leaves the ones it replaced untagged, so this removes them. A rollback on a moving tag needs those images. Prune only once the upgrade satisfies you. A pinned release keeps its own tag and survives the prune. Remove one of those by naming its tag in `docker image rm`. +On the default moving tag, the pull moves `v1` onto the new images and leaves the ones it replaced untagged, so this removes them. A rollback on a moving tag needs those images. Prune only once the upgrade is known good. A pinned release keeps its own tag and survives the prune. Remove one of those by naming its tag in `docker image rm`. `docker system prune -a` reclaims more. It also deletes every image that no container references, including ones unrelated to Fluxer. diff --git a/fluxer_docs/src/content/docs/snowflakes.md b/fluxer_docs/src/content/docs/snowflakes.md index 7c06a51e4..020e756fd 100644 --- a/fluxer_docs/src/content/docs/snowflakes.md +++ b/fluxer_docs/src/content/docs/snowflakes.md @@ -9,12 +9,12 @@ A snowflake is a 64-bit identifier for a Fluxer resource such as a user, guild, A snowflake is unique within the deployment that issued it. The value stays stable, and Fluxer does not reuse it after the resource is deleted. Separate deployments can issue the same numeric value, so a client MUST scope every snowflake to its deployment. :::caution[Never parse a snowflake as a JSON number] -Snowflake values exceed the exact integer range of a JSON double, so parsing one as a number can silently change it. A client MUST parse the string into a 64-bit integer and MUST compare snowflakes as integers, because unequal-length decimal strings do not sort as text. +Snowflake values exceed a JSON double's exact integer range, so parsing one as a number can silently change it. A client MUST parse the string into a 64-bit integer and MUST compare snowflakes as integers, because unequal-length decimal strings do not sort as text. ::: ## Format -A snowflake packs a timestamp, a worker ID, and a sequence into 64 bits. The worker and sequence fields distinguish identifiers minted during the same millisecond. Bit 63 is always zero, so an issued snowflake fits a signed 64-bit integer. +A snowflake packs a timestamp, a worker ID, and a sequence into 64 bits. The worker and sequence fields distinguish identifiers issued during the same millisecond. Bit 63 is always zero, so an issued snowflake fits a signed 64-bit integer. | Field | Bits | Description | | --- | --- | --- | @@ -22,15 +22,15 @@ A snowflake packs a timestamp, a worker ID, and a sequence into 64 bits. The wor | Worker ID | 21 to 12 | The unsigned allocator worker identifier, from `0` through `1023` | | Sequence2 | 11 to 0 | The unsigned per-worker sequence, from `0` through `4095` | -1 The timestamp records the instant the identifier was minted, which can fall shortly before the resource exists +1 The timestamp records the instant the identifier was issued, which can fall shortly before the resource exists 2 Sequence order applies only within one worker and one millisecond The epoch is 1420070400000 milliseconds after the Unix epoch. Every Fluxer instance uses the same value. The lower 22 bits expose allocator details, so a client MUST NOT use them for routing or resource semantics. -One worker issues strictly increasing values. It advances the sequence for each identifier minted during the same millisecond, and it waits for the next millisecond once the sequence passes 4095. +One worker issues strictly increasing values. It advances the sequence for each identifier issued during the same millisecond, and it waits for the next millisecond once the sequence passes 4095. -A worker whose clock moves backwards keeps minting against the highest millisecond it has already used. An extracted timestamp can therefore fall later than the wall clock at the moment of minting. +A worker whose clock moves backwards keeps issuing against the highest millisecond it has already used. An extracted timestamp can therefore fall later than the wall clock at the moment of issuing. ## Extracting a timestamp @@ -65,7 +65,7 @@ Fluxer emits a snowflake as an unsigned decimal string in every JSON body, path Both HTTP validation codes are element codes inside a 400 [`INVALID_FORM_BODY`](/http-api/errors/#api-error-code-registry) response. Each Gateway [command](/gateway/commands/) states what its own rejection does, from discarding the field to abandoning the whole command. -A snowflake sent as a JSON number keeps its exact value at any size. A fractional JSON number is an ordinary type failure and reports [`INVALID_FORMAT`](/http-api/errors/#validation-error-code-registry). Fluxer reads an exponent form as the number it denotes, and one above 9007199254740991 reports [`INVALID_SNOWFLAKE_FORMAT`](/http-api/errors/#validation-error-code-registry). Path parameters and query string parameters always arrive as text. +A snowflake sent as a JSON number keeps its exact value at any size. A fractional JSON number is an ordinary type failure and reports [`INVALID_FORMAT`](/http-api/errors/#validation-error-code-registry). Fluxer reads an exponent form as the number it means, and one above 9007199254740991 reports [`INVALID_SNOWFLAKE_FORMAT`](/http-api/errors/#validation-error-code-registry). Path parameters and query string parameters always arrive as text. :::note[`0` is the beginning of snowflake time] An HTTP pagination cursor accepts `0`. An HTTP path parameter or field that identifies a real resource also accepts `0`, and the request then receives the ordinary not-found result for that resource. @@ -85,7 +85,7 @@ A collection endpoint that pages over a snowflake-ordered resource accepts a cur The type of a `before` or `after` cursor follows the operation. [List pinned messages](/http-api/messages/#list-pinned-messages) pages on the pin time, so its `before` is an ISO 8601 timestamp. [List blocklist entries](/admin-api/blocklists/#list-blocklist-entries) pages on the entry value. Its `after` is the stored value of the last entry on the previous page. Each operation states the type of its own cursors. -Because the timestamp occupies the high bits, numeric snowflake order is creation time order at millisecond resolution. +Because the timestamp uses the high bits, numeric snowflake order is creation time order at millisecond resolution. :::note[A cursor is a numeric boundary] A cursor does not need to identify a resource, so a snowflake derived from a timestamp can delimit a time range without a dedicated timestamp parameter. diff --git a/fluxer_docs/src/content/docs/topics/captcha.md b/fluxer_docs/src/content/docs/topics/captcha.md index 62161e50f..08547fed6 100644 --- a/fluxer_docs/src/content/docs/topics/captcha.md +++ b/fluxer_docs/src/content/docs/topics/captcha.md @@ -40,7 +40,7 @@ The instance account policy grants the `captcha_exempt` capability to a contact The other exemption is the `APP_STORE_REVIEWER` user flag. Fluxer tests it against the resolved account, then against the account an `email` member of the request body resolves to. The body check parses the request body as JSON and reads a string `email` member, and a body that is absent, is not JSON, or is not a JSON object yields no address. That check exempts a login or a registration attempt before any account is resolved. -The exemptions run before request validation on the authentication operations, on create application, and on redeem gift. Create private channel and add group direct message recipient validate the request first, so an invalid request is rejected before any exemption is tested. +Both exemptions run before request validation on the authentication operations, on create application, and on redeem gift. Create private channel and add group direct message recipient validate the request first, so an invalid request is rejected before any exemption is tested. No exemption is visible in an API response. A client cannot predict one and handles a challenge on every gated operation. diff --git a/fluxer_docs/src/content/docs/topics/locales.md b/fluxer_docs/src/content/docs/topics/locales.md index 8bc9aacbf..fb314f627 100644 --- a/fluxer_docs/src/content/docs/topics/locales.md +++ b/fluxer_docs/src/content/docs/topics/locales.md @@ -55,7 +55,7 @@ The registry below is the complete set for every Fluxer surface. Wherever Fluxer Fluxer resolves the response locale once for each request. When a request resolves an authenticated user with a stored locale, Fluxer takes that locale. Every other request negotiates the `Accept-Language` header against the [supported locale registry](#supported-locales), and `en-US` is the result whenever negotiation selects no registry value. -An account created by password registration stores the locale negotiated from its own registration request, so the `Accept-Language` header on that request sets the stored value. An account provisioned through single sign-on stores no account locale. Its [user settings](/http-api/users/settings/) locale reads `en-US`, and its requests negotiate `Accept-Language` until the locale setting is changed. +An account created by password registration stores the locale negotiated from its own registration request, so the `Accept-Language` header on that request sets the stored value. An account created through single sign-on stores no account locale. Its [user settings](/http-api/users/settings/) locale reads `en-US`, and its requests negotiate `Accept-Language` until the locale setting is changed. 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. diff --git a/fluxer_docs/src/content/docs/topics/rate-limits.md b/fluxer_docs/src/content/docs/topics/rate-limits.md index dcd1a14df..3ba35f99a 100644 --- a/fluxer_docs/src/content/docs/topics/rate-limits.md +++ b/fluxer_docs/src/content/docs/topics/rate-limits.md @@ -12,7 +12,7 @@ Nearly every route declares its own bucket. A bucket name can have a path parame Every bucket is also keyed by the caller's identity. An authenticated request is keyed by the account and its credential kind, and an OAuth2 bearer credential is keyed by the owning application as well. A session, a bot token, an Admin API key, and each bearer application therefore draw on separate allowances for the same account. -A request that resolves no account is keyed by the client IP address, exactly for IPv4 and by the `/64` for IPv6, so clients in the same `/64` share an allowance. Where the deployment is configured to read the address from a header the request does not have, it is refused with 403 `FORBIDDEN` before any bucket is evaluated. +A request that resolves no account is keyed by the client IP address, exactly for IPv4 and by the `/64` for IPv6, so clients in the same `/64` share an allowance. Where the deployment is configured to read the address from a header the request does not have, Fluxer refuses the request with 403 `FORBIDDEN` before evaluating any bucket. 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. @@ -24,7 +24,7 @@ The global window is one second. The default allowance is 50 requests per second Some operations enforce a further limit inside the handler. `RATE_LIMIT_BYPASS` exempts an account from none of them. [Limits enforced inside a handler](#limits-enforced-inside-a-handler) has the complete set. -The global bucket is evaluated first. A route bucket is consumed only after the global check admits the request. +A route bucket is consumed only after the global bucket admits the request. :::note[An allowance drains continuously] Every bucket is a leaky bucket. It admits at most the declared limit at once and refills continuously at that limit for each declared window, so a client that exhausts an allowance can send again as soon as enough of it has drained. @@ -43,7 +43,7 @@ Four routes charge a second bucket. [Create private channel](/http-api/users/pri [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. :::caution[A global denial revokes a user session] -When the global bucket denies a request authenticated by a user session token belonging to a non-bot account, Fluxer revokes that session token before writing the 429 and 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. +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. @@ -63,7 +63,7 @@ The denial body has the two members of the ordinary [error response](/http-api/# 1 A limit enforced outside the route bucket middleware can reuse this body with its own code. [Send phone verification](/http-api/users/phone-verification/#send-phone-verification) is the only live one, reporting `PHONE_RATE_LIMIT_EXCEEDED` -2 The locale is the one resolved for the request, which the account setting selects ahead of [Accept-Language](/http-api/#standard-request-headers) +2 The locale [resolved](/topics/locales/#negotiation) for the request, which the account setting selects ahead of [Accept-Language](/http-api/#standard-request-headers) 3 Never below 0.001, falling back to the whole-second `Retry-After` value when no fractional delay was computed diff --git a/fluxer_docs/src/content/docs/topics/uploads.md b/fluxer_docs/src/content/docs/topics/uploads.md index df14ea6e5..2a63bfa58 100644 --- a/fluxer_docs/src/content/docs/topics/uploads.md +++ b/fluxer_docs/src/content/docs/topics/uploads.md @@ -16,7 +16,7 @@ When a deployment disables them, the plan request and the completion request bot [Request attachment upload URLs](/http-api/messages/#request-attachment-upload-urls) takes from 1 through 10 attachment declarations. Each is a client-chosen `id`, a `filename`, an exact `file_size` in bytes, and a `content_type`. A user session credential and a bot token are accepted, and an OAuth2 bearer credential is rejected with 403 `ACCESS_DENIED`. -The channel must support messages, and any other channel type returns 400 `CANNOT_SEND_MESSAGES_IN_NON_TEXT_CHANNEL`. A guild channel additionally requires [SEND_MESSAGES](/http-api/permissions/) and [ATTACH_FILES](/http-api/permissions/), returning 403 `MISSING_PERMISSIONS` otherwise, and a caller under a communication timeout receives 403 `COMMUNICATION_DISABLED`. +The channel must support messages, and any other channel type returns 400 `CANNOT_SEND_MESSAGES_IN_NON_TEXT_CHANNEL`. A guild channel also requires [SEND_MESSAGES](/http-api/permissions/) and [ATTACH_FILES](/http-api/permissions/), returning 403 `MISSING_PERMISSIONS` otherwise, and a caller under a communication timeout receives 403 `COMMUNICATION_DISABLED`. Fluxer then checks every declared size on its own against the `max_attachment_file_size` limit resolved for the caller and the guild. A size above it returns 400 `FILE_SIZE_TOO_LARGE` with the resolved ceiling before anything is planned. That limit defaults to 26214400 bytes, the 25 MiB non-premium allowance, and to 524288000 bytes, the 500 MiB premium allowance. A bot credential is clamped to 52428800 bytes, the 50 MiB bot ceiling, even when the resolved limit is higher. @@ -47,7 +47,7 @@ The direct storage capability signs the exact byte count, so a `PUT` of any othe 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. -The instance decides per request whether to relay. It resolves the caller's country from the client IP address and issues a direct storage URL only when that country is on the deployment's direct-upload list. Every other caller, including one whose geolocation lookup fails, receives a URL on the [upload relay](/media-proxy/upload-relay/). A client treats both shapes the same way and MUST NOT parse, rewrite, or reorder the query string of either. +The instance decides per request whether to relay. It resolves the caller's country from the client IP address and issues a direct storage URL only when that country is on the deployment's direct-upload list. Every other caller receives a URL on the [upload relay](/media-proxy/upload-relay/), including one whose country cannot be resolved. A client treats both shapes the same way and MUST NOT parse, rewrite, or reorder the query string of either. ### Capability lifetimes @@ -65,7 +65,7 @@ An issued upload URL authorises writing one object or one part. A client treats ## Completing a multipart upload -[Complete attachment upload](/http-api/messages/#complete-attachment-upload) finalises from 1 through 10 multipart uploads. Each entry names the `upload_filename` and the `upload_id` from the plan. The caller sends no part list and no entity tags. The server lists the parts the storage backend has already accepted, sorts them by part number, and assembles them in that order. +[Complete attachment upload](/http-api/messages/#complete-attachment-upload) finishes from 1 through 10 multipart uploads. Each entry names the `upload_filename` and the `upload_id` from the plan. The caller sends no part list and no entity tags. The server lists the parts the storage backend has already accepted, sorts them by part number, and assembles them in that order. 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. @@ -78,7 +78,7 @@ Two failures are reported as [validation error object](/http-api/#validation-err 1 An upload another identity planned, one planned for another channel, one planned as singlepart, and one a message has already consumed are all reported this way, so a caller cannot probe another identity's upload state -2 The multipart upload is aborted before the error is returned +2 Fluxer aborts the multipart upload before it returns the error Fluxer then sums the listed part sizes. A total above the resolved file size limit aborts the upload and returns 400 `FILE_SIZE_TOO_LARGE`. A storage failure during assembly aborts it as well. An aborted upload discards its parts, and its `upload_filename` can never be claimed, so a client requests a new plan for the file. @@ -112,7 +112,7 @@ The operation answers 204 once the image is accepted. Fluxer absorbs a transient 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. -[Get stream preview](/http-api/streams/#get-stream-preview) returns the current image bytes with `Cache-Control: no-store, private`, and [Delete stream preview](/http-api/streams/#delete-stream-preview) removes it. The stored preview record expires one day after the inline upload that wrote it, or one day after the call that issued the capability, and using a capability again does not extend it. A read past that point answers an empty 404 even when the object is still in storage. +[Get stream preview](/http-api/streams/#get-stream-preview) returns the current image bytes with `Cache-Control: no-store, private`, and [Delete stream preview](/http-api/streams/#delete-stream-preview) removes it. The stored preview record expires one day after the inline upload that wrote it, or one day after the call that issued the capability. Using a capability again does not extend it. A read past that point answers an empty 404 even when the object is still in storage. ## Failures diff --git a/fluxer_docs/src/content/docs/voice/index.md b/fluxer_docs/src/content/docs/voice/index.md index 41923cba3..ed9f1e78b 100644 --- a/fluxer_docs/src/content/docs/voice/index.md +++ b/fluxer_docs/src/content/docs/voice/index.md @@ -4,7 +4,7 @@ title: Voice description: Voice placement, guild voice channels, calls, Go Live, and entrance sounds. --- -Fluxer runs voice over LiveKit. The [main Gateway](/gateway/overview/) places a session into a voice channel and hands it one credential, the client presents that credential to a media server, and every track goes over the connection it opens there. Microphone, camera, and screen share are track sources on that one connection, so going live opens nothing new. +Fluxer runs voice over LiveKit. The [main Gateway](/gateway/overview/) places a session into a voice channel and hands it one credential. The client presents that credential to a media server, and every track goes over the connection it opens there. Microphone, camera, and screen share are track sources on that one connection, so going live opens nothing new. [Client commands](/gateway/commands/) and [Gateway events](/gateway/events/) define the placement protocol. @@ -22,7 +22,7 @@ Every statement on this page is what an instance serves today, and a later relea | Entrance sound2 | A short clip announced to everyone already connected to a voice channel | [Entrance sounds](/http-api/entrance-sounds/) | | Voice activity sharing | Whether a friend is told which voice channel the account is in | [User settings](/http-api/users/settings/#modify-voice-activity-sharing) | -1 Going live opens no second connection and mints no second credential +1 Going live opens no second connection and issues no second credential 2 The one surface that publishes no media track @@ -42,13 +42,13 @@ 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 admits the microphone source, and STREAM admits 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 two screen share sources. A server-deafened connection may neither publish nor subscribe. ## Deployment feature state A deployment can be configured without voice. The [instance discovery document](/http-api/instance/#instance-features-object) reports that state as `features.voice_enabled`. No other surface warns a client in advance. -Where `features.voice_enabled` is false, Fluxer mints no media credential, so a placement request is refused with `VOICE_TOKEN_FAILED`. [List RTC regions](/http-api/channels/#list-rtc-regions) answers 200 with an empty array before it resolves the channel, and [Modify call region](/http-api/calls/#modify-call-region) accepts any region string. +Where `features.voice_enabled` is false, Fluxer issues no media credential, so a placement request is refused with `VOICE_TOKEN_FAILED`. [List RTC regions](/http-api/channels/#list-rtc-regions) answers 200 with an empty array before it resolves the channel, and [Modify call region](/http-api/calls/#modify-call-region) accepts any region string. ## Placement @@ -69,11 +69,11 @@ The server answers with [Voice State Update](/gateway/events/#voice-state-update A grant is issued when a connection opens, when it moves to another channel, and when the region changes. An update that stays in the same channel reissues nothing, so toggling `self_mute`, `self_video`, or `self_stream` produces one [Voice State Update](/gateway/events/#voice-state-update) and no new grant. -The grant `token` is minted for the media server and consumed by the media connection alone. No route on this API accepts it. +The grant `token` is issued for the media server and consumed by the media connection alone. No route on this API accepts it. -Fluxer reports a guild refusal as [Voice State Ack](/gateway/events/#voice-state-ack) with a `status` of `rejected` and an `error_code` naming the exact reason, and only a command with a `mutation_id` receives one. A refused placement without it produces no Dispatch, so a client that needs to observe a guild failure MUST send `mutation_id`. +Fluxer reports a guild refusal as [Voice State Ack](/gateway/events/#voice-state-ack) with a `status` of `rejected` and an `error_code` naming the exact reason, and only a command with a `mutation_id` receives one. A refused placement without it produces no Dispatch, so a client that needs to observe a guild refusal MUST send `mutation_id`. -A call never acks. A refused placement into a direct message or group direct message call produces no Dispatch whether or not the command had `mutation_id`, so a client observes that refusal only as the absence of a grant. +A call never acks. A refused placement into a direct message or group direct message call produces no Dispatch whether or not the command had `mutation_id`. A client observes that refusal only as the absence of a grant. ## Guild voice channels @@ -99,9 +99,9 @@ A guild voice channel stores its `bitrate`, `user_limit`, `voice_connection_limi 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, which the guild grants 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. 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. -A grant is evaluated when it is minted, 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. +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. The guild applies the new result to a live connection and issues no new [Voice Server Update](/gateway/events/#voice-server-update). The media server mutes a published microphone, camera, or screen share track the member may no longer publish, and drops a connection that fails the VIEW_CHANNEL and CONNECT check. @@ -129,7 +129,7 @@ A member whose `communication_disabled_until` is still in the future is refused ### Regions -[List RTC regions](/http-api/channels/#list-rtc-regions) returns the [RTC region objects](/http-api/channels/#rtc-region-object) the caller MAY select for one guild voice channel. A region is returned only when the caller passes every restriction configured for it and at least one voice server accessible to the caller is active in it, so the array can be empty. +[List RTC regions](/http-api/channels/#list-rtc-regions) returns the [RTC region objects](/http-api/channels/#rtc-region-object) the caller MAY select for one guild voice channel. A region is returned only when the caller passes every restriction configured for it and at least one voice server accessible to the caller is active in it. The array can be empty. `rtc_region` is written by [Modify channel](/http-api/channels/#modify-channel) and requires UPDATE_RTC_REGION. A null value selects automatic routing, and so does a stored value the placing account cannot reach. @@ -143,7 +143,7 @@ An operator manages the regions and the voice servers registered inside them thr ## Private calls -A private call is the direct message and group direct message counterpart of a voice channel. It has no moderator, no permission overwrites, and no moderator mute, deafen, or disconnect. +A private call has no moderator, no permission overwrites, and no moderator mute, deafen, or disconnect. The [Calls resource](/http-api/calls/) owns its HTTP surface, which reads whether the caller may ring, changes the region of an active call, rings recipients, and stops ringing them. [End call session](/http-api/calls/#end-call-session) ends no call. [Call Create](/gateway/events/#call-create), [Call Update](/gateway/events/#call-update), and [Call Delete](/gateway/events/#call-delete) publish the call state, and both joining and leaving are the same [Voice State Update](/gateway/commands/#voice-state-update) a guild voice channel uses. @@ -155,17 +155,17 @@ A recipient's [incoming call flags](/http-api/users/#incoming-call-flags) decide ## Go Live streams -Going live publishes a screen share track, and its screen share audio track, on the LiveKit participant the member already holds in the channel. There is no second connection, no second participant, and no second `connection_id`. A member that held STREAM at placement already has both screen share sources in its grant, so no new credential is minted and no [Voice Server Update](/gateway/events/#voice-server-update) follows. +Going live publishes a screen share track, and its screen share audio track, on the LiveKit participant the member already holds in the channel. There is no second connection, no second participant, and no second `connection_id`. A member that held STREAM at placement already has both screen share sources in its grant, so no new credential is issued and no [Voice Server Update](/gateway/events/#voice-server-update) follows. The publisher advertises the stream by setting `self_stream` on that connection with [Voice State Update](/gateway/commands/#voice-state-update), naming the connection's own `connection_id`. The server increments the voice state `version` and rebroadcasts the state as one [Voice State Update](/gateway/events/#voice-state-update). The voice state of a connection without STREAM in its channel has `self_stream` and `self_video` false, whatever the client sent. The guild clears both when a live connection loses STREAM, and rebroadcasts the state. -A viewer declares which streams it is watching with `viewer_stream_keys` on its own voice state, and a channel move resets that list to empty. +A viewer declares which streams it watches with `viewer_stream_keys` on its own voice state, and a channel move resets that list to empty. That connection's `connection_id` is the last segment of the [stream key](/http-api/streams/#stream-key), which is `{guild_id}:{channel_id}:{connection_id}` for a guild voice channel and `dm:{channel_id}:{connection_id}` for a private call. -The [Streams resource](/http-api/streams/) owns the operations addressed by that key, which record a region preference and read, upload, and delete a JPEG preview image. Reading a preview takes the same access that lets a member join the channel, so any member holding CONNECT there MAY read it without owning the connection. Mutating one additionally requires STREAM on a guild channel and a voice state matching exactly the channel and the connection the key names. +The [Streams resource](/http-api/streams/) owns the operations addressed by that key, which record a region preference and read, upload, and delete a JPEG preview image. Reading a preview takes the same access that lets a member join the channel, so any member holding CONNECT there MAY read it without owning the connection. Mutating one also requires STREAM on a guild channel and a voice state matching exactly the channel and the connection the key names. :::caution[An oversized track loses only its own source] On an instance that is not self-hosted, Fluxer mutes a camera or screen share track above 1280x720 from a member without the higher video quality entitlement, and removes that source from the connection's grant. The voice connection stays up.