From 3f6b13572e04e67878037918ceb6640feb794254 Mon Sep 17 00:00:00 2001 From: lorentz Date: Thu, 7 May 2026 20:39:42 -0400 Subject: [PATCH] feat(08-01): add getUserPhotoBytes() to MsGraphClient - New public method fetches binary photo from Graph /users/{id}/photo/$value - Returns { bytes, contentType } on 200, null on 404 (no photo) - Throws on other non-2xx for upstream caller to map to 502/503 - Reuses getToken() OAuth2 cache; no retry (best-effort per D-26) - Existing methods (getToken, fetchJson, getUsers, etc.) untouched --- lib/services/msgraph-client.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lib/services/msgraph-client.ts b/lib/services/msgraph-client.ts index fc962c1..258de81 100644 --- a/lib/services/msgraph-client.ts +++ b/lib/services/msgraph-client.ts @@ -408,6 +408,35 @@ export class MsGraphClient { throw new Error(`Graph move failed ${res.status}: ${text}`); } + /** + * Fetch the raw photo bytes for a Microsoft Graph user. + * Returns { bytes, contentType } on 200, null when the user has no photo (404), + * and throws on any other non-2xx response so the caller can map to 502/503. + * + * Does NOT reuse fetchJson (which is JSON-only); issues its own fetch for binary data. + * No retry logic — photo fetches are best-effort per D-26. + */ + async getUserPhotoBytes(userId: string): Promise<{ bytes: ArrayBuffer; contentType: string } | null> { + const token = await this.getToken(); + const url = `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(userId)}/photo/$value`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (res.status === 404) { + return null; + } + + if (!res.ok) { + const text = await res.text().catch(() => ''); + throw new Error(`Graph photo error ${res.status} for user ${userId}: ${text}`); + } + + const bytes = await res.arrayBuffer(); + const contentType = res.headers.get('content-type') ?? 'image/jpeg'; + return { bytes, contentType }; + } + /** * Get calendar events for a user in a date range (paginated). * Returns empty array and logs if the mailbox is not Exchange Online (graceful degradation).