/** * Microsoft Graph client wrapper. * * Builds a `Client` from `@microsoft/microsoft-graph-client` whose auth provider * pulls from the per-account TokenStore, with auto-refresh when the access * token is within the refresh threshold of expiry. * * Centralises error classification — every Graph error decision keys off the * status code and the structured `error.code` field, never the prose body. * Aligns with `feedback_no_stdout_parsing_for_control_flow.md`. */ import { Client } from "@microsoft/microsoft-graph-client"; import type { TokenStore } from "../auth/token-store.js"; export interface GraphClientConfig { clientId: string; tenantId: string; accountId: string; tokenStore: TokenStore; } /** * Options shared by every Graph call helper. * * `tool` names the caller, and drives the scope-insufficient and rate-limit * messages. `target` is the specific resource path an id-addressed call acts on: * without it a `graph-error` records that *something* 404'd but not *what*, * which is why identifying the dead draft in Task 1688 required the session * JSONL rather than the log. Collection queries address no single item and * correctly leave it unset. */ export interface GraphCallOpts { tool: string; target?: string; } export type GraphErrorClass = { kind: "auth"; statusCode: number; code: string; } | { kind: "rate-limit-recoverable"; statusCode: number; retryAfterMs: number; } | { kind: "rate-limit-terminal"; statusCode: number; } | { kind: "on-prem"; statusCode: number; mailServer?: string; } | { kind: "scope-insufficient"; statusCode: number; code: string; } | { kind: "5xx"; statusCode: number; code: string; } | { kind: "other"; statusCode: number; code: string; }; export declare function classifyGraphError(err: unknown): GraphErrorClass; /** * Opt into Graph immutable item ids. * * Outlook item ids are NOT stable: Graph rotates an item's id when the item is * moved or re-saved — including by the operator's own mail client. That is what * killed a live draft handle in Task 1688: our PATCH returned 200, and four * minutes later the same id returned 404 ErrorItemNotFound, so the agent * concluded the draft had vanished and rebuilt it. An immutable id survives that * event; a regular id does not. * * This rides on the one client factory because it is plugin-wide by necessity, * not preference: the immutable and default namespaces carry DIFFERENT values * for the same item, so a per-tool opt-in would have one tool emitting ids that * another cannot address. Never set this per-tool. * * TRAP for whoever adds the next header: `Prefer` is also how Graph carries * `outlook.timezone` and `odata.maxpagesize`. A per-request * `.header("Prefer", ...)` REPLACES this one for that call, silently dropping * that tool back to regular ids — the mixed-namespace break, invisible until an * id passed between tools 404s. `Prefer` is a comma-separated multi-value * header: append to this value, never set `Prefer` on its own. */ export declare const IMMUTABLE_ID_PREFER = "IdType=\"ImmutableId\""; /** * Factory wrapping `Client.init`. Exported as a named function so tests can * `vi.spyOn(graphClient, "createGraphClient")` reliably (avoids the * destructured-ESM mock no-op pitfall). */ export declare function createGraphClient(config: GraphClientConfig): Client; /** * Return a usable access token, refreshing via the stored refresh token when * needed. With `forceRefresh`, skip the local `needsRefresh()` fast-path and * always mint a new access token — used only by `callGraph`'s 401 retry, where * Graph rejected a token that is still locally un-expired (server-side * revocation), so the cached token must not be reused. The terminal guards * below (no blob, expired/absent refresh token) run regardless of `forceRefresh`. */ export declare function getOrRefreshAccessToken(config: GraphClientConfig, forceRefresh?: boolean): Promise; /** * Wait for the peer holding the refresh lock to publish a new access token. * * Keys on the access token *changing*, not on `needsRefresh()` going false. * Under `forceRefresh` — the 401 retry after a server-side revocation — the * stored token is locally un-expired, so a freshness check would hand back the * very token Graph just rejected. Task 1480 exists because that short-circuit * burned the 401 retry once already. * * Never refreshes on its own. Issuing a second refresh is the exact race the * lock exists to remove. */ declare function waitForPeerRefresh(config: GraphClientConfig, previousAccessToken: string, waitMs: number, pollMs: number): Promise; /** * Run a Graph call with classification, single retry on `auth` (refresh + * retry once), and respect for 429 Retry-After. All terminal classes throw * with operator-readable messages. */ export declare function callGraph(config: GraphClientConfig, invoke: (client: Client) => Promise, opts?: GraphCallOpts): Promise; /** A single page of a Graph collection: the items plus the opaque continuation. */ export interface GraphPage { value: T[]; /** The server's `@odata.nextLink` (a full URL carrying `$skiptoken`), or null when the collection is exhausted. */ nextLink: string | null; } /** * Run a paged Graph GET and surface `@odata.nextLink` as an opaque cursor. * `buildRequest` returns the prepared request chain (`.api(path).filter(...).top(...)`), * or `client.api(cursorUrl)` when continuing a prior page — the nextLink URL already * encodes filter/orderby/top/skiptoken, so nothing is re-derived. Retry/429/auth * classification is inherited from `callGraph`; a rejected `$skiptoken` or `$filter` * throws — it never silently falls back to page one. */ export declare function callGraphPaged(config: GraphClientConfig, buildRequest: (client: Client) => { get(): Promise; }, opts?: GraphCallOpts): Promise>; /** POST to Graph. See `writeGraph`. */ export declare function postGraph(config: GraphClientConfig, path: string, body: object | undefined, opts?: GraphCallOpts): Promise<{ status: number; json: unknown | null; }>; /** PATCH to Graph. See `writeGraph`. */ export declare function patchGraph(config: GraphClientConfig, path: string, body: object | undefined, opts?: GraphCallOpts): Promise<{ status: number; json: unknown | null; }>; /** DELETE to Graph. See `writeGraph`. */ export declare function deleteGraph(config: GraphClientConfig, path: string, opts?: GraphCallOpts): Promise<{ status: number; json: unknown | null; }>; /** Exported for tests only. */ export declare const _internals: { waitForPeerRefresh: typeof waitForPeerRefresh; }; export {}; //# sourceMappingURL=graph-client.d.ts.map