mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
feat(captcha): add ALTCHA proof-of-work captcha experiment (#2986)
This commit is contained in:
@@ -37,6 +37,7 @@ Missing settings use the defaults documented below. Invalid stored configuration
|
||||
| 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 |
|
||||
| altcha_captcha | [ALTCHA captcha configuration](#altcha-captcha-configuration-object) object | ALTCHA proof-of-work captcha 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 |
|
||||
@@ -173,6 +174,32 @@ Only the official web client acts on this configuration. On any other instance i
|
||||
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.
|
||||
:::
|
||||
|
||||
## ALTCHA captcha configuration object
|
||||
|
||||
The instance rollout that replaces the configured captcha provider with an ALTCHA proof-of-work challenge the API issues and verifies itself. [Experiments](/http-api/experiments/#altcha-captcha-assignment-object) defines what a signed-in client resolves from it, and [CAPTCHA handling](/topics/captcha/#altcha-proof-of-work) defines the challenge exchange.
|
||||
|
||||
### 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 `altcha-captcha-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_enabled | boolean | Whether logged-out requests get ALTCHA (default false) |
|
||||
| cost | integer | PBKDF2 iterations per solving attempt (1000-100000, default 5000) |
|
||||
| max_counter | integer | Upper bound of the hidden counter a client searches for (100-1000000, default 10000) |
|
||||
|
||||
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. `anonymous_enabled` has no effect on signed-in requests, and the account rules have no effect on logged-out ones.
|
||||
|
||||
Each challenge hides its counter between half of `max_counter` and `max_counter`, and a client tries counters from 0 upward. Solve time grows with `cost` times `max_counter`. Fluxer spends one attempt at `cost` to issue each challenge.
|
||||
|
||||
The rollout only changes which provider answers a captcha that is already required. While the instance captcha provider is `none`, it has no effect.
|
||||
|
||||
## 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.
|
||||
@@ -583,6 +610,7 @@ The body has one optional object for each section. Fluxer leaves an absent secti
|
||||
| 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 |
|
||||
| altcha_captcha? | object | Any subset of the [ALTCHA captcha](#altcha-captcha-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 |
|
||||
@@ -600,6 +628,8 @@ The body has one optional object for each section. Fluxer leaves an absent secti
|
||||
|
||||
`domain_migration` works the same way, over the [domain migration configuration](#domain-migration-configuration-object) fields and its own `config_version`.
|
||||
|
||||
`altcha_captcha` works the same way, over the [ALTCHA captcha configuration](#altcha-captcha-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
|
||||
@@ -636,7 +666,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`, `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.
|
||||
The order is `gateway_rollout`, `voice_noise_suppression`, `push_service_delivery`, `domain_migration`, `altcha_captcha`, `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
|
||||
|
||||
@@ -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, and `domain_migration`.
|
||||
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, `domain_migration` and `altcha_captcha`.
|
||||
|
||||
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.
|
||||
|
||||
@@ -34,6 +34,7 @@ One entry per experiment. The envelope reports this object even when it is empty
|
||||
| --- | --- | --- |
|
||||
| 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 |
|
||||
| altcha_captcha? | [ALTCHA captcha assignment](#altcha-captcha-assignment-object) object | The caller's captcha provider assignment |
|
||||
|
||||
Ignore unknown experiments and treat a missing experiment as off.
|
||||
|
||||
@@ -116,6 +117,20 @@ A caller is drawn either by the operator's allowlist or by the sampled share of
|
||||
|
||||
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.
|
||||
|
||||
## ALTCHA captcha assignment object
|
||||
|
||||
One resolution of the instance ALTCHA captcha rollout against one account. This server version writes the key on every response.
|
||||
|
||||
### Structure
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| enabled | boolean | Whether the caller's captcha challenges are ALTCHA proof-of-work challenges |
|
||||
|
||||
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.
|
||||
|
||||
The assignment is informational. The server applies the same resolution to every request that needs a captcha and names the provider in the [captcha error](/topics/captcha/#altcha-proof-of-work), so a client needs no copy of this value to answer a challenge.
|
||||
|
||||
## Get experiment assignments
|
||||
|
||||
<RouteHeader method="GET" path="/v1/experiments" bot />
|
||||
|
||||
@@ -37,11 +37,11 @@ Fluxer skips the check in three cases, and the operation then proceeds with no C
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| X-Captcha-Token?<sup>1</sup> | string | The solution issued by the provider widget |
|
||||
| X-Captcha-Type?<sup>2</sup> | string | The provider that produced the solution, accepting `hcaptcha` or `turnstile` |
|
||||
| X-Captcha-Type?<sup>2</sup> | string | The provider that produced the solution, accepting `hcaptcha`, `turnstile` or `altcha` |
|
||||
|
||||
<sup>1</sup> An absent or empty value on a gated operation returns 400 `CAPTCHA_REQUIRED`
|
||||
|
||||
<sup>2</sup> An absent value selects the instance's configured provider, and so does any value other than `hcaptcha` or `turnstile`. Naming a provider the instance holds no secret key for returns 400 `INVALID_CAPTCHA`.
|
||||
<sup>2</sup> An absent value selects the instance's configured provider, and so does any value other than `hcaptcha`, `turnstile` or `altcha`. Naming a provider the instance holds no secret key for returns 400 `INVALID_CAPTCHA`. The value `altcha` is accepted only from a requester the [ALTCHA rollout](#altcha-proof-of-work) selects.
|
||||
|
||||
## The retry handshake
|
||||
|
||||
@@ -53,6 +53,21 @@ An accepted solution allows the operation to proceed. A rejected solution return
|
||||
The provider treats an already redeemed solution as invalid. A client obtains a new solution before retrying after `INVALID_CAPTCHA` and MUST NOT replay the previous `X-Captcha-Token` value.
|
||||
:::
|
||||
|
||||
## ALTCHA proof-of-work
|
||||
|
||||
An operator can move selected requesters from the configured provider to an [ALTCHA](https://altcha.org) proof-of-work challenge that the API issues and verifies itself. The [ALTCHA captcha configuration](/admin-api/instance/#altcha-captcha-configuration-object) selects signed-in accounts by rollout share and allowlist, and logged-out requests with one switch. The check applies only where a captcha is already required, so an instance whose `provider` is `none` never serves it.
|
||||
|
||||
For a selected requester, the `CAPTCHA_REQUIRED` and `INVALID_CAPTCHA` bodies have two more fields.
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| captcha_provider | string | Always `altcha` |
|
||||
| altcha_challenge | object | An ALTCHA v2 challenge, with `parameters` and `signature` |
|
||||
|
||||
The challenge uses `PBKDF2/SHA-256` and expires 10 minutes after it is issued. Solve it with an ALTCHA v2 solver, then retry with `X-Captcha-Type` set to `altcha` and `X-Captcha-Token` set to the base64 encoding of the JSON object `{"challenge": <the challenge>, "solution": <the solution>}`. Each challenge is accepted once. A replayed, expired or wrong solution returns 400 `INVALID_CAPTCHA` with a new challenge.
|
||||
|
||||
A selected requester can still answer with the configured provider, so a client that does not read these fields keeps working.
|
||||
|
||||
## Provider verification
|
||||
|
||||
A rejected solution or unavailable provider returns 400 `INVALID_CAPTCHA`. The response does not distinguish between these causes.
|
||||
|
||||
Reference in New Issue
Block a user