/** * Typed taxonomy of auth-flow failure reasons. * * Every value is a sanitized enum literal — never a raw error message, * never a truncated user input, never any byte from Cognito's response. * Adding a new failure mode means extending this union; the compiler * then forces every emit site to either use a known value or produce * a type error. * * Keep values snake_case to match GA4 custom-dimension conventions * used elsewhere in the codebase (`step`, `source`, `reason`). */ export type AuthFailureReason = 'port_in_use' | 'port_range_exhausted' | 'server_listen_failed' | 'browser_open_failed' | 'login_timeout' | 'cognito_rejected' | 'state_mismatch' | 'no_authorization_code' | 'token_exchange_failed' | 'token_exchange_network_error' | 'keychain_write_failed' | 'user_aborted' | 'unknown'; /** * Auth flow stage the failure occurred in. Useful for GA4 faceting; * each reason has an implicit stage but keeping it as a distinct * param lets dashboards split `login_timeout` at `awaiting_callback` * from `login_timeout` at `storing_tokens` (which would be a bug, * not user AFK). */ export type AuthStage = 'server_start' | 'browser_open' | 'awaiting_callback' | 'exchanging_code' | 'storing_tokens' | 'unknown'; /** * Fire the `auth_completed` success beacon. Replaces the inline POST * previously duplicated in `auth-cli.ts`. Includes `user_id` so the * event can be joined against DynamoDB activity for funnel analysis. * * `userId` here is the Cognito `sub` — a pseudonymous identifier that * we already ship to GA4 on this event today. It is NOT an email * address. */ export declare function fireAuthCompletedBeacon(userId: string): Promise; /** * Fire the `auth_failed` failure beacon with a sanitized `reason` * literal. Optional `httpStatus` captures the numeric HTTP status * code from Cognito on `token_exchange_failed` (so dashboards can * split "401 bad client" from "500 Cognito down" from "429 rate * limited"). Optional `stage` overrides the default from * `DEFAULT_STAGE`. * * `reason` is constrained to the `AuthFailureReason` union — this * is the ONLY input path; passing a raw error message is a compile * error. * * Optional `errorFragment` is a diagnostic dimension reserved for * `reason: 'unknown'`. The outer `auth-cli` catch passes the first * portion of `error.message` here so the next analytics pass can * see what's hiding in `unknown` and we can ship a typed reason in * a follow-up. Sanitized inside: ANSI escape sequences stripped, * newlines/tabs/quotes/backslashes replaced with spaces, non-ASCII * dropped, then split into two GA4 dimensions to work around the * Measurement Protocol 100-char per-param limit: * - `error_fragment` = bytes 0-99 * - `error_fragment_2` = bytes 100-199 (only set when input >100 chars) * Reports concatenate both dims for display. Mirrors the install.sh * `sanitize_fragment` / `sanitize_fragment_tail` shell helpers so * fragments fired from either path follow the same redaction + * slicing contract. */ export declare function fireAuthFailedBeacon(reason: AuthFailureReason, extra?: { httpStatus?: number; stage?: AuthStage; errorFragment?: string; }): Promise; /** * Coarse reason a machine fell back to file-based credential storage. * - `no_keyring_service`: keytar's native addon LOADED but there's no OS * keyring service to talk to (structurally-headless Linux — no D-Bus session). * - `keytar_load_failed`: keytar's native addon failed to load AND the machine * is structurally headless (libsecret absent on a no-keyring box). */ export type KeychainFallbackReason = 'no_keyring_service' | 'keytar_load_failed'; /** * Fire the `keychain_file_fallback` beacon when keychain-backend auto-selects * the file backend on a structurally-headless machine. Deliberately a DISTINCT * event from `auth_failed` — this is a successful, working fallback, not a * failure. Lets analytics see the headless cohort and confirm the headless fix * is reaching the field (the old `keychain_write_failed` signal goes quiet for * these users, so we need a positive beacon, not just an absence). No PII: only * platform / source / the coarse reason literal. */ export declare function fireKeychainFileFallbackBeacon(reason: KeychainFallbackReason): Promise; /** * Attach the reason + beaconed marker to an Error. Non-enumerable so * the symbols don't serialize if the error object is ever JSON'd. */ export declare function markErrorBeaconed(err: Error, reason: AuthFailureReason): Error; /** Has this error already had a beacon fired for it? */ export declare function errorWasBeaconed(err: unknown): boolean; /** Retrieve the tagged reason from a previously-marked error, if any. */ export declare function getErrorReason(err: unknown): AuthFailureReason | undefined; export type DeviceCountBucket = '0' | '1' | '2-5' | '6+'; /** * Bucket a raw device count into a coarse range to prevent device-count * fingerprinting via GA4 client_id stickiness. The `'0'` bucket exists as * a safe default for callers that may pass 0; no current emitter actually * fires with count 0 in production paths. */ export declare function bucketDeviceCount(count: number): DeviceCountBucket; /** * SHA-256-truncate a sessionId for cross-beacon correlation without leaking * the raw UUID. 8 hex chars = 32 bits ⇒ no practical reverse lookup absent * a GA4 + DDB join. */ export declare function hashSessionId(sessionId: string): string; /** * Bug B per-device: fired inside `createSessionKey`'s per-device catch. * `session_hash` is optional because keychain-manager intentionally does * not know which session is being keyed — the per-session aggregate in * `fireSessionEncryptionPartialSuccessBeacon` provides the correlation. */ export declare function fireSessionEncryptionDeviceSkippedBeacon(params: { skipped_count_bucket: DeviceCountBucket; session_hash?: string; }): Promise; /** * Bug B per-session: fired once per session creation when at least one * device was skipped. Caller MUST gate on `skippedDeviceIds.length > 0`. */ export declare function fireSessionEncryptionPartialSuccessBeacon(params: { session_hash: string; encrypted_count_bucket: DeviceCountBucket; skipped_count_bucket: DeviceCountBucket; }): Promise; /** * Bug A2: fired when `rekeySessionForNewDevices` grants ≥1 device on resume. * Caller MUST gate on `grantedCount > 0`. */ export declare function fireSessionEncryptionCatchUpGrantBeacon(params: { session_hash: string; granted_count_bucket: DeviceCountBucket; }): Promise; /** Bug A1: fired when we enter the self-rekey path (our deviceId missing). */ export declare function fireSessionEncryptionSelfRekeyRequestBeacon(params: { session_hash: string; other_device_count_bucket: DeviceCountBucket; }): Promise; /** Bug A1: fired when polling resolves with our entry + decrypt succeeds. */ export declare function fireSessionEncryptionSelfRekeySuccessBeacon(params: { session_hash: string; attempt_count: number; }): Promise; /** Bug A1: fired when polling exhausts maxAttempts without our entry. */ export declare function fireSessionEncryptionSelfRekeyTimeoutBeacon(params: { session_hash: string; attempt_count: number; }): Promise;