d59e76dd9d
The #733 miss budget (MissBurst / MissRefillPerSecond) was process-wide, because the impersonation seam did not carry the client's address, so one noisy client could drain it for everybody while the known-id set was unavailable. - IOpcUaUserAuthenticator gains AuthenticateUserNameAsync(username, password, IPEndPoint? remoteEndpoint, ct) (default: ignore it and delegate); the server calls it. IApiKeyStore gains the matching FindAsync(..., IPAddress?, ...). - The SDK (1.5.378) exposes no client address publicly. SecureChannelRemoteEndpoint reads it from the TCP listener's private channel map by reflection (through the protected TransportListeners, exposed by OtOpcUaSdkServer), keyed by the session's secure channel id. Fail-soft: null => process-wide accounting only. - BoundedApiKeyStore charges a miss to the client's own bucket (keyed by IP address) and then to the process-wide one, which stays as the backstop. A refusal by either takes from neither; a hit refunds both. New options Security:ApiKeys:MissBurstPerEndpoint (10) / MissRefillPerSecondPerEndpoint (2), 0 disables; telemetry outcome endpoint_miss_budget. At most 4096 addresses are tracked; idle buckets are pruned and an address beyond the cap is charged process-wide only. Tests: BoundedApiKeyStorePerEndpointTests (noisy client vs quiet one, process-wide backstop, refund + refill, off switch, authenticator forwards the address) and over the wire ApiKeyMissBudgetPerEndpointTests (activation hands the authenticator the loopback endpoint; real authenticator + bounded store refuse a client's 3rd and 4th unknown key without a store read). Each new behaviour test fails with its fix mutated out. Host.Tests 224/0, OpcUaServer.Tests 1019/1 (the one failure is the wall-clock NodeManagerLockTimeBenchmarkTests under suite load; passes alone). Docs: security.md. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV