/** * Gmail and Google Calendar over their REST APIs. * * Endpoints and parameter names here were read from Google's live API * reference on 2026-07-26 rather than recalled: * - GET https://gmail.googleapis.com/gmail/v1/users/{userId}/messages * (q, maxResults, pageToken, labelIds, includeSpamTrash) * - POST https://gmail.googleapis.com/gmail/v1/users/{userId}/messages/send * - GET https://www.googleapis.com/calendar/v3/calendars/{calendarId}/events * (timeMin, timeMax, singleEvents, orderBy, maxResults) * * Two properties this module is responsible for: * * 1. **No token ever escapes.** Access tokens go into an Authorization * header and nowhere else. Every error is built from status and Google's * own error message, with the token scrubbed. * * 2. **Mail content is marked untrusted at the boundary.** Message bodies * are attacker-controlled: anyone who knows the address can put text in * front of the agent. Results carry an explicit provenance marker so the * caller cannot accidentally treat a body as instructions. The * surface-authority layer enforces what may then be done with it. */ import type { GoogleTokenManager } from './token-manager.js'; import { type HistoryDeltaDeps, type HistoryDeltaOptions, type HistoryListDeltaResult } from './history-delta.js'; /** Injected HTTP, so every call is testable without network. */ export interface GoogleApiFetchPort { fetch(url: string, init: RequestInit): Promise; } /** A failure the caller can act on, with no secret material in it. */ export interface GoogleApiFailure { readonly ok: false; readonly status: number | null; /** * Set when the failure is the token manager's dead-grant verdict: the * refresh token is expired, revoked or otherwise invalid, and only a person * re-authorizing can fix it. Carried as a field because the verdict is * structured at its source (`GoogleRefreshFailure`), and a downstream mapper * that had only `problem` prose to look at was left guessing from text. */ readonly reason?: 'grant-invalid' | undefined; readonly problem: string; readonly fix: string; } export type GoogleApiResult = { readonly ok: true; readonly value: T; } | GoogleApiFailure; /** * Provenance marker carried by every piece of mail-derived content. * `'untrusted-external'` means: evidence about the world, never instructions. */ export declare const MAIL_CONTENT_PROVENANCE: "untrusted-external"; export interface GmailMessageSummary { readonly id: string; readonly threadId: string; readonly from: string; readonly to: string; readonly subject: string; readonly date: string; readonly snippet: string; readonly unread: boolean; /** Always `'untrusted-external'`. Present so it cannot be forgotten downstream. */ readonly provenance: typeof MAIL_CONTENT_PROVENANCE; } /** * A message read under `format=METADATA`: headers and delivery evidence, and * **no body of any kind**. * * Endpoint and parameters read from Google's live reference on 2026-07-28, not * recalled: * https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.messages/get * * - `GET https://gmail.googleapis.com/gmail/v1/users/{userId}/messages/{id}` * with query parameters `format` (enum) and `metadataHeaders[]` (string), * whose description is verbatim: "When given and format is `METADATA`, * only include headers specified." * - Authorization scopes for the method, verbatim and in full: * `https://mail.google.com/`, `.../auth/gmail.modify`, * `.../auth/gmail.readonly`, `.../auth/gmail.metadata`. A `gmail.metadata` * token is therefore authorized to make this call, which is the entire * reason this type exists. * - `Message.historyId` is declared **string**, and `internalDate` is a * string in int64 format. Neither is ever parsed to a `Number` here: a * decimal uint64 above 2^53 truncates, and a truncated history position is * a cursor that looks valid and names the wrong record. * * Why this is a SEPARATE type from `GmailMessageBody` rather than one with an * empty `body` * ───────────────────────────────────────────────────────────────────────── * `GmailMessageBody` extends this and adds `body`, so the assignability runs * one way only: a body-bearing message satisfies a metadata-shaped parameter, * and a metadata-only message does NOT satisfy a body-bearing one. Anything * that needs a body, matching a verification link, redacting a body excerpt, * takes `GmailMessageBody` and cannot be handed one of these by accident. * * `snippet` is deliberately EMPTY on this path, and that is not cosmetic. A * snippet is derived from the message body: Gmail should not return one to a * `gmail.metadata` token at all, but "should not" is the provider's promise * rather than this daemon's guarantee, and a body excerpt arriving through the * one path built to carry no body is exactly the shape nobody would look for. * `readMessageMetadata` blanks it unconditionally, so the property holds * whatever Google sends. */ export interface GmailMessageMetadata extends GmailMessageSummary { /** * Every `Delivered-To` / `X-Original-To` header on the message, in order. * * These are written by the receiving infrastructure, not the sender, which is * what makes them usable as proof of which address a message actually * arrived at. `To:` is sender-controlled and is deliberately kept separate. */ readonly deliveredTo: readonly string[]; } export interface GmailMessageBody extends GmailMessageMetadata { readonly body: string; } /** * The mailbox itself, as `users.getProfile` describes it. * * Endpoint read from Google's live reference on 2026-07-28, not recalled: * https://developers.google.com/workspace/gmail/api/reference/rest/v1/users/getProfile * * - `GET https://gmail.googleapis.com/gmail/v1/users/{userId}/profile`, where * `userId` takes "The user's email address. The special value `me` can be * used to indicate the authenticated user.", verbatim. * - Response body: `emailAddress` (string), `messagesTotal` (integer), * `threadsTotal` (integer), and `historyId` (string), whose description * reads **"The ID of the mailbox's current history record"**, verbatim. * - Authorization scopes, verbatim and in full: `https://mail.google.com/`, * `.../auth/gmail.modify`, `.../auth/gmail.compose`, * `.../auth/gmail.readonly`, `.../auth/gmail.metadata`. Every scope that * authorizes `users.history.list` therefore also authorizes this call, so a * credential that can read a delta can always establish a position. * * `messagesTotal` and `threadsTotal` are carried because they are what the * response contains, and dropping them would make this a partial mapping that * the next caller has to widen. Nothing here is a secret: the address is the * one the owner connected, and the counts are two integers. */ export interface GmailProfile { readonly emailAddress: string; readonly messagesTotal: number; readonly threadsTotal: number; /** Decimal uint64 as a STRING. Never parsed to a number, it does not fit one. */ readonly historyId: string; } export interface CalendarEventRecord { readonly id: string; readonly summary: string; readonly start: string; readonly end: string; readonly allDay: boolean; readonly location: string; readonly description: string; readonly htmlLink: string; /** * The organizer's address as Google reported it, absent when Google named * none. * * Claimed, never verified, the same standing as a `From:` header. Carried so * a read of this event can be recorded against the party who wrote its text * rather than against "the calendar"; it is not part of any gateway response. * * Optional because this interface is a CONTRACT a caller may implement, not * only a shape this client produces. A required field would have broken every * such implementation, and the reading side already treats "said nothing" * correctly: absent means nobody was named, which is not a claim of * ownership. */ readonly organizer?: string; /** * Google Calendar API v3, Events resource, `organizer.self`: "Whether the * organizer corresponds to the calendar on which this copy of the event * appears. Read-only. The default is False." * * `true` means the owner organized it, so it is not externally sourced. * Anything else, `false`, or absent because Google or an implementer said * nothing, reads as somebody else's, which is the direction that fails * towards recording rather than towards silence. */ readonly organizerIsSelf?: boolean; } export interface SendMailInput { readonly to: string; readonly subject: string; readonly body: string; readonly from?: string; } export interface CreateEventInput { readonly summary: string; readonly start: string; readonly end: string; readonly location?: string; readonly description?: string; readonly calendarId?: string; } export declare class GoogleApiClient { private readonly tokens; private readonly fetchPort; constructor(tokens: GoogleTokenManager, fetchPort: GoogleApiFetchPort); /** * Authorized request. Refreshes once on a 401, a token can expire between * the expiry check and the call landing, and one silent retry is the * difference between a working tool and a flaky one. */ private request; private describeHttpFailure; /** List message summaries. `query` uses Gmail search syntax (e.g. "is:unread"). */ listMessages(options?: { query?: string; maxResults?: number; }): Promise>; /** Read one message including its plain-text body. */ getMessage(id: string): Promise>; /** * Read one message's HEADERS AND DELIVERY EVIDENCE, with no body. * * `GET .../messages/{id}?format=metadata&metadataHeaders=...`, the request a * `gmail.metadata` token is authorized to make (see `GmailMessageMetadata` * for the live-docs citation). This is the call that gives * `surfaces.email.inbound.onInsufficientCapability: 'notice-only'` something * to actually do: a grant that excludes bodies can still say who sent what, * to which address, and when it landed. * * Deliberately NOT a `format` parameter on `getMessage`. The two calls return * different guarantees and the return TYPE is what carries the difference, * a caller that needs a body gets a compile error rather than a `body` field * that is empty for a reason it cannot see. That is also why this does not * reuse `fetchMessage`: that method's `metadata` overload feeds * `listMessages`, whose summaries keep Gmail's `snippet`, and this path must * blank it. */ readMessageMetadata(id: string): Promise>; private fetchMessage; /** * Incremental sync via `users.history.list`, what changed since * `options.startHistoryId`, without re-listing the mailbox. * * Gated on the token's actual granted scopes, checked at call time via * `this.tokens.scopes()` rather than assumed from what setup requested. * With no Gmail read scope present this returns * `unavailable: 'no-gmail-scope'`, never an empty success. A `startHistoryId` * that has aged out of Gmail's retention window returns * `unavailable: 'resync-required'` instead of an empty delta. See * `history-delta.ts` for the full design rationale and the live-docs * citations it is built against. */ historyListDelta(options: HistoryDeltaOptions): Promise; /** * The narrow I/O slice `collectHistoryDelta` takes, over this client. * * Exposed because a long-lived caller, `GmailMailSource`, drives the delta * itself rather than through `historyListDelta`: it has to inspect * `unreadable` before it may move its cursor, and it re-enters on its own * poll interval. Handing it THIS object rather than letting it build a second * one is the point. `request` is private, so a hand-built port would need its * own fetch, its own Authorization header and its own 401-retry, and the * scope gate would then read a different token manager's answer than the one * actually making the calls. */ historyDeltaPort(): HistoryDeltaDeps; /** The mailbox's address, size and current history position. See `GmailProfile`. */ getProfile(): Promise>; /** * The mailbox's current `historyId`, for a caller establishing a position. * * This is the call `GmailMailSource.currentHistoryId` needed and had nowhere * to get: its own comment recorded that "`GoogleApiClient` exposes neither a * profile call nor a `historyId` today", which is what left the Gmail source * unbuildable in any composition. * * Google's sync guide names `messages.get` / `messages.list` as where a * client reads a message's `historyId` for a full sync * (https://developers.google.com/workspace/gmail/api/guides/sync, verified * live 2026-07-28: "To retrieve the historyId of a recent message, use the * messages.get or messages.list methods"). `users.getProfile` is used here * instead, deliberately: it answers "the ID of the mailbox's current history * record" in one request that lists nothing, which is exactly what * establishing-without-backfilling means. Reading it off the newest message * would name a position BELOW that message, and re-announcing mail that was * already in the mailbox is precisely what the establish path refuses to do. */ currentHistoryId(): Promise>; /** Send a plain-text message. */ sendMessage(input: SendMailInput): Promise>; listEvents(options?: { calendarId?: string; timeMin?: string; timeMax?: string; maxResults?: number; }): Promise>; getEvent(id: string, calendarId?: string): Promise>; createEvent(input: CreateEventInput): Promise>; } //# sourceMappingURL=api-client.d.ts.map