📋 Shared Instructions: shared-instructions.md — Cross-cutting concerns.
Add Office 365 Users (Wrapper)This skill is a thin wrapper. Use /add-data-source as the single implementation path.
Delegation contract
Invoke /add-data-source with:
api-id:shared_office365usersmode:action
Office 365 Users: Method Selection Guide
After the connector is added, import the generated service:
import { Office365UsersService } from '../../generated/services/Office365UsersService'
Use the V2 methods for profiles and reporting relationships:
| Scenario | Method | Result |
|---|---|---|
| Current signed-in user | MyProfile_V2($select?) |
One GraphUser_V1 |
| Specific user by ID or UPN | UserProfile_V2(id, $select?) |
One GraphUser_V1 |
| User's manager | Manager_V2(id, $select?) |
One GraphUser_V1 |
| Direct reports | DirectReports_V2(id, $select?, $top?) |
Profiles in result.data.value |
| Search people | SearchUserV2(searchTerm, top?, ...) |
Search response in result.data.value |
| Profile photo | UserPhotoMetadata(userId), then UserPhoto_V2(id) |
Metadata, then binary image data |
DirectReports_V2 requires the target manager's Microsoft Entra directory ID or user
principal name (UPN). For the signed-in user, call
MyProfile_V2('id,userPrincipalName'), then pass the returned id or fall back to
userPrincipalName. For a known person, pass their directory ID or UPN directly.
Profile and direct-report pattern
Request only the fields the UI needs:
const fields = [
'id',
'displayName',
'jobTitle',
'department',
'mail',
'userPrincipalName',
'officeLocation',
].join(',')
const myProfile = await Office365UsersService.MyProfile_V2(
'id,userPrincipalName',
)
if (!myProfile.success) {
throw new Error(myProfile.error?.message ?? 'Failed to resolve current user')
}
const managerId =
myProfile.data?.id ?? myProfile.data?.userPrincipalName
if (!managerId) {
throw new Error('Current user profile did not include an ID or UPN')
}
const result = await Office365UsersService.DirectReports_V2(
managerId,
fields,
)
if (!result.success) {
throw new Error(result.error?.message ?? 'Failed to load direct reports')
}
const people = result.data?.value ?? []
An empty value array is valid and means the user has no direct reports.
Profile photos: binary response and CSP-safe rendering
Call UserPhotoMetadata before downloading the photo. HasPhoto: false is a valid
no-photo state; render initials or another accessible fallback.
Image responses are a special runtime contract: the generated UserPhoto_V2 signature may
declare IOperationResult<string>, but the Managed Apps SDK decodes an image/* response into
a Uint8Array. Do not interpolate that byte array directly into a base64 URL, and do not use a
blob: URL—the deployed App Player content security policy may block blob: under img-src.
Convert the bytes to a data: URL:
function bytesToBase64(bytes: Uint8Array) {
const chunkSize = 0x8000
let binary = ''
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
binary += String.fromCharCode(
...bytes.subarray(offset, Math.min(offset + chunkSize, bytes.length)),
)
}
return btoa(binary)
}
function toImageDataUrl(value: unknown, contentType = 'image/jpeg') {
if (value instanceof Uint8Array) {
return `data:${contentType};base64,${bytesToBase64(value)}`
}
if (value instanceof ArrayBuffer) {
return `data:${contentType};base64,${bytesToBase64(new Uint8Array(value))}`
}
if (typeof value === 'string') {
if (/^data:image\/[a-z0-9.+-]+;base64,/i.test(value)) {
return value
}
if (/^https?:/i.test(value)) {
throw new Error(
'Profile photo returned an HTTP URL instead of binary image data.',
)
}
return `data:${contentType};base64,${value}`
}
throw new Error('The profile photo response used an unsupported format.')
}
UserPhoto_V2 has a binary response contract and does not return an external image URL.
An HTTP value indicates an unexpected response shape, so the helper rejects it rather than
passing a potentially CSP-blocked or authenticated URL to <img src>.
Complete photo flow:
const metadata = await Office365UsersService.UserPhotoMetadata(userId)
if (!metadata.success) {
throw new Error(metadata.error?.message ?? 'Failed to check profile photo')
}
if (!metadata.data?.HasPhoto) {
return undefined
}
const photo = await Office365UsersService.UserPhoto_V2(userId)
if (!photo.success) {
throw new Error(photo.error?.message ?? 'Failed to load profile photo')
}
return photo.data
? toImageDataUrl(photo.data, metadata.data.ContentType)
: undefined
Always fetch photos through the generated connector service. Do not call Microsoft Graph
directly and do not assign the authenticated connector runtime URL to <img src>.