feat(web): prepare the fluxer.com domain migration (#2952)

This commit is contained in:
Hampus
2026-09-25 13:43:34 +02:00
committed by GitHub
parent e62ae77643
commit 6730a242db
223 changed files with 13412 additions and 2669 deletions
+2 -2
View File
@@ -271,7 +271,7 @@ const ACCEPTED_TABLE_FINDINGS = new Map<string, Readonly<Partial<Record<TableRul
['admin-api/discovery.mdx', {'table-identifier': 1}],
['admin-api/guilds.mdx', {'table-identifier': 3}],
['admin-api/index.mdx', {'table-fit': 1, 'table-identifier': 3}],
['admin-api/instance.mdx', {'table-fit': 1, 'table-identifier': 5}],
['admin-api/instance.mdx', {'table-fit': 1, 'table-identifier': 6}],
['admin-api/messages.mdx', {'table-identifier': 1}],
['admin-api/reports.mdx', {'table-fit': 1, 'table-identifier': 2}],
['admin-api/users.mdx', {'table-fit': 1, 'table-identifier': 1}],
@@ -298,7 +298,7 @@ const ACCEPTED_TABLE_FINDINGS = new Map<string, Readonly<Partial<Record<TableRul
['http-api/guild-moderation.mdx', {'table-cell': 1}],
['http-api/guild-stickers.mdx', {'table-cell': 3}],
['http-api/guilds.mdx', {'table-fit': 1, 'table-identifier': 4}],
['http-api/instance.mdx', {'table-identifier': 5}],
['http-api/instance.mdx', {'table-identifier': 6}],
['http-api/invites.mdx', {'table-cell': 6}],
['http-api/messages.mdx', {'table-fit': 1, 'table-cell': 20}],
['http-api/permissions.mdx', {'table-cell': 8}],
@@ -36,6 +36,7 @@ Missing settings use the defaults documented below. Invalid stored configuration
| gateway_rollout | [Gateway rollout configuration](#gateway-rollout-configuration-object) object | Gateway admission and dispatch tuning |
| voice_noise_suppression | [voice noise suppression configuration](#voice-noise-suppression-configuration-object) object | Client-side noise suppression rollout |
| push_service_delivery | [push service delivery configuration](#push-service-delivery-configuration-object) object | Push service delivery rollout |
| domain_migration | [domain migration configuration](#domain-migration-configuration-object) object | Web domain migration rollout |
| experiment_delivery | [experiment delivery configuration](#experiment-delivery-configuration-object) object | Cadence every client polls the experiments route on |
| registration | [registration configuration](#registration-configuration-object) object | Registration policy, issued URLs, and pending registrations |
| self_hosted | boolean | Whether the deployment runs in self-hosted mode |
@@ -138,6 +139,35 @@ The instance rollout of push service delivery.
Every field is present on read. An absent document or missing field uses the defaults above.
## Domain migration configuration object
The instance rollout that moves the official web client from its legacy origin to a new one. [Experiments](/http-api/experiments/#domain-migration-assignment-object) defines what a signed-in client resolves from it. The [instance discovery document](/http-api/instance/#domain-migration-object) publishes `enabled`, `anonymous_rollout_basis_points`, `rollout_salt`, and `standalone_forwarding` for clients that are not signed in.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| enabled | boolean | Whether the rollout runs at all (default false) |
| config_version | integer | Revision counter, raised by Fluxer and never accepted from a request |
| rollout_basis_points | integer | Share of accounts the rollout selects, in basis points (0-10000, default 0) |
| rollout_salt | string | Salt of the sampling hash (1-64 printable ASCII characters, default `domain-migration-v1`) |
| included_user_ids | array[snowflake] | Accounts the rollout always selects, up to 1000 entries (default empty) |
| excluded_user_ids | array[snowflake] | Accounts the rollout never selects, up to 1000 entries (default empty) |
| anonymous_rollout_basis_points | integer | Share of logged-out devices the rollout moves, in basis points (0-10000, default 0) |
| standalone_forwarding | boolean | Whether installed desktop web apps forward to the new origin once their data has moved (default false) |
Every field is present on read. An absent document or missing field uses the defaults above.
`excluded_user_ids` is applied before `included_user_ids`, so the rollout never selects an account in both.
An installed Chromium desktop web app moves its data like a browser tab, then stays on the legacy origin and offers to install the app from the new one. Set `standalone_forwarding` once the web app manifest lists the new origin in `scope_extensions` and the new origin serves the matching association file. From then on the installed app forwards like a browser tab. Installed mobile and Safari web apps never forward, whatever the value.
Only the official web client acts on this configuration. On any other instance it changes nothing a client does.
:::caution[`enabled` is the kill switch]
Setting `enabled` to false stops new migrations and also stops forwarding from the legacy origin for clients that already moved. Excluding an account only stops a migration that has not started yet.
:::
## Experiment delivery configuration object
How often a client polls [Get experiment assignments](/http-api/experiments/#get-experiment-assignments), and how widely those polls are spread. The setting is instance-wide and applies to every experiment at once, so adding an experiment adds no second cadence to tune.
@@ -547,6 +577,7 @@ The body has one optional object for each section. Fluxer leaves an absent secti
| gateway_rollout? | object | Any subset of the [Gateway rollout configuration](#gateway-rollout-configuration-object) fields, each bound as documented there |
| voice_noise_suppression? | object | Any subset of the [noise suppression](#voice-noise-suppression-configuration-object) fields |
| push_service_delivery? | object | Any subset of the [push service delivery](#push-service-delivery-configuration-object) fields |
| domain_migration? | object | Any subset of the [domain migration](#domain-migration-configuration-object) fields |
| experiment_delivery? | object | Any subset of the [experiment delivery](#experiment-delivery-configuration-object) fields |
| registration? | object | `mode` and `admin_registration_urls_enabled` |
| app_public?<sup>2</sup> | object | `branding`, `setup`, `legal`, and `registration` sub-objects, each merged field by field |
@@ -562,6 +593,8 @@ The body has one optional object for each section. Fluxer leaves an absent secti
`push_service_delivery` works the same way, over the [push service delivery configuration](#push-service-delivery-configuration-object) fields and its own `config_version`.
`domain_migration` works the same way, over the [domain migration configuration](#domain-migration-configuration-object) fields and its own `config_version`.
`experiment_delivery` takes both [experiment delivery configuration](#experiment-delivery-configuration-object) fields, each bound as documented there. It is a section of its own, so a write to it changes no `config_version` and changes no assignment, only the cadence on which clients ask for one.
<sup>3</sup> 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
@@ -598,7 +631,7 @@ Fluxer skips URL validation while the merged configuration leaves single sign-on
| 400 | [error response](/admin-api/#error-response) | A policy transition is refused, returned as `INSTANCE_POLICY_TRANSITION_NOT_ALLOWED` |
:::caution[Sections are applied one after another]
The order is `gateway_rollout`, `voice_noise_suppression`, `push_service_delivery`, `experiment_delivery`, `sso`, `registration`, `app_public` branding, legal, and registration fields, `integrations`, `media`, `policy`, and finally `app_public.setup`. A failure part way through leaves the earlier sections written.
The order is `gateway_rollout`, `voice_noise_suppression`, `push_service_delivery`, `domain_migration`, `experiment_delivery`, `sso`, `registration`, `app_public` branding, legal, and registration fields, `integrations`, `media`, `policy`, and finally `app_public.setup`. A failure part way through leaves the earlier sections written.
:::
### Side effects
@@ -1,7 +1,7 @@
---
# SPDX-License-Identifier: AGPL-3.0-or-later
title: Authentication resource
description: Registration, login, credentials, sessions, recovery, SSO, IP authorisation, and desktop handoff.
description: Registration, login, credentials, sessions, recovery, SSO, IP authorisation, desktop handoff, and origin handoff.
---
import RouteHeader from '@/components/RouteHeader.astro';
@@ -16,7 +16,7 @@ The Authentication resource covers signing up, signing in, recovering an account
A client MUST treat every session token, one-use token, ticket, SSO state, and handoff code as confidential. It MUST NOT parse one, expose it to untrusted code, or write it to logs, analytics, or a URL the operation did not assign.
:::
Most operations need no `Authorization` credential, because the request has a one-use token, MFA ticket, WebAuthn assertion, or account credential of its own. [List authentication sessions](#list-authentication-sessions), [terminate authentication sessions](#terminate-authentication-sessions), [resend email verification](#resend-email-verification), [log out](#log-out), and [complete desktop handoff](#complete-desktop-handoff) are the only operations on this page that read an `Authorization` credential. Complete desktop handoff reads the header only when its body omits the `token` field. The first three require an ordinary user session, and each rejects a bot token and an OAuth2 bearer token with 403 `ACCESS_DENIED`. All three still admit a session whose account is flagged for suspicious activity.
Most operations need no `Authorization` credential, because the request has a one-use token, MFA ticket, WebAuthn assertion, or account credential of its own. [List authentication sessions](#list-authentication-sessions), [terminate authentication sessions](#terminate-authentication-sessions), [resend email verification](#resend-email-verification), [log out](#log-out), [complete desktop handoff](#complete-desktop-handoff), and [create origin handoff](#create-origin-handoff) are the only operations on this page that read an `Authorization` credential. Complete desktop handoff reads the header only when its body omits the `token` field. The first three and create origin handoff require an ordinary user session, and each rejects a bot token and an OAuth2 bearer token with 403 `ACCESS_DENIED`. All four still admit a session whose account is flagged for suspicious activity.
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`.
@@ -301,6 +301,26 @@ The state of one desktop handoff as observed by the device that initiated it.
<sup>2</sup> The fields are present together only when the status is `completed`
## Origin handoff object
The identifier of one stored origin handoff.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| handoff_id | string | The single-use identifier the receiving origin redeems, 32 random bytes as 43 base64url characters |
## Origin handoff payload object
The client state released by one redeemed origin handoff.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| payload | string | The base64url string stored by [create origin handoff](#create-origin-handoff), returned unchanged |
## Get SSO status
<RouteHeader method="GET" path="/v1/auth/sso/status" unauthenticated />
@@ -1491,3 +1511,82 @@ Cancelling prevents any further token retrieval but does not revoke a session al
### Rate limit
10 requests per minute, on the `auth:handoff:cancel` bucket.
## Create origin handoff
<RouteHeader method="POST" path="/v1/auth/origin-handoff" />
Stores encrypted client state so that another web origin of the same instance can take it over once. The official web client uses it to move a signed-in browser from its legacy origin to a new one. Requires a user session token. Returns an [origin handoff](#origin-handoff-object) object.
The sending origin encrypts the state and keeps the key. Fluxer never receives the key and never reads the payload. The receiving origin holds a nonce, and the sender passes only its digest here.
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| nonce_hash | string | The SHA-256 digest of the receiving origin's nonce, as 64 lowercase hex characters |
| payload | string | The encrypted client state as base64url, 1 to 8388608 characters |
A bot token and an OAuth2 bearer token both return 403 `ACCESS_DENIED`. A session whose account is flagged for suspicious activity is admitted.
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | [origin handoff](#origin-handoff-object) object | The state was stored |
| 400 | [error response](/http-api/#error-response) | The body is invalid, returning `INVALID_FORM_BODY` |
| 400 | [error response](/http-api/#error-response) | The body is larger than 8 MiB plus 1 KiB and the request returns `FILE_SIZE_TOO_LARGE` |
| 401 | [error response](/http-api/#error-response) | The user session credential is missing or invalid |
| 403 | [error response](/http-api/#error-response) | The credential is a bot token or an OAuth2 bearer token |
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
### Side effects
Fluxer keeps the payload, the nonce digest, and the account ID for two minutes under a key derived from the identifier. Creating a handoff creates no session and changes no account state. No Gateway Dispatch is emitted.
### Rate limit
3 requests per 10 minutes, on the `auth:origin_handoff:create` bucket.
## Redeem origin handoff
<RouteHeader method="POST" path="/v1/auth/origin-handoff/redeem" unauthenticated />
Releases the client state stored by [create origin handoff](#create-origin-handoff) and deletes it in the same step. Authentication is not required. Returns an [origin handoff payload](#origin-handoff-payload-object) object.
### JSON body
| Field | Type | Description |
| --- | --- | --- |
| handoff_id | string | The identifier returned by create origin handoff, 43 base64url characters |
| nonce | string | The nonce whose digest the sender supplied, as base64url, 16 to 256 characters |
An unknown, expired, or already redeemed identifier returns 404 `UNKNOWN_ORIGIN_HANDOFF`. A nonce whose SHA-256 digest does not match returns 400 `INVALID_ORIGIN_HANDOFF_NONCE`.
:::caution[Every attempt consumes the handoff]
Fluxer deletes the stored state before it compares the nonce. A wrong nonce leaves nothing to retry, so the sender MUST create a new handoff.
:::
On an instance that is not self-hosted, the request MUST send an `Origin` header naming one of the instance's web app origins. Any other `Origin`, or none, returns 403 `INVALID_API_ORIGIN`. A self-hosted instance skips this check.
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | [origin handoff payload](#origin-handoff-payload-object) object | The state was released and deleted |
| 400 | [error response](/http-api/#error-response) | The body is invalid, or the nonce does not match |
| 403 | [error response](/http-api/#error-response) | The `Origin` is not a web app origin of the instance, returning `INVALID_API_ORIGIN` |
| 404 | [error response](/http-api/#error-response) | No stored handoff has this identifier, returning `UNKNOWN_ORIGIN_HANDOFF` |
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
### Side effects
A redemption that finds the handoff deletes it, whether or not the nonce matches. Redemption creates no session. The payload is all the receiving origin gets. No Gateway Dispatch is emitted.
### Rate limit
10 requests per minute, on the `auth:origin_handoff:redeem` bucket.
@@ -540,6 +540,10 @@ Invalid form body
Invalid handoff code
### `INVALID_ORIGIN_HANDOFF_NONCE`
This sign-in transfer doesn't match the one you started
### `INVALID_PERMISSIONS_INTEGER`
Permissions must be a valid integer
@@ -1100,6 +1104,10 @@ Member wasn't found in this community
Message wasn't found
### `UNKNOWN_ORIGIN_HANDOFF`
This sign-in transfer has expired or was already used
### `UNKNOWN_REPORT`
Unknown report
@@ -6,7 +6,7 @@ description: The experiment assignments envelope, the revalidation and polling c
import RouteHeader from '@/components/RouteHeader.astro';
An experiment is an instance-wide rollout that an operator configures. For each account, Fluxer works out from that configuration whether the account is in the rollout and which settings the account receives. The single route on this page resolves every experiment the server defines and returns them in one envelope, together with the polling cadence they share. This server defines `voice_noise_suppression`, whose placement protocol [Voice](/voice/) defines.
An experiment is an instance-wide rollout that an operator configures. For each account, Fluxer works out from that configuration whether the account is in the rollout and which settings the account receives. The single route on this page resolves every experiment the server defines and returns them in one envelope, together with the polling cadence they share. This server defines `voice_noise_suppression`, whose placement protocol [Voice](/voice/) defines, and `domain_migration`.
Every assignment is advice. A client that ignores one behaves as it does with the rollout off, and no route and no Gateway event reports what a client actually ran.
@@ -33,6 +33,7 @@ One entry per experiment. The envelope reports this object even when it is empty
| Field | Type | Description |
| --- | --- | --- |
| voice_noise_suppression? | [noise suppression assignment](#noise-suppression-assignment-object) object | The caller's noise suppression assignment |
| domain_migration? | [domain migration assignment](#domain-migration-assignment-object) object | The caller's web domain migration assignment |
Ignore unknown experiments and treat a missing experiment as off.
@@ -101,6 +102,20 @@ One backend replacement scoped to one guild. While the caller is connected to a
An override naming a backend that is absent from `enabled_backends` is dropped before the response is written, so every entry is runnable.
## Domain migration assignment object
One resolution of the instance web domain migration rollout against one account. This server version writes the key on every response.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| enabled | boolean | Whether the caller's web client moves to the new web origin |
A caller is drawn either by the operator's allowlist or by the sampled share of the account population. `enabled` is false in every other case, the rollout being off included.
Only the official web client acts on this assignment, and only on its legacy origins. Every other client ignores it. The logged-out share and the instance-wide switch are published in the [instance discovery document](/http-api/instance/#domain-migration-object) instead, because a client that holds no credential cannot read this route.
## Get experiment assignments
<RouteHeader method="GET" path="/v1/experiments" bot />
@@ -16,7 +16,7 @@ Each of the routes answers with `Access-Control-Allow-Origin: *`, replaced by th
## Instance discovery object
The discovery document is the complete public description of one deployment. Every field is always present.
The discovery document is the complete public description of one deployment. Every field is always present. The one optional field, `domain_migration`, is absent only from a server older than this version.
### Structure
@@ -34,6 +34,7 @@ The discovery document is the complete public description of one deployment. Eve
| limits | [limit configuration](#limit-configuration-object) object | Ordered limit rules and the trait names the deployment declares |
| push | [push configuration](#push-configuration-object) object | Public Web Push identity |
| app_public | [public application configuration](#public-application-configuration-object) object | Public client configuration |
| domain_migration? | [domain migration](#domain-migration-object) object | Web domain migration switch and logged-out rollout |
<sup>1</sup> The value is a plain integer that increases when the API introduces a change a client is expected to notice, and the current value is `1`
@@ -424,6 +425,25 @@ Which extra fields public registration collects.
| --- | --- | --- |
| collect_date_of_birth | boolean | Whether public registration collects and validates a date of birth |
## Domain migration object
The public part of the instance [domain migration configuration](/admin-api/instance/#domain-migration-configuration-object). The official web client reads it before sign-in to decide whether its legacy origin forwards to the new one. Every instance publishes it, and only the official web client acts on it.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| enabled | boolean | Whether the migration runs at all |
| anonymous_rollout_basis_points | integer | Share of logged-out devices moved to the new origin, in basis points (0-10000) |
| rollout_salt | string | Salt of the sampling hash, shared by accounts and devices |
| standalone_forwarding | boolean | Whether installed desktop web apps forward to the new origin once their data has moved |
`enabled` is the instance-wide switch. While it is false, no client starts a migration and a client that already migrated stops forwarding from the legacy origin.
A logged-out device draws its own random ID once and keeps it. The device moves when its bucket over that ID and `rollout_salt` falls below `anonymous_rollout_basis_points`. Signed-in accounts use the [domain migration assignment](/http-api/experiments/#domain-migration-assignment-object) instead.
While `standalone_forwarding` is false, an installed desktop web app moves its data and stays on the legacy origin. Browser tabs forward either way.
## Geolocation object
The approximate location Fluxer resolved for the request, together with the regions where mature content is gated behind an age check or is unavailable.
@@ -32,7 +32,7 @@ The [global HTTP limit](/topics/rate-limits/) applies to every management route.
## Origin refusal
Fluxer refuses a call from the official web client on [Execute webhook](#execute-webhook), [Get webhook message](#get-webhook-message), [Edit webhook message](#edit-webhook-message), and [Delete webhook message](#delete-webhook-message). A request whose `Origin` header is exactly `https://web.fluxer.app` or `https://web.canary.fluxer.app` returns 403 `INVALID_API_ORIGIN`.
Fluxer refuses a call from a first-party web client on [Execute webhook](#execute-webhook), [Get webhook message](#get-webhook-message), [Edit webhook message](#edit-webhook-message), and [Delete webhook message](#delete-webhook-message). A request whose `Origin` header is exactly `https://web.fluxer.app`, `https://web.canary.fluxer.app`, or one of the instance's own web app origins returns 403 `INVALID_API_ORIGIN`. Those are the origin of `FLUXER_APP_ENDPOINT` and every `FLUXER_APP_ORIGIN_ALIASES` entry.
A request that sends no `Origin`, or any other `Origin` value, passes the check. It runs after the route rate limit and before path validation.
@@ -1135,7 +1135,7 @@ The operation removes the webhook, frees its guild and channel webhook slot, and
Creates a webhook-authored [message](/http-api/messages/#message-object) in the webhook's channel. When `wait` is true the created message is returned, and otherwise the operation returns 204 with an empty body.
The route refuses a call from the official web client. See [origin refusal](#origin-refusal). A successful execution emits a [Message Create](/gateway/events/#message-create) Gateway Dispatch whether or not wait is true.
The route refuses a call from a first-party web client. See [origin refusal](#origin-refusal). A successful execution emits a [Message Create](/gateway/events/#message-create) Gateway Dispatch whether or not wait is true.
### Path parameters
@@ -1187,7 +1187,7 @@ An attachment metadata entry whose `id` matches a supplied file index supplies t
| 400<sup>1</sup> | [error response](/http-api/#error-response) | The resolved payload has no content, embed, or attachment |
| 400<sup>1</sup> | [error response](/http-api/#error-response) | The content exceeds the effective maximum length |
| 400<sup>1</sup> | [error response](/http-api/#error-response) | The embed or attachment count exceeds its effective ceiling |
| 403 | [error response](/http-api/#error-response) | Request has an official web client `Origin`, returning `INVALID_API_ORIGIN` |
| 403 | [error response](/http-api/#error-response) | Request has a first-party web client `Origin`, returning `INVALID_API_ORIGIN` |
| 403 | [error response](/http-api/#error-response) | The resolved content or embed text is blocked, returning `CONTENT_BLOCKED` |
| 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` |
@@ -1223,7 +1223,7 @@ The operation updates channel state and search results and emits [Message Create
Returns a [message](/http-api/messages/#message-object) that the webhook authored.
The route refuses a call from the official web client. See [origin refusal](#origin-refusal).
The route refuses a call from a first-party web client. See [origin refusal](#origin-refusal).
### Path parameters
@@ -1240,7 +1240,7 @@ The route refuses a call from the official web client. See [origin refusal](#ori
| Status | Body | Condition |
| --- | --- | --- |
| 200 | [message](/http-api/messages/#message-object) object | Webhook message was returned |
| 403 | [error response](/http-api/#error-response) | Request has an official web client `Origin`, returning `INVALID_API_ORIGIN` |
| 403 | [error response](/http-api/#error-response) | Request has a first-party web client `Origin`, returning `INVALID_API_ORIGIN` |
| 403 | [error response](/http-api/#error-response) | The message was authored by another webhook, a user, or a bot, returning `MISSING_PERMISSIONS` |
| 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 webhook has no channel target, returning `UNKNOWN_CHANNEL` |
@@ -1256,7 +1256,7 @@ The route refuses a call from the official web client. See [origin refusal](#ori
Updates a message that the webhook authored and returns the modified [message](/http-api/messages/#message-object).
The route refuses a call from the official web client. See [origin refusal](#origin-refusal). A successful edit emits a [Message Update](/gateway/events/#message-update).
The route refuses a call from a first-party web client. See [origin refusal](#origin-refusal). A successful edit emits a [Message Update](/gateway/events/#message-update).
### Path parameters
@@ -1284,7 +1284,7 @@ Any other type returns 400 `CANNOT_MODIFY_SYSTEM_WEBHOOK`, and a forward, which
| 400 | [error response](/http-api/#error-response) | The content or embed count exceeds its effective ceiling |
| 400 | [error response](/http-api/#error-response) | The target message is not an editable type |
| 400 | [error response](/http-api/#error-response) | The webhook's stored channel no longer resolves to a guild channel |
| 403 | [error response](/http-api/#error-response) | Request has an official web client `Origin`, returning `INVALID_API_ORIGIN` |
| 403 | [error response](/http-api/#error-response) | Request has a first-party web client `Origin`, returning `INVALID_API_ORIGIN` |
| 403 | [error response](/http-api/#error-response) | The message was authored by another webhook, a user, or a bot, returning `MISSING_PERMISSIONS` |
| 403 | [error response](/http-api/#error-response) | The replacement content or embed text is blocked, returning `CONTENT_BLOCKED` |
| 404 | [error response](/http-api/#error-response) | Webhook and token pair does not exist, returning `UNKNOWN_WEBHOOK` |
@@ -1314,7 +1314,7 @@ The operation replaces the supplied fields, and a content change marks the messa
Deletes a message that the webhook authored and returns 204 with an empty body on success.
The route refuses a call from the official web client. See [origin refusal](#origin-refusal). Deletion emits a [Message Delete](/gateway/events/#message-delete).
The route refuses a call from a first-party web client. See [origin refusal](#origin-refusal). Deletion emits a [Message Delete](/gateway/events/#message-delete).
### Path parameters
@@ -1331,7 +1331,7 @@ The route refuses a call from the official web client. See [origin refusal](#ori
| 204 | empty | Webhook message was deleted |
| 400 | [error response](/http-api/#error-response) | Path parameters are invalid |
| 400 | [error response](/http-api/#error-response) | The webhook's stored channel no longer resolves to a guild channel, returning `CANNOT_EXECUTE_ON_DM` |
| 403 | [error response](/http-api/#error-response) | Request has an official web client `Origin`, returning `INVALID_API_ORIGIN` |
| 403 | [error response](/http-api/#error-response) | Request has a first-party web client `Origin`, returning `INVALID_API_ORIGIN` |
| 403 | [error response](/http-api/#error-response) | The message was authored by another webhook, a user, or a bot, returning `MISSING_PERMISSIONS` |
| 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 message does not exist in the webhook's channel, returning `UNKNOWN_MESSAGE` |
@@ -199,6 +199,10 @@ Defaults to the derived endpoint. The API base handed to clients. Setting only o
Defaults to the derived endpoint. The web app origin. Also one of the allowed CORS origins. Serving the client from another hostname requires setting this.
#### `FLUXER_APP_ORIGIN_ALIASES`
Default empty. Further web app origins, comma separated. Each entry must be an HTTP or HTTPS origin, or the API refuses to start. The API treats each one like the `FLUXER_APP_ENDPOINT` origin. It allows it through CORS, reads invite links on its host, skips unfurling client routes on its host, and refuses webhook token calls from it. Set it when one instance serves the web app on more than one hostname.
#### `FLUXER_GATEWAY_ENDPOINT`
Defaults to the derived endpoint. The public Gateway WebSocket URL. Also the source of the internal Gateway URL when `FLUXER_INTERNAL_GATEWAY_ENDPOINT` is unset, with `ws` rewritten to `http`.
@@ -1597,6 +1601,14 @@ Default `/api`. The API path put in the page bootstrap.
No default. The absolute API URL in the bootstrap. Compose builds it from the public origin.
#### `FLUXER_APP_PROXY_SAME_ORIGIN_HOSTS`
Default empty. Hostnames, comma separated, without a scheme, port or path. When a request's `Host` is in the list, the page bootstrap points the web app and its API at that host, as `https://<host>` and `https://<host>/api`. Every other request gets the discovery values unchanged. Set it when the web app is served on more than one hostname and each hostname routes `/api` to the API. Invalid entries are logged and dropped.
#### `FLUXER_APP_PROXY_MANIFEST_SCOPE_EXTENSIONS`
Default empty. HTTPS origins, comma separated, without a path, query or credentials. `app-proxy` writes each one into the `scope_extensions` list of the web app manifest, so an installed web app keeps other origins inside its window. Empty leaves the list empty. Each listed origin must serve `/.well-known/web-app-origin-association` naming this web app, or browsers ignore the entry. Invalid entries are logged and dropped.
## Admin settings
`admin` serves the dashboard at `/admin`. All are optional.
@@ -2049,7 +2061,7 @@ The edge is the only HTTP entry point, and every route is served from the one pu
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`.
CORS origins are exactly the app and marketing endpoints. Serving the client from a hostname other than `FLUXER_DOMAIN` requires overriding `FLUXER_APP_ENDPOINT`.
CORS origins are exactly the app endpoint, the `FLUXER_APP_ORIGIN_ALIASES` origins, and the marketing endpoint. Serving the client from a hostname other than `FLUXER_DOMAIN` requires overriding `FLUXER_APP_ENDPOINT`.
## Volumes and buckets