import type { AccessToken } from '../domain/access-token.js'; import type { FileSystem } from '../use-cases/ports/filesystem.js'; import type { Logger } from '../use-cases/ports/logger.js'; type BrowserTokenResult = { accessToken: AccessToken; refreshToken: string | null; }; /** * Discriminated outcome of an elevated-token capture attempt. Distinct * failure variants let the caller pick the right error message AND let * the auto-heal in auth.ts decide whether a retry is worthwhile (a * recoverable failure like `launch_timeout` or `sso_timeout` is worth * one retry after wiping the profile; `navigation_failed` is a network * issue, not worth retrying). * * Login-fix round-1: was previously `AccessToken | null`, which conflated * "browser launch hung", "navigation broke", and "silent-SSO polling * timed out" into a single null and made the error message inaccurate. */ type ElevatedFailureReason = 'launch_timeout' | 'navigation_failed' | 'sso_timeout'; type ElevatedTokenResult = { readonly ok: true; readonly token: AccessToken; } | { readonly ok: false; readonly reason: ElevatedFailureReason; }; /** * chatsvcagg capture result. Carries the same three failure modes as * `ElevatedTokenResult` PLUS the parsed regional segment from the URL * the bearer rode on (`emea` / `amer` / `apac` / etc.) — the new * substrate host (`teams.microsoft.com/api/csa//api/...`) routes * per region, so callers must persist this alongside the token to * construct future URLs. See `gotcha_chatsvcagg_substrate_moved` in * the project memory for the 2026-05 migration that made this * necessary. */ type ChatsvcaggTokenResult = { readonly ok: true; readonly token: AccessToken; readonly region: string; } | { readonly ok: false; readonly reason: ElevatedFailureReason; }; /** * IC3 capture result. The "next-gen" Teams chat-message substrate at * `teams.microsoft.com/api/chatsvc//v1/users/ME/conversations/{id}/messages` * — the path Teams web actually uses for chat scrollback (the existing * chatsvcagg substrate at `/api/csa//api/v1/chats/{id}/messages` * is the chat-list aggregator and caps at the 200 most recent messages * with no working pagination cursor). The IC3 path supports `syncState` * + `startTime` pagination, unlocking arbitrary-depth chat history. * * Bearer audience: `https://ic3.teams.office.com`. Standard `Bearer` * authorization (NOT the purple-teams-documented `skype_token X` * header — those forks predate Teams' move to OAuth bearer on this * surface). Empirically discovered 2026-05-21 by widening the * Playwright capture listener. */ type Ic3TokenResult = { readonly ok: true; readonly token: AccessToken; readonly region: string; } | { readonly ok: false; readonly reason: ElevatedFailureReason; }; /** * Combined outcome of capturing all four tokens (Teams basic / M365 * elevated / chatsvcagg substrate / IC3 substrate) inside one browser * session. Login-fix round-2 introduced this for the basic+elevated * pair; chatsvcagg was added next; IC3 is the most recent leg, capturing * the bearer Teams web uses for unbounded chat-history reads. * * All four legs ride on the SAME `teams.microsoft.com` session, so * adding capture legs is essentially free — MSAL multiplexes the * audience-scoped bearers via silent acquisition. */ type BothTokensResult = { readonly teams: BrowserTokenResult | null; readonly elevated: ElevatedTokenResult; readonly chatsvcagg: ChatsvcaggTokenResult; readonly ic3: Ic3TokenResult; }; type BrowserAuth = { acquireToken: (scopes: string[], startUrl: string) => Promise; /** * Capture an "elevated" Graph access token by navigating to a * different Microsoft web app whose first-party app identity is on * Microsoft's allow-list for ODSP `logicalPermissions` (the scope our * Teams web client token lacks for historical-version stream * content). Reuses the persistent profile cookies — no second * sign-in. Headless by default. * * Returns a discriminated union so the caller can distinguish the three * failure modes (browser-launch hang, navigation failure, silent-SSO * polling timeout). The caller decides whether the failure is fatal * (it isn't for most commands; only the version + chat commands need * this). * * Standalone fallback path: used by `getElevatedAccessToken` when the * cached elevated token expires and the Teams session is already in * the cache (no fresh sign-in needed). On `login`, use * `acquireBothTokens` instead so the user sees only one browser. */ acquireElevatedToken: () => Promise; /** * Capture a chatsvcagg-audience bearer (Teams basic identity, but the * `chatsvcagg.teams.microsoft.com` resource instead of Graph) by * navigating headless to `teams.microsoft.com/v2/`. The persistent * profile's SSO cookies do the auth silently. Used by * `getChatsvcaggAccessToken()` when the cached chatsvcagg token has * expired and the basic Teams session is still warm. * * Same failure-mode shape as `acquireElevatedToken` — launch hang, * navigation failure, silent SSO timeout — so the auth-manager can * reuse the same auto-heal logic. */ acquireChatsvcaggToken: () => Promise; /** * Capture an IC3-audience bearer (aud `https://ic3.teams.office.com`, * same basic Teams appid). The bearer Teams web client uses to call * `teams.microsoft.com/api/chatsvc//v1/users/ME/conversations/{id}/messages` * — the chat-message substrate with proper `syncState` pagination, * enabling reads beyond the 200-message chatsvcagg cap. Same failure * modes as `acquireChatsvcaggToken`. */ acquireIc3Token: () => Promise; /** * Login-fix round-2: capture BOTH tokens inside ONE browser session. * After the Teams response listener intercepts the Teams token, * navigate the SAME page to the elevated URL and harvest the * M365ChatClient bearer from outgoing request headers. Cookies are * live in memory, so federated SSO chains (e.g. Okta-fronted * tenants) work without a second visible sign-in. * * Returns `teams: null` if no Teams token came back within the full * `pollDeadlineMs` (5 min). Returns * `elevated: { ok: false, reason: ... }` if the elevated capture * failed inside the same session — caller decides whether to surface * the partial success. */ acquireBothTokens: (scopes: string[], teamsUrl: string) => Promise; close: () => Promise; }; type ResponseLike = { url(): string; headers(): Record; text(): Promise; }; type ResponseHandler = (response: ResponseLike) => void; type PageLike = { on(event: 'response', handler: ResponseHandler): void; on(event: 'request', handler: RequestHandler): void; goto(url: string, options: { waitUntil: 'domcontentloaded'; timeout: number; }): Promise; url(): string; evaluate(fn: () => void): Promise; close(): Promise; }; type ContextLike = { newPage(): Promise; clearCookies(): Promise; close(): Promise; }; type LaunchOptions = { headless: boolean; channel?: 'msedge' | 'chrome'; args: string[]; }; type RequestLike = { url(): string; headers(): Record; }; type RequestHandler = (request: RequestLike) => void; type BrowserAuthApi = { launchPersistentContext(profileDir: string, options: LaunchOptions): Promise; }; type ChromiumLike = { launchPersistentContext(profileDir: string, options: LaunchOptions): Promise; }; type PlaywrightLoader = () => Promise<{ readonly chromium: ChromiumLike; }>; type TraceFn = (message: string) => void; type BrowserAuthConfig = { readonly logger: Logger; readonly fs: FileSystem; readonly trace?: TraceFn; readonly profileDir?: string; readonly initialSettleMs?: number; readonly postReloginSettleMs?: number; readonly pollIntervalMs?: number; readonly pollDeadlineMs?: number; readonly navigationTimeoutMs?: number; /** * Deadline for the SILENT elevated-token recapture flow (no user * interaction expected — persistent profile cookies do the SSO). * Defaults to 20s. The audit (v1.0.0 §1.1) flagged that reusing the * 5-minute interactive `pollDeadlineMs` for this silent path made * `list-chats` etc. hang for minutes when cookies were stale, blowing * the LLM tool-call window. With a tight cap, the flow either yields * a token quickly or fails with `auth_failed: elevated token capture * timed out — run `ask-marcel login` to refresh.` */ readonly elevatedRecaptureTimeoutMs?: number; /** * Hard deadline on `launchPersistentContext` + `newPage` for the * elevated capture path. Defaults to 15s. Distinct from * `elevatedRecaptureTimeoutMs` so the error message can name which * step hung — launch vs polling. Audit login-fix round-1: previously * unguarded, so a hung Playwright launch (corrupt persistent profile * with stale `Singleton*` locks, or a slow browser binary) would * block the whole command indefinitely. */ readonly elevatedLaunchTimeoutMs?: number; }; declare const createPlaywrightApi: (loader: PlaywrightLoader) => BrowserAuthApi; declare const createBrowserAuthFromApi: (api: BrowserAuthApi, config: BrowserAuthConfig) => BrowserAuth; declare const createBrowserAuth: (deps: { logger: Logger; fs?: FileSystem; }) => BrowserAuth; export { createBrowserAuth, createBrowserAuthFromApi, createPlaywrightApi }; export type { BothTokensResult, BrowserAuth, BrowserAuthApi, BrowserAuthConfig, BrowserTokenResult, ChatsvcaggTokenResult, Ic3TokenResult, ChromiumLike, ContextLike, ElevatedFailureReason, ElevatedTokenResult, PageLike, PlaywrightLoader, RequestLike, ResponseLike, };