export type FeishuDomain = 'feishu' | 'lark'; /** Persisted user token bundle (epoch-ms expiries so freshness is a pure comparison). */ export interface UserToken { access_token: string; refresh_token: string; access_expires_at: number; refresh_expires_at: number; scope?: string; obtained_at: number; } /** Minimal fetch surface (injectable in tests). Mirrors the global fetch we actually use. */ export type FetchLike = (url: string, init: { method: string; headers: Record; body: string; }) => Promise<{ ok: boolean; status: number; json: () => Promise; }>; /** Thrown when there is no usable user token and the operator must (re-)run `cortex feishu login`. */ export declare class FeishuUserTokenError extends Error { constructor(message: string); } export interface OAuthHosts { /** Host that serves the user-facing authorization (consent) page. */ authorizeBase: string; /** Host that serves the OAuth token endpoint (code exchange + refresh). */ tokenBase: string; } /** * OAuth hosts per Feishu/Lark deployment. Feishu hosts are from the official OAuth v2 docs; * the larksuite.com equivalents mirror the same path layout (not separately documented). */ export declare function hostsFor(domain?: FeishuDomain): OAuthHosts; /** * Default user scopes requested at login when neither --scope nor FEISHU_USER_SCOPE is set. * Chosen to keep the WHOLE set review-free (免审) — so a developer can `cortex feishu login` and * click "开通并授权" without any scope tripping Feishu's review gate. Verified empirically: * - docx:document / sheets:spreadsheet / bitable:app / wiki:wiki — create/read/write content * (link-share via permissionPublic also works on these resource scopes alone). * - space:document:delete — whole-file delete (drive.v1.file.delete accepts this in place of * the broad, review-gated drive:drive). * Intentionally omitted: drive:drive (broad, review-gated) and drive:drive:readonly (review-gated; * there is no 免审 read:meta alternative). The only casualty is docx canonical-URL resolution via * meta.batchQuery — callers fall back to a constructed https://feishu.cn/docx/ link, which * still redirects to the doc for its owner. Add drive:drive:readonly via --scope if you accept a * review step and want the canonical tenant-subdomain URL. Unopened scopes are ignored at consent. */ export declare const DEFAULT_DOC_SCOPE = "docx:document sheets:spreadsheet bitable:app wiki:wiki space:document:delete im:resource"; /** Default location of the on-disk token store (alongside .env in CONFIG_DIR). */ export declare function userTokenPath(): string; /** Build the OAuth authorization URL the operator opens in a browser to grant access. */ export declare function buildAuthorizeUrl(opts: { appId: string; redirectUri: string; scope?: string; state?: string; domain?: FeishuDomain; }): string; /** Accept either a bare authorization code or a full callback URL; return the code or null. */ export declare function parseCodeFromInput(input: string): string | null; /** Exchange an authorization code for a user_access_token + refresh_token. */ export declare function exchangeCode(opts: { appId: string; appSecret: string; code: string; redirectUri?: string; domain?: FeishuDomain; fetchImpl?: FetchLike; now?: () => number; }): Promise; /** Response of the device-authorization request. */ export interface DeviceAuthorization { device_code: string; user_code: string; verification_uri: string; /** verification_uri with user_code pre-filled (falls back to verification_uri). */ verification_uri_complete: string; /** seconds until the device_code expires. */ expires_in: number; /** seconds the client must wait between token polls. */ interval: number; } /** * Request a device authorization code. The app (client_id/secret) authenticates via * HTTP Basic; the body carries client_id + scope (offline_access is always included). */ export declare function requestDeviceAuthorization(opts: { appId: string; appSecret: string; scope?: string; domain?: FeishuDomain; fetchImpl?: FetchLike; }): Promise; /** * Poll the token endpoint with the device_code until the user authorizes (success), * the code is denied/expires (throws), or the deadline passes (throws). Honours the * server-suggested interval and backs off on `slow_down`. */ export declare function pollDeviceToken(opts: { appId: string; appSecret: string; deviceCode: string; interval?: number; expiresIn?: number; domain?: FeishuDomain; fetchImpl?: FetchLike; now?: () => number; /** Injectable wait (defaults to setTimeout) — lets tests run the loop instantly. */ sleep?: (ms: number) => Promise; /** Called once per pending/slow_down poll (e.g. to print progress). */ onPending?: () => void; }): Promise; /** Refresh a user_access_token using the (rotating) refresh_token. */ export declare function refreshUserToken(opts: { appId: string; appSecret: string; refreshToken: string; domain?: FeishuDomain; fetchImpl?: FetchLike; now?: () => number; }): Promise; export declare function saveUserToken(tok: UserToken, file?: string): void; export declare function loadUserToken(file?: string): UserToken | null; export declare function clearUserToken(file?: string): boolean; /** * Return a valid user_access_token, refreshing (and persisting) it when near expiry. * Throws FeishuUserTokenError — never falls back to bot identity — when no token exists * or the refresh_token itself has expired. */ export declare function getValidUserAccessToken(opts: { appId: string; appSecret: string; domain?: FeishuDomain; file?: string; fetchImpl?: FetchLike; now?: () => number; bufferMs?: number; }): Promise;