mirror of
https://github.com/Sea-Haven-Industries/sh-mcp.git
synced 2026-10-07 16:18:58 +00:00
140 lines
5.1 KiB
TypeScript
140 lines
5.1 KiB
TypeScript
|
|
/**
|
|||
|
|
* Gmail client interface + thin implementation.
|
|||
|
|
*
|
|||
|
|
* The real Google API call is clearly stubbed/guarded behind the interface so
|
|||
|
|
* that nothing in this module imports the Google client SDK at module load time.
|
|||
|
|
* All consumers (tools.ts and tests) inject a GmailClient — no live network
|
|||
|
|
* traffic ever occurs during import or unit tests.
|
|||
|
|
*
|
|||
|
|
* Per §2.4 of the design:
|
|||
|
|
* - This module acts AS the signed-in user via a per-user Google OAuth token.
|
|||
|
|
* - The GoogleTokenProvider is injected; the implementation fetches a
|
|||
|
|
* short-lived access token on demand and never caches it.
|
|||
|
|
* - The refresh token is NEVER returned or logged.
|
|||
|
|
* - The client is instantiated with the minimal scope for the requested tool
|
|||
|
|
* only (gmail.readonly) — never the union of a user's scopes.
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
export interface EmailMessage {
|
|||
|
|
id: string;
|
|||
|
|
threadId: string;
|
|||
|
|
subject: string;
|
|||
|
|
from: string;
|
|||
|
|
to: string;
|
|||
|
|
date: string;
|
|||
|
|
/** Plain-text snippet — safe for log output. */
|
|||
|
|
snippet: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface EmailThread {
|
|||
|
|
threadId: string;
|
|||
|
|
subject: string;
|
|||
|
|
messages: Array<{
|
|||
|
|
id: string;
|
|||
|
|
from: string;
|
|||
|
|
to: string;
|
|||
|
|
date: string;
|
|||
|
|
/** Full decoded plain-text body. */
|
|||
|
|
body: string;
|
|||
|
|
}>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface SearchInboxParams {
|
|||
|
|
/** Gmail query string, e.g. "from:vendor@example.com subject:invoice" */
|
|||
|
|
query: string;
|
|||
|
|
/** Maximum number of results to return (1–50). */
|
|||
|
|
maxResults: number;
|
|||
|
|
/** Sub of the calling user — used to look up the per-user Google token. */
|
|||
|
|
userSub: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface GetThreadDetailParams {
|
|||
|
|
threadId: string;
|
|||
|
|
userSub: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* The interface all callers (tools + tests) program against.
|
|||
|
|
* Tests inject a mock; production code injects GmailApiClient.
|
|||
|
|
*/
|
|||
|
|
export interface GmailClient {
|
|||
|
|
searchInbox(params: SearchInboxParams): Promise<EmailMessage[]>;
|
|||
|
|
getThreadDetail(params: GetThreadDetailParams): Promise<EmailThread>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Provides a short-lived Google access token for a given user sub.
|
|||
|
|
* The real implementation reads the encrypted refresh token from DynamoDB
|
|||
|
|
* (KMS-CMK, ABAC-partitioned by sub) and exchanges it for an access token.
|
|||
|
|
* Tests inject a mock that returns a hard-coded dummy token.
|
|||
|
|
*
|
|||
|
|
* IMPORTANT: The refresh token MUST NOT be returned or surfaced anywhere
|
|||
|
|
* outside this provider. Access tokens are minted per request; do not cache.
|
|||
|
|
*/
|
|||
|
|
export interface GoogleTokenProvider {
|
|||
|
|
getAccessToken(userSub: string): Promise<string>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Production Gmail client.
|
|||
|
|
*
|
|||
|
|
* The actual HTTP call to the Gmail REST API is guarded behind the
|
|||
|
|
* STUB comment below. To complete the real implementation:
|
|||
|
|
* 1. npm install googleapis (or use undici/fetch directly)
|
|||
|
|
* 2. Build a google.auth.OAuth2 client from the access token returned
|
|||
|
|
* by tokenProvider.getAccessToken()
|
|||
|
|
* 3. Call gmail.users.messages.list / gmail.users.threads.get
|
|||
|
|
*
|
|||
|
|
* The stub throws so that accidental real calls fail loudly in tests.
|
|||
|
|
*/
|
|||
|
|
export class GmailApiClient implements GmailClient {
|
|||
|
|
// Stored for use by the real implementation once the Gmail SDK call is wired.
|
|||
|
|
private readonly _tokenProvider: GoogleTokenProvider;
|
|||
|
|
|
|||
|
|
constructor(tokenProvider: GoogleTokenProvider) {
|
|||
|
|
this._tokenProvider = tokenProvider;
|
|||
|
|
// Mark as intentionally stored-but-unused until the real SDK call is wired.
|
|||
|
|
void this._tokenProvider;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async searchInbox(params: SearchInboxParams): Promise<EmailMessage[]> {
|
|||
|
|
// TODO: Replace this stub with the real Gmail API call.
|
|||
|
|
// When implementing, fetch a short-lived access token first:
|
|||
|
|
// const accessToken = await this.tokenProvider.getAccessToken(params.userSub);
|
|||
|
|
// Example (googleapis):
|
|||
|
|
// const auth = new google.auth.OAuth2();
|
|||
|
|
// auth.setCredentials({ access_token: accessToken });
|
|||
|
|
// const gmail = google.gmail({ version: 'v1', auth });
|
|||
|
|
// const res = await gmail.users.messages.list({
|
|||
|
|
// userId: 'me',
|
|||
|
|
// q: params.query,
|
|||
|
|
// maxResults: params.maxResults,
|
|||
|
|
// });
|
|||
|
|
// return parseMessageList(res.data);
|
|||
|
|
void params; // suppress unused-param warning; remove when real call is wired
|
|||
|
|
throw new Error(
|
|||
|
|
'GmailApiClient.searchInbox is not implemented — inject a GmailClient mock in tests and wire the real SDK call here for production.',
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async getThreadDetail(params: GetThreadDetailParams): Promise<EmailThread> {
|
|||
|
|
// TODO: Replace this stub with the real Gmail API call.
|
|||
|
|
// When implementing, fetch a short-lived access token first:
|
|||
|
|
// const accessToken = await this.tokenProvider.getAccessToken(params.userSub);
|
|||
|
|
// Example (googleapis):
|
|||
|
|
// const auth = new google.auth.OAuth2();
|
|||
|
|
// auth.setCredentials({ access_token: accessToken });
|
|||
|
|
// const gmail = google.gmail({ version: 'v1', auth });
|
|||
|
|
// const res = await gmail.users.threads.get({
|
|||
|
|
// userId: 'me',
|
|||
|
|
// id: params.threadId,
|
|||
|
|
// format: 'full',
|
|||
|
|
// });
|
|||
|
|
// return parseThread(res.data);
|
|||
|
|
void params; // suppress unused-param warning; remove when real call is wired
|
|||
|
|
throw new Error(
|
|||
|
|
'GmailApiClient.getThreadDetail is not implemented — inject a GmailClient mock in tests and wire the real SDK call here for production.',
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|