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
This commit is contained in:
lorentz 2026-05-07 20:39:42 -04:00
parent 3f35e1e785
commit 3f6b13572e

View file

@ -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).