feat(pds): Einladungscodes für createAccount
Die Instanz soll öffentlich erreichbar werden. createAccount hatte bis
jetzt keinerlei Schranke: kein Code, kein Rate-Limit, describeServer
meldete invite_code_required hartkodiert false. Jeder hätte beliebig viele
Konten anlegen können, jedes mit eigenem Repo, Blöcken und
Firehose-Events.
Zwei Tabellen: invite_codes trägt Zähler und Sperre, invite_code_uses
protokolliert, welcher DID welchen Code eingelöst hat — ein Code kann
mehrere Konten wert sein, also ist "wer hat ihn benutzt" eine Menge. Die
DID hat bewusst keinen Fremdschlüssel: das Protokoll soll das Konto
überleben.
Der Kern ist das Einlösen ohne Rennen. Geprüft wird nicht vorher, sondern
im Schreiben selbst:
UPDATE invite_codes SET used_count = used_count + 1
WHERE code = $1 AND NOT disabled AND used_count < max_uses
RETURNING used_count, max_uses
Der Verlierer zweier gleichzeitiger Registrierungen blockiert auf der
Zeilensperre, liest danach die committete Zeile neu, wertet die
WHERE-Klausel erneut aus, trifft nichts und bekommt 400. Ein
Lesen-dann-Schreiben hätte beide durchgelassen — ich habe genau das
probeweise eingebaut, woraufhin die Nebenläufigkeitstests umfielen
("a one-use code let 5 concurrent registrations through").
Das Einlösen ist die erste Anweisung in der bestehenden Transaktion von
create_account: die Zeilensperre hält über die Konto-Inserts, und ein
Rollback gibt den Code wieder frei. Wer ein Handle-Rennen verliert,
verliert nicht auch noch seine Einladung.
Alle Fehlerfälle — fehlend, leer, unbekannt, gesperrt, aufgebraucht —
liefern dieselbe Meldung, damit der unauthentifizierte Endpoint kein
Orakel zum Abtasten des Code-Raums wird.
Codes erzeugt das Binary selbst, statt dafür einen Endpoint zu öffnen:
`pds-server invite create --count 5 --uses 1`, dazu list und disable.
PDS_INVITE_REQUIRED steht auf false per Default — sonst brechen die
Integrationstests und jede Dev-Instanz. Der Server warnt beim Start
deutlich, solange es aus ist, und describeServer meldet jetzt den
tatsächlichen Wert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013HC9HLrUU1LNwkzp8nkDLX
This commit is contained in:
co-authored by
Claude Opus 5
parent
b58cb75cfe
commit
f04d63dd7b
@@ -44,7 +44,32 @@ async fn describe_server() {
|
||||
let did = r["did"].as_str().expect("describeServer must return a did");
|
||||
assert!(did.starts_with("did:web:"), "did = {did}");
|
||||
assert!(r["available_user_domains"].is_array());
|
||||
assert_eq!(r["invite_code_required"], json!(false));
|
||||
// `invite_code_required` used to be a hardcoded `false` here. It is
|
||||
// now whatever `PDS_INVITE_REQUIRED` says, so this suite — which
|
||||
// talks to whatever PDS the developer happens to be running — can
|
||||
// only assert the type. That the value actually tracks the switch is
|
||||
// pinned in `invite_integration.rs`, which starts a PDS with the
|
||||
// flag set both ways and checks both answers.
|
||||
assert!(
|
||||
r["invite_code_required"].is_boolean(),
|
||||
"invite_code_required = {}",
|
||||
r["invite_code_required"]
|
||||
);
|
||||
// If this test process shares the server's environment (the
|
||||
// documented way to run the suite is
|
||||
// `set -a; . ./.env; set +a; cargo test`), hold it to the exact
|
||||
// value too.
|
||||
if let Ok(raw) = std::env::var("PDS_INVITE_REQUIRED") {
|
||||
let expected = matches!(
|
||||
raw.trim().to_ascii_lowercase().as_str(),
|
||||
"1" | "true" | "yes" | "on"
|
||||
);
|
||||
assert_eq!(
|
||||
r["invite_code_required"],
|
||||
json!(expected),
|
||||
"describeServer disagrees with PDS_INVITE_REQUIRED={raw}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `GET /.well-known/did.json` — the document the AppView fetches to
|
||||
|
||||
Reference in New Issue
Block a user