fix: tighten edge cases across services (#3158)

This commit is contained in:
Hampus
2026-10-03 13:04:29 +02:00
committed by GitHub
parent a9f7a23c0d
commit 4e6b837ccc
170 changed files with 2028 additions and 421 deletions
+6 -6
View File
@@ -38,7 +38,7 @@ const MAIN_SPEC_EXEMPT = new Map<string, {file: string; anchor: string; reason:
{
file: 'fluxer_api/src/api/openapi/OpenAPIController.ts',
anchor: "app.get('/openapi.json'",
reason: 'the handler serves the spec file itself and carries no OpenAPI({...}) block',
reason: 'the handler serves the spec file itself and has no OpenAPI({...}) block',
},
],
[
@@ -1154,7 +1154,7 @@ console.log('self-hosting guide against deploy/self-hosting');
for (const row of shellRows(fn, 'KEYS', label)) {
const parsed = row.match(/^([A-Z][A-Z0-9_]*) ([a-z0-9_]+)(?: .*)?$/u);
if (parsed == null) {
problems.push(`${label} carries the row \`${row}\`, which is not "NAME kind"`);
problems.push(`${label} has the row \`${row}\`, which is not "NAME kind"`);
continue;
}
keys.set(parsed[1], parsed[2]);
@@ -1167,7 +1167,7 @@ console.log('self-hosting guide against deploy/self-hosting');
for (const row of powershellRows(variable, label)) {
const parsed = row.match(/^\s*@\{Name = '([A-Z][A-Z0-9_]*)'; Kind = '([a-z0-9_]+)'/u);
if (parsed == null) {
problems.push(`${label} carries the row \`${row.trim()}\`, which is not an @{Name; Kind} entry`);
problems.push(`${label} has the row \`${row.trim()}\`, which is not an @{Name; Kind} entry`);
continue;
}
keys.set(parsed[1], parsed[2]);
@@ -1291,7 +1291,7 @@ console.log('self-hosting guide against deploy/self-hosting');
for (const row of powershellRows('FluxerStackFiles', 'the install.ps1 download list')) {
const parsed = row.match(/^\s*'([^']+)'\s*$/u);
if (parsed == null) {
problems.push(`the install.ps1 download list carries \`${row.trim()}\`, which is not a quoted file name`);
problems.push(`the install.ps1 download list has \`${row.trim()}\`, which is not a quoted file name`);
continue;
}
powershellStackFiles.push(parsed[1]);
@@ -1314,7 +1314,7 @@ console.log('self-hosting guide against deploy/self-hosting');
for (const row of powershellRows('FluxerComposeNames', 'the install.ps1 compose name list')) {
const parsed = row.match(/^\s*'([^']+)'\s*$/u);
if (parsed == null) {
problems.push(`the install.ps1 compose name list carries \`${row.trim()}\`, which is not a quoted file name`);
problems.push(`the install.ps1 compose name list has \`${row.trim()}\`, which is not a quoted file name`);
continue;
}
powershellComposeNames.push(parsed[1]);
@@ -1391,7 +1391,7 @@ console.log('self-hosting guide against deploy/self-hosting');
for (const [where, text] of digestBearing) {
if (/\b[0-9a-f]{64}\b/u.test(text)) {
problems.push(
`${where} carries a literal 64-character hex digest, which goes stale the next time a script changes`,
`${where} contains a literal 64-character hex digest, which goes stale the next time a script changes`,
);
}
}
@@ -240,15 +240,14 @@ The account's set of live sessions changed. The payload is a bare JSON array of
### <span id="auth-session-change"></span>AUTH_SESSION_CHANGE
The account's authentication session was rotated, for example by a password change on another device.
The account's authentication session was rotated by a password change.
| Field | Type | Description |
| --- | --- | --- |
| 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 | 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`.
The event never includes the replacement token. The API closes every gateway session of the replaced authentication session before it sends the event, so those sessions do not receive it. The client that changed the password gets the replacement token and `auth_session_id_hash` in the HTTP response. It MUST use that token for every later HTTP request and for any later [Identify](/gateway/commands/#identify), and MUST replace its own `auth_session_id_hash` from [Ready](#ready) with the one in the response.
### <span id="rate-limited"></span>RATE_LIMITED
@@ -86,7 +86,7 @@ No permission applies.
Outside a development instance, Fluxer also rejects a URL that omits a top-level domain.
Explicit media can carry `CONTAINS_EXPLICIT_MEDIA` in its [flags](/http-api/messages/#attachment-flags). Klipy media is not classified. Message previews in channels that permit explicit media can have different flags.
Explicit media can have `CONTAINS_EXPLICIT_MEDIA` in its [flags](/http-api/messages/#attachment-flags). Klipy media is not classified. Message previews in channels that permit explicit media can have different flags.
:::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.
@@ -156,7 +156,7 @@ 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.
:::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).
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 user object in the response then also has the fields of the [password change completion object](/http-api/users/email-and-password/#password-change-completion-object), and the client sends every later request with that token. [Auth Session Change](/gateway/events/#auth-session-change) never includes it.
:::
### Response
@@ -164,6 +164,7 @@ Supplying `new_password` on a claimed account deletes every other authentication
| Status | Body | Condition |
| --- | --- | --- |
| 200 | [user](/http-api/users/#user-object) object | Account was returned after applying every permitted field |
| 200 | [user](/http-api/users/#user-object) object with the [password change completion](/http-api/users/email-and-password/#password-change-completion-object) fields | Account was returned after a password change rotated the session |
| 400 | [error response](/http-api/#error-response) | Body, image, tag, password, entitlement, secondary control, or sudo proof is invalid |
| 403 | [error response](/http-api/#error-response) | Email verification, sudo verification, or a staff-only field is required, or content is blocked |
| 403 | [error response](/http-api/#error-response) | The account is limited and the body changes a profile field, and the request returns `ACCOUNT_LIMITED` |
@@ -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) returns after the password is written.
The replacement session [Complete password change](#complete-password-change) returns after the password is written. [Modify current user](/http-api/users/current-user/#modify-current-user) adds the same fields to its user object when it changes the password.
### Structure
+1 -1
View File
@@ -73,7 +73,7 @@ Fluxer reports a refusal by sending no Dispatch. A client observes a refused pla
A guild voice channel stores its `bitrate`, `user_limit`, `voice_connection_limit`, and `rtc_region` on the [channel object](/http-api/channels/#channel-object). It also has ordinary messages, pins, and slowmode, so its text history is read and written through the [Messages resource](/http-api/messages/).
A new voice channel stores a `bitrate` of 64000. The ceiling is 96000, and the `AUDIO_BITRATE_128_KBPS`, `AUDIO_BITRATE_256_KBPS`, and `AUDIO_BITRATE_384_KBPS` [guild features](/http-api/guilds/#guild-features) raise it to 128000, 256000, and 384000. A direct message and a group direct message call carry no `bitrate` and always run at 64000.
A new voice channel stores a `bitrate` of 64000. The ceiling is 96000, and the `AUDIO_BITRATE_128_KBPS`, `AUDIO_BITRATE_256_KBPS`, and `AUDIO_BITRATE_384_KBPS` [guild features](/http-api/guilds/#guild-features) raise it to 128000, 256000, and 384000. A direct message and a group direct message call have no `bitrate` and always run at 64000.
### Permissions
+6 -6
View File
@@ -11,11 +11,11 @@
# -Rollback Put the images and the stack files of the last recorded upgrade back.
#
# Why one script and not a separate upgrader: an upgrade needs the host checks, the stack
# download, the readiness poll and the health probe that the install already carries. A second
# download, the readiness poll and the health probe that the install already has. A second
# script either copies them or drifts from them, and the operator has two downloads and two
# checksums to verify instead of one.
#
# This file is also the procedure. Every step of the upgrade carries the command an operator
# This file is also the procedure. Every step of the upgrade includes the command an operator
# types to do that step by hand, and the reason the step exists.
#
# Read this file before running it. The default mode writes .env, which holds every secret the
@@ -664,7 +664,7 @@ function Get-FluxerStackFiles([string]$StagingDir, [string]$RefValue) {
# None of these files is part of an image, and all four are read from the working directory, so
# docker compose pull never updates any of them. That is why an upgrade refreshes them itself.
#
# A refreshed docker-compose.yml can declare a variable the running .env does not carry. Compose
# A refreshed docker-compose.yml can declare a variable the running .env does not define. Compose
# writes ${NAME:?message} for a variable the stack requires and stops with that message until .env
# sets it, and ${NAME:-default} for one that needs nothing from the operator. Every optional
# override ships commented out in .env.example, so a new required key is the only kind that asks
@@ -684,7 +684,7 @@ function Move-FluxerStackFiles([string]$StagingDir, [string]$TargetDir) {
# values only the operator knows. The five other non-secret keys in the list above ship correct in
# .env.example and need no edit.
#
# Every secret in .env.example carries the literal CHANGE_ME. A key whose name ends in _BASE64
# Every secret in .env.example contains the literal CHANGE_ME. A key whose name ends in _BASE64
# takes 32 random bytes as base64, every other key takes 32 random bytes as hex, and the VAPID pair
# comes from the generator above.
#
@@ -1233,7 +1233,7 @@ function Write-FluxerTextFile([string]$Path, [string[]]$Lines) {
# keep, and what makes a rollback possible on a moving tag.
#
# The reference list comes from Compose and the ID under each reference comes from the container
# running it, for the reason in Get-FluxerRunningImageIds. A reference no container carries is
# running it, for the reason in Get-FluxerRunningImageIds. A reference no container uses is
# recorded as `-`, which a rollback skips, because a version that was not running is not a version
# to go back to.
#
@@ -1748,7 +1748,7 @@ function Invoke-FluxerUpgrade([string]$TargetDir, [string]$EnvPath, [string]$Bac
#
# Two shapes, depending on what the upgrade moved:
#
# A pinned tag moved, so the old images still carry their own tag. The tag goes back into .env
# A pinned tag moved, so the old images still have their own tag. The tag goes back into .env
# and Compose finds them.
#
# By hand: set FLUXER_IMAGE_TAG back, then docker compose up -d
+9 -9
View File
@@ -14,11 +14,11 @@
#
# Why one script and not a separate upgrader: an upgrade needs the host checks,
# the stack download, the readiness poll and the health probe that the install
# already carries. A second script either copies them or drifts from them, and
# already has. A second script either copies them or drifts from them, and
# the operator has two downloads and two checksums to verify instead of one.
# The modes share one contract, one digest and one set of exit codes.
#
# This file is also the procedure. Every step of the upgrade carries the command
# This file is also the procedure. Every step of the upgrade includes the command
# an operator types to do that step by hand, and the reason the step exists.
#
# Read this file before you run it. The default mode writes .env, which holds
@@ -86,7 +86,7 @@ FLUXER_DUMP_FILE='fluxer.dump'
# with it.
FLUXER_VOLUME_HEADROOM=110
# The keys .env carries, in the order they are written. The installer iterates
# The keys .env holds, in the order they are written. The installer iterates
# these two lists, so a key that leaves a list is a key the installer stops
# writing. The docs CI parses the same text and compares it against
# deploy/self-hosting/.env.example.
@@ -801,7 +801,7 @@ fluxer_fetch_stack() {
# upgrade refreshes them itself.
#
# A refreshed docker-compose.yml can declare a variable the running .env does not
# carry. Compose writes ${NAME:?message} for a variable the stack requires and
# define. Compose writes ${NAME:?message} for a variable the stack requires and
# stops with that message until .env sets it, and ${NAME:-default} for one that
# needs nothing from the operator. Every optional override ships commented out in
# .env.example, so a new required key is the only kind that asks for an edit.
@@ -900,7 +900,7 @@ fluxer_generate_vapid() {
# operator knows. The five other non-secret keys in the list above ship correct
# in .env.example and need no edit.
#
# Every secret in .env.example carries the literal CHANGE_ME. A key whose name
# Every secret in .env.example contains the literal CHANGE_ME. A key whose name
# ends in _BASE64 takes openssl rand -base64 32, every other key takes
# openssl rand -hex 32, and the VAPID pair comes from the generator above.
#
@@ -1412,7 +1412,7 @@ $(fluxer_indent_file "$fluxer_scratch/inspect-err" ' ')"
sort -u "$fluxer_scratch/inspected"
}
# The recorded ID for one reference, or nothing when no container carries it.
# The recorded ID for one reference, or nothing when no container uses it.
fluxer_recorded_id_for() {
awk -v fluxer_want="$1" '$1 == fluxer_want {print $2; exit}' "$fluxer_scratch/running"
}
@@ -1427,7 +1427,7 @@ fluxer_recorded_id_for() {
#
# The reference list comes from Compose and the ID under each reference comes
# from the container running it, for the reason in fluxer_running_image_ids. A
# reference no container carries is recorded as `-`, which a rollback skips,
# reference no container uses is recorded as `-`, which a rollback skips,
# because a version that was not running is not a version to go back to.
#
# By hand:
@@ -1770,7 +1770,7 @@ fluxer_prepare_record() {
mkdir -m 700 "$fluxer_record"
}
# Record names carry a UTC stamp, so the shell expands the glob in byte order
# Record names include a UTC stamp, so the shell expands the glob in byte order
# and the last match is the most recent upgrade.
fluxer_newest_record() {
fluxer_newest=''
@@ -2008,7 +2008,7 @@ fluxer_set_image_tag() {
#
# Two shapes, depending on what the upgrade moved:
#
# A pinned tag moved, so the old images still carry their own tag. The tag goes
# A pinned tag moved, so the old images still have their own tag. The tag goes
# back into .env and Compose finds them.
#
# By hand: set FLUXER_IMAGE_TAG back, then docker compose up -d