/**
 * directory/directoryPasswords — the `directory-password` strategy's check:
 * a person types their Windows username and password, and Active Directory
 * says who they are (design §4.5).
 *
 * The steps, in order (the sign-in door around this already applied the door
 * guard, the JSON body, the minimum time and the attempt limits):
 *
 *  1. **An empty password is refused before the directory is touched.** RFC
 *     4513 §5.1.2: a simple bind with a name and an empty password is an
 *     UNAUTHENTICATED bind, which a directory may answer as a success.
 *  2. **The username is checked before the bind**: 1–64 characters of
 *     `A–Z a–z 0–9 . _ -`, after removing a trailing `@<domain>` or a leading
 *     `<NETBIOS>\` only when it is exactly the configured one (people paste
 *     their UPN, or type the down-level `CORP\alice`). Anything else — another
 *     `@domain` or `OTHER\`, `,`, `=`, parentheses, `*`, a space, a quote,
 *     letters outside ASCII (`o'brien`, `mary ann`, `josé` are legal Windows
 *     names this door does not take) — never reaches the directory. A password
 *     holding a control character never does either (Samba cuts a password at
 *     NUL — review idI57 N-7).
 *  3. **Bind as `<name>@<domain>`** with the typed password, over LDAPS only.
 *     No service account, so no stored directory secret.
 *  4. **Ask who it authenticated: RFC 4532 Who-am-I**, on the same connection.
 *     An empty answer is anonymous and refused (a second guard behind step 1).
 *  5. **Parse the answer**: `u:<NETBIOS>\<sAMAccountName>` (the domain must be
 *     the configured NetBIOS domain) or `u:<SID>`. Anything else is refused.
 *  6. **Find EXACTLY ONE entry** for that account under the base DN, by
 *     `sAMAccountName` or `objectSid`, every value escaped (RFC 4515). Zero or
 *     two is a refusal. The id is THAT entry's `objectGUID` (base64 of its 16
 *     bytes) — never re-derived from the typed text.
 *  7. **The required group**, when set: nested membership of that same DN
 *     (`LDAP_MATCHING_RULE_IN_CHAIN`), the group DN escaped as a filter value.
 *
 * ── Why Who-am-I (the rename collision, review B-3) ─────────────────────────
 * AD does not authenticate "the object whose name is the typed text". An
 * explicit userPrincipalName beats another object's implicit
 * `sAMAccountName@domain` (MS-ADTS "Simple Authentication"). If Jane's UPN is
 * `jsmith@corp` and a new John's sAMAccountName is `jsmith`, Jane typing
 * `jsmith` and HER password authenticates Jane — and a search for
 * `sAMAccountName=jsmith` finds John. Asking the directory which object it
 * authenticated is the only answer that cannot hand Jane John's conversations.
 *
 * **The attempt budget is kept per ACCOUNT** (`budgetKey`, review idI57 B-1):
 * AD folds case and binds `alice`, `CORP\alice` and `alice@corp` to one
 * object, so the door counts them under one key — lower-cased, prefix and
 * suffix removed, NFC.
 *
 * Every wrong credential — an unknown name, a wrong password, an expired or
 * must-change password (AD sub-codes 52e, 525, 530, 531, 532, 533, 701, 773,
 * 775), a name that fails the pattern, a missing group — is ONE answer
 * (`undefined`). A directory that cannot be reached throws
 * `PasswordCheckUnreachableError` (the door answers 503 and un-counts the
 * attempt); a failure after the bind was sent throws anything else (503, and
 * the attempt stays counted — AD may have counted it). The password is never
 * stored, logged or forwarded.
 *
 * **Why, for the operator:** each refusal is also NOTED to the sign-in door
 * (`PasswordChecker.check`'s `note`) as a reason class — `wrong-credential`
 * with AD's sub-code and its name (`52e` bad-password, `775` locked, `532`
 * password-expired, `773` must-change, `533` disabled, `701`
 * account-expired), `name-refused`, `account-unresolved`, `not-in-group` —
 * which the door writes to its audit record. The person's answer is the same.
 */
import type { PasswordChecker } from '../../../hosting/signin/types.js';
import { type Directory } from './port.js';
export interface DirectoryPasswordsOptions {
    /** Where connections come from — `ldapDirectory({ … })`, or a fake in tests. */
    readonly directory: Directory;
    /** The DNS domain people bind as `<name>@<domain>` (`corp.example`). */
    readonly domain: string;
    /** The NetBIOS domain Who-am-I must name (`CORP`). */
    readonly netbiosDomain: string;
    /** Where the account's entry is searched (`DC=corp,DC=example`). */
    readonly baseDn: string;
    /** Optional: the DN of a group the person must be in, directly or nested. */
    readonly requiredGroup?: string;
    /** Told the directory's refusal reason for an operator log — never the password. */
    readonly log?: (line: string) => void;
}
export declare function directoryPasswords(options: DirectoryPasswordsOptions): PasswordChecker;
/**
 * The account name to bind as, or `undefined` for a name that never reaches
 * the directory. A trailing `@<domain>` and a leading `<NETBIOS>\` are
 * removed only when they are exactly the configured ones.
 */
export declare function accountName(typed: string, domainSuffix: string, netbiosDomain?: string): string | undefined;
/**
 * The key a typed name's attempts are counted under — the ACCOUNT, as AD
 * resolves it: lower-cased (AD names are case-insensitive), ANY `@suffix` and
 * ANY `DOMAIN\` prefix removed, NFC. Deliberately wider than `accountName`: a
 * spelling that never binds still shares the account's budget, so no spelling
 * buys a fresh one (review idI57 B-1).
 */
export declare function accountBudgetKey(typed: string): string;
/**
 * Who-am-I's answer → the filter for exactly that account, or `undefined`.
 * `u:CORP\alice` → `(sAMAccountName=alice)` (the domain must be ours);
 * `u:S-1-5-21-…` → `(objectSid=<bytes>)`; anything else (empty = anonymous,
 * `dn:` = AD LDS) is refused.
 */
export declare function accountFilter(authzId: string, netbiosDomain: string): string | undefined;
