fix: apply the review items for #684 at merge

Multi-user: add a (username, IP) failure bucket checked before verifyPassword, capped at AUTH_FAILURE_MAX, with the same decay as the other buckets; the per-IP pre-gate stays at AUTH_IP_FAILURE_MAX and the per-username bucket stays after the verify
A correct password now also clears that account's (username, IP) bucket, beside the IP and per-username buckets
The new bucket is disposed on server stop like the others; the comments above the gate and the constants' JSDoc describe the three buckets
Tests: 10 wrong then the correct password from one IP is a 429, the same account logs in from another IP, another account behind that IP is unaffected, and a correct password clears the pair bucket
test/multiuser-auth.test.ts: the spray test loops to AUTH_IP_FAILURE_MAX instead of a literal 50
Reworded the rate-limit lines in auth-config.ts, security-architecture.md, README.md, README.zh-CN.md, wiki/Security.md, endpoints.md (plus the plugin mirror), CLAUDE.md and architecture-invariants.md
docs/wiki/Remote-Access.md: add the multi-user note to the tunnel page

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-24 14:15:21 +02:00
1 parent 175e3b8320
commit b7897b6d0e
13 files changed
+173 -49

No files matched your search

+6
View File
@@ -457,6 +457,7 @@ export class WebServer extends EventEmitter {
private qrAuthFailures: StaleExpirationMap<string, number> | null = null;
private hookSecretFailures: StaleExpirationMap<string, number> | null = null;
private userFailures: StaleExpirationMap<string, number> | null = null;
private userIpFailures: StaleExpirationMap<string, number> | null = null;
private pushStore: PushSubscriptionStore = new PushSubscriptionStore();
/** ntfy / Slack / Discord / generic webhook for the push events; config in webhook.json (0600). */
private webhookNotifier = new WebhookNotifier(() => readWebhookConfig(getDataDir()), webviewFetch);
@@ -959,6 +960,7 @@ export class WebServer extends EventEmitter {
this.qrAuthFailures = authState.qrAuthFailures;
this.hookSecretFailures = authState.hookSecretFailures;
this.userFailures = authState.userFailures;
this.userIpFailures = authState.userIpFailures;
}
// WebSocket support (terminal I/O — low-latency bidirectional channel)
@@ -4324,6 +4326,10 @@ export class WebServer extends EventEmitter {
this.userFailures.dispose();
this.userFailures = null;
}
if (this.userIpFailures) {
this.userIpFailures.dispose();
this.userIpFailures = null;
}
this.activePlanOrchestrators.clear();
this.cleaningUp.clear();
this.killingSessions.clear();