A TypeScript utility for Microsoft Graph API operations with automatic token management and cross-platform storage support.
- ✅ Global instance pattern - Configure once, export, use everywhere in your app
- ✅ AWS SDK-style API - Familiar
Azure.servicepattern (e.g.,Azure.outlook.sendMail()) - ✅ Automatic token refresh - Handles token expiration transparently
- ✅ Multiple auth modes - Access token, refresh token, or custom token provider
- ✅ Cross-platform storage - Works on Windows, Mac, and Linux using XDG standards
- ✅ TypeScript support - Full type safety and autocomplete
- ✅ Fluent builders - Clean, chainable APIs for emails and Teams cards
- ✅ Zero config imports - Import configured instance, no setup needed at call site
Successfully designed and implemented a progressive complexity model for Azure authentication that supports all user types from beginners to enterprise.
Purpose: Quick testing, POC, experimentation
API:
Azure.config({ accessToken: "eyJ0eX..." });
await Azure.outlook.getMe();Key Features:
- Zero setup - paste token and go
- No credentials required
- Perfect for testing
- Expires in ~1 hour
- No automatic renewal
Critical Design Decision:
- ✅ Don't assume
expiredAt- only set when Azure provides it - ✅ Fail gracefully with helpful error message
- ✅ Don't force users to "upgrade" - respect their choice
Purpose: Production automation, scheduled tasks, single-tenant apps
API:
Azure.config({
refreshToken: "xxx",
clientId: "...",
clientSecret: "...",
tenantId: "...",
});
await Azure.outlook.sendMail({...});Key Features:
- Automatic token refresh for ~90 days
- Cross-platform storage (XDG standard)
- Works for 90% of users
Storage Strategy:
- ✅ Store:
refreshToken,accessToken,clientId,tenantId,expiresAt - ❌ NEVER store:
clientSecret(security best practice) - Client secret must be provided via init config
Purpose: Enterprise apps, multi-tenant SaaS, long-running services
API:
Azure.config({
clientId: "...",
clientSecret: "...",
tenantId: "...",
tokenProvider: async (callback) => await getAuthCode(callback),
});
await Azure.teams.postMessage({...});Key Features:
- Never expires (provider handles rotation)
- Multi-tenant support
- Integration with secret vaults (HashiCorp Vault, AWS Secrets Manager, etc.)
- Provider called only when needed
- Skips storage (provider is source of truth)
npm install ms-graph-devtoolsCreate a single config file and export the configured instance:
// config/azure.ts (or src/lib/azure.ts, etc.)
import Azure from 'ms-graph-devtools';
Azure.config({
clientId: process.env.AZURE_CLIENT_ID,
clientSecret: process.env.AZURE_CLIENT_SECRET,
tenantId: process.env.AZURE_TENANT_ID,
tokenProvider: async (callback) => {
return await Playwright.getAzureCode(callback);
},
});
// Export configured instance - this is your single source of truth
export default Azure;Then import and use everywhere in your app:
// anywhere in your codebase
import Azure from './config/azure';
// Use services directly - already configured!
await Azure.outlook.sendMail({...});
await Azure.teams.postMessage({...});
await Azure.calendar.getCalendars();
await Azure.sharePoint.getLists();Why this pattern?
- ✅ Configure once, use everywhere
- ✅ Single source of truth for credentials
- ✅ No config duplication across files
- ✅ Easy to test (mock one import)
- ✅ Consistent auth across your entire app
- ✅ Clean imports - no config needed at call site
Set global configuration for all Azure services. Call once at app startup.
Parameters:
interface AzureConfig {
// Authentication options (choose one):
accessToken?: string; // Light mode: temporary token (~1 hour)
refreshToken?: string; // Medium mode: 90-day auto-renewal
tokenProvider?: (callback: string) => Promise<string> | string; // Super mode: infinite renewal
// Required credentials:
clientId?: string; // Azure app client ID
clientSecret?: string; // Azure app client secret
tenantId?: string; // Azure tenant ID
// Optional:
scopes?: string[]; // Custom OAuth scopes
allowInsecure?: boolean; // Allow insecure SSL (dev only)
}Examples:
// Medium User: Refresh token (90-day renewal)
Azure.config({
clientId: process.env.AZURE_CLIENT_ID,
clientSecret: process.env.AZURE_CLIENT_SECRET,
tenantId: process.env.AZURE_TENANT_ID,
refreshToken: process.env.AZURE_REFRESH_TOKEN,
});
// Super User: Token provider (infinite renewal)
Azure.config({
clientId: process.env.AZURE_CLIENT_ID,
clientSecret: process.env.AZURE_CLIENT_SECRET,
tenantId: process.env.AZURE_TENANT_ID,
tokenProvider: async (callback) => {
// Your custom auth logic (e.g., Playwright, browser automation)
return await getAuthorizationCode(callback);
},
});
// Light User: Access token only (temporary)
Azure.config({
accessToken: "eyJ0eX...", // Expires in ~1 hour
});After calling Azure.config(), access services directly:
Azure.outlook- Email operationsAzure.teams- Teams messaging and adaptive cardsAzure.calendar- Calendar and holidaysAzure.sharePoint- SharePoint lists and sites
Reset global configuration and clear all service instances. Useful for testing or switching accounts.
Azure.reset();
Azure.config({ refreshToken: "new-token" });List all stored credential files. Useful for debugging multi-tenant setups.
const stored = await Azure.listStoredCredentials();
console.log(stored);
// [
// { tenantId: 'abc123', clientId: 'def456', file: 'tokens.abc123.def456.json' },
// { tenantId: 'xyz789', clientId: 'app2', file: 'tokens.xyz789.app2.json' },
// { file: 'tokens.json' } // Legacy file
// ]Clear stored credentials for a specific tenant/client or all.
// Clear specific tenant/client
await Azure.clearStoredCredentials("abc123", "def456");
// Clear all stored credentials
await Azure.clearStoredCredentials();The utility loads tokens in this priority order:
- Memory - Token provided in
init()config - Storage - Saved token file (cross-platform)
- Provider - Custom
tokenProvidercallback - Error - If none available, throws helpful error
Tokens are automatically saved to platform-specific locations with multi-tenant support:
Storage Directory:
- Windows:
C:\Users\{user}\AppData\Local\ms-graph-devtools\ - Mac/Linux:
~/.config/ms-graph-devtools/
File Naming (Automatic):
- With tenant+client:
tokens.{tenantId}.{clientId}.json - Legacy/fallback:
tokens.json
Example:
~/.config/ms-graph-devtools/
├── tokens.abc123-tenant.def456-client.json
├── tokens.xyz789-tenant.app2-client.json
└── tokens.json (fallback)
Benefits:
- ✅ Multiple tenants/clients supported automatically
- ✅ No overwrites when switching configurations
- ✅ Backward compatible with single-tenant setups
- ✅ Files created with secure permissions (owner read/write only)
// config/azure.ts - Configure once, use everywhere
import Azure from 'ms-graph-devtools';
Azure.config({
clientId: process.env.AZURE_CLIENT_ID,
clientSecret: process.env.AZURE_CLIENT_SECRET,
tenantId: process.env.AZURE_TENANT_ID,
refreshToken: process.env.AZURE_REFRESH_TOKEN,
});
export default Azure;// anywhere in your app
import Azure from './config/azure';
await Azure.outlook.sendMail({...});
await Azure.teams.postMessage({...});// scheduled-task.ts
import Azure from './config/azure'; // Pre-configured instance
async function dailyReport() {
const emails = await Azure.outlook.getMails(
new Date().toISOString(),
"Invoice"
);
await Azure.outlook.sendMail({
message: {
subject: "Daily Report",
body: {
contentType: "Text",
content: `Found ${emails.length} invoices today`,
},
toRecipients: [
{
emailAddress: { address: "admin@example.com" },
},
],
},
});
}
dailyReport();Add to cron:
0 9 * * * cd /path/to/project && node scheduled-task.js// config/azure.ts
import Azure from "ms-graph-devtools";
import Playwright from "./playwright-helper";
Azure.config({
clientId: process.env.AZURE_CLIENT_ID,
clientSecret: process.env.AZURE_CLIENT_SECRET,
tenantId: process.env.AZURE_TENANT_ID,
tokenProvider: async (callback) => {
// Playwright automates browser to get auth code
return await Playwright.getAzureCode(callback);
},
});
export default Azure;// anywhere in your app
import Azure from './config/azure';
// Just works - tokenProvider handles auth automatically
await Azure.outlook.getMe();
await Azure.calendar.getCalendars();import Azure from "ms-graph-devtools";
// Company A
Azure.config({
tenantId: "company-a-tenant-id",
clientId: "app-1-client-id",
clientSecret: process.env.COMPANY_A_SECRET,
refreshToken: "company-a-token",
});
await Azure.outlook.getMe();
// Saved to: tokens.company-a-tenant-id.app-1-client-id.json
// Switch to Company B (reset first)
Azure.reset();
Azure.config({
tenantId: "company-b-tenant-id",
clientId: "app-2-client-id",
clientSecret: process.env.COMPANY_B_SECRET,
refreshToken: "company-b-token",
});
await Azure.teams.getTeams();
// Saved to: tokens.company-b-tenant-id.app-2-client-id.json
// Both token files are preserved!
// Switch between them by calling reset() and config()// automation-script.js
import Azure from 'ms-graph-devtools';
// Load credentials from GitHub Secrets or environment
Azure.config({
clientId: process.env.AZURE_CLIENT_ID,
clientSecret: process.env.AZURE_CLIENT_SECRET,
tenantId: process.env.AZURE_TENANT_ID,
refreshToken: process.env.AZURE_REFRESH_TOKEN,
});
await Azure.outlook.sendMail({...});
await Azure.teams.postMessage({...});# .github/workflows/automation.yml
steps:
- run: node automation-script.js
env:
AZURE_REFRESH_TOKEN: ${{ secrets.AZURE_REFRESH_TOKEN }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}This library provides specialized service classes for different Microsoft Graph APIs.
Two usage patterns:
- Global instance (Recommended):
Azure.outlook,Azure.teams, etc. after callingAzure.config() - Direct instantiation:
new Outlook()for advanced use cases (multiple configs, dependency injection)
Email operations with fluent builder pattern.
import Azure from "ms-graph-devtools";
// Pattern 1: Via Azure global instance (recommended)
const user = await Azure.outlook.getMe();
// Get emails
const emails = await Azure.outlook.getMails("2024-01-15", "invoice");
// Send email with fluent builder
await Azure.outlook
.compose()
.subject("Meeting Reminder")
.body("Don't forget our meeting tomorrow!", "Text")
.to(["colleague@example.com"])
.cc(["manager@example.com"])
.importance("high")
.send();
// With attachments
await Azure.outlook
.compose()
.subject("Monthly Report")
.body("<h1>Report</h1>", "HTML")
.to(["boss@example.com"])
.attachments(["./report.pdf", "./charts.xlsx"])
.send();
// Pattern 2: Direct instantiation (advanced)
import { Outlook } from "ms-graph-devtools";
const outlook = new Outlook({ refreshToken: "..." });
await outlook.sendMail({...});Available Methods:
getMe()- Get current user profilesendMail(payload)- Send email (low-level)getMails(date, subjectFilter?)- Get emails by datecompose()- Create fluent email builder
Email Builder Methods:
subject(text)- Set subjectbody(content, type?)- Set body (Text/HTML)to(recipients[])- Set recipientscc(recipients[])- Set CCbcc(recipients[])- Set BCCattachments(files[])- Add attachmentsimportance(level)- Set priority (low/normal/high)requestReadReceipt(bool)- Request read receiptsend()- Send the email
Teams messaging, adaptive cards, and channel management.
import Azure from "ms-graph-devtools";
// Get user's teams
const myTeams = await Azure.teams.getTeams();
// [{ id: '...', displayName: 'Engineering Team', description: '...' }]
// Get channels for a team
const channels = await Azure.teams.getChannels(myTeams[0].id);
// [{ id: '...', displayName: 'General', membershipType: 'standard' }]
// Get tags for mentions
const tags = await Azure.teams.getTags(myTeams[0].id);
// [{ id: '...', displayName: 'Backend Team', memberCount: 5 }]
// Send adaptive card with builder
await Azure.teams
.compose()
.team("team-id")
.channel("channel-id")
.card({
type: "AdaptiveCard",
version: "1.4",
body: [
{
type: "TextBlock",
text: "Deployment Complete!",
size: "Large",
weight: "Bolder",
},
],
actions: [
{
type: "Action.OpenUrl",
title: "View Dashboard",
url: "https://dashboard.example.com",
},
],
})
.mentionTeam("team-id", "Engineering Team")
.mentionUser("user-id", "John Doe")
.send();Available Methods:
getTeams()- Get all teams user has joinedgetChannels(teamId)- Get channels in a teamgetTags(teamId)- Get tags for mentionspostAdaptiveCard(teamId, channelId, card, tags?)- Post card (low-level)compose()- Create fluent adaptive card builder
Adaptive Card Builder Methods:
team(teamId)- Set target teamchannel(channelId)- Set target channelcard(adaptiveCard)- Set card JSONmentionTeam(id, name)- Add team mentionmentionTag(id, name)- Add tag mentionmentionUser(id, name)- Add user mentionmentions(tags[])- Add multiple mentionssend()- Post the card
SharePoint list operations and site management.
import Azure from "ms-graph-devtools";
// Search for sites
const sites = await Azure.sharePoint.searchSites("Engineering");
Azure.sharePoint.setSiteId(sites[0].id);
// Get all lists in site
const lists = await Azure.sharePoint.getLists();
// [{ id: '...', displayName: 'Tasks', webUrl: '...' }]
// Get list columns
const columns = await Azure.sharePoint.getListColumns("Tasks");
// Create list item
await Azure.sharePoint.createListItem("Tasks", {
Title: "New Task",
Status: "Active",
Priority: "High",
DueDate: "2024-12-31",
});
// Query items with filtering
const items = await Azure.sharePoint.getListItems("Tasks", {
filter: "fields/Status eq 'Active'",
orderby: "createdDateTime desc",
top: 10,
expand: "fields",
});
// Update item
await Azure.sharePoint.updateListItem("Tasks", "item-id", {
Status: "Completed",
});
// Delete item
await Azure.sharePoint.deleteListItem("Tasks", "item-id");
// Advanced: Query and process (task queue pattern)
const tasks = await Azure.sharePoint.queryAndProcess(
"TaskQueue",
"fields/taskName eq 'send-notification'",
(item) => ({
id: item.id,
recipient: item.fields.recipient,
message: item.fields.message,
}),
true // delete after processing
);
// Get latest item
const latest = await Azure.sharePoint.getLatestItem("Tasks");Available Methods:
setSiteId(siteId)- Set default site IDsearchSites(query)- Search for sitesgetSiteByPath(hostname, path)- Get specific sitegetLists(siteId?)- Get all listsgetList(listId, siteId?)- Get specific listgetListColumns(listId, siteId?)- Get list columnsgetListItems(listId, options?, siteId?)- Query list itemsgetListItem(listId, itemId, expand?, siteId?)- Get single itemcreateListItem(listId, fields, siteId?)- Create itemupdateListItem(listId, itemId, fields, siteId?)- Update itemdeleteListItem(listId, itemId, siteId?)- Delete itemdeleteListItems(listId, itemIds[], siteId?)- Bulk deletequeryAndProcess(listId, filter, processor, deleteAfter?, siteId?)- Query and process patterngetLatestItem(listId, orderBy?, filter?, siteId?)- Get most recent item
Calendar and holiday management.
import Azure from "ms-graph-devtools";
// Get all calendars
const calendars = await Azure.calendar.getCalendars();
// Get holidays
const indiaHolidays = await Azure.calendar.getIndiaHolidays(
"2024-01-01",
"2024-12-31"
);
const japanHolidays = await Azure.calendar.getJapanHolidays(
"2024-01-01",
"2024-12-31"
);Available Methods:
getCalendars()- Get user's calendarsgetIndiaHolidays(start, end)- Get India holidaysgetJapanHolidays(start, end)- Get Japan holidays
// config/azure.ts
import Azure from 'ms-graph-devtools';
Azure.config({
clientId: process.env.AZURE_CLIENT_ID,
clientSecret: process.env.AZURE_CLIENT_SECRET,
tenantId: process.env.AZURE_TENANT_ID,
refreshToken: process.env.AZURE_REFRESH_TOKEN,
});
export default Azure;// anywhere in your app
import Azure from './config/azure';
await Azure.outlook.compose().subject('Test').to(['user@example.com']).send();
await Azure.teams.compose().team('id').channel('id').card({...}).send();
await Azure.sharePoint.createListItem('Tasks', { Title: 'New Task' });For cases where you need multiple configurations or dependency injection:
import { Outlook, Teams } from "ms-graph-devtools";
const outlook = new Outlook({
clientId: process.env.AZURE_CLIENT_ID,
clientSecret: process.env.AZURE_CLIENT_SECRET,
tenantId: process.env.AZURE_TENANT_ID,
refreshToken: process.env.AZURE_REFRESH_TOKEN,
});
const teams = new Teams(); // Uses global configimport { AzureAuth, Outlook, Teams } from "ms-graph-devtools";
// Create shared auth instance
const auth = new AzureAuth({ refreshToken: "your-token" });
// Share across services
const outlook = new Outlook(auth);
const teams = new Teams(auth);
await outlook.getMe();
await teams.getTeams();import Azure from './config/azure';
// Process task queue
const tasks = await Azure.sharePoint.queryAndProcess(
"NotificationQueue",
"fields/status eq 'pending'",
async (item) => {
// Send notification to Teams
await Azure.teams
.compose()
.team(item.fields.teamId)
.channel(item.fields.channelId)
.card(JSON.parse(item.fields.cardData))
.send();
return { id: item.id, processed: true };
},
true // delete after processing
);
console.log(`Processed ${tasks.length} notifications`);The recommended way to use this library:
// ✅ GOOD: Single config file pattern
// config/azure.ts
import Azure from 'ms-graph-devtools';
Azure.config({
clientId: process.env.AZURE_CLIENT_ID,
clientSecret: process.env.AZURE_CLIENT_SECRET,
tenantId: process.env.AZURE_TENANT_ID,
tokenProvider: async (callback) => await getAuthCode(callback),
});
export default Azure;// Everywhere else in your app
import Azure from './config/azure';
await Azure.outlook.sendMail({...});Why this is best:
- Single source of truth for configuration
- No config duplication across files
- Easier to maintain and update credentials
- Consistent authentication across entire app
- Simpler imports (just the instance, no config needed)
Avoid this pattern:
// ❌ BAD: Repeating config everywhere
import { Outlook } from 'ms-graph-devtools';
const outlook = new Outlook({
clientId: process.env.AZURE_CLIENT_ID, // Repeated in every file
clientSecret: process.env.AZURE_CLIENT_SECRET,
tenantId: process.env.AZURE_TENANT_ID,
refreshToken: process.env.AZURE_REFRESH_TOKEN,
});File structure:
your-project/
├── config/
│ └── azure.ts # Azure config + export
├── src/
│ ├── email.ts # import Azure from '../config/azure'
│ ├── notifications.ts # import Azure from '../config/azure'
│ └── reports.ts # import Azure from '../config/azure'
└── .env # Credentials (gitignored)
Always load credentials from environment variables, never hardcode:
// config/azure.ts
import Azure from 'ms-graph-devtools';
import 'dotenv/config'; // If using dotenv
Azure.config({
clientId: process.env.AZURE_CLIENT_ID!,
clientSecret: process.env.AZURE_CLIENT_SECRET!,
tenantId: process.env.AZURE_TENANT_ID!,
tokenProvider: async (callback) => await getAuthCode(callback),
});
export default Azure;// config/azure.ts
import Azure from 'ms-graph-devtools';
// Validate required env vars at startup
const requiredEnvVars = ['AZURE_CLIENT_ID', 'AZURE_CLIENT_SECRET', 'AZURE_TENANT_ID'];
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(`Missing required environment variable: ${envVar}`);
}
}
Azure.config({
clientId: process.env.AZURE_CLIENT_ID!,
clientSecret: process.env.AZURE_CLIENT_SECRET!,
tenantId: process.env.AZURE_TENANT_ID!,
tokenProvider: async (callback) => await getAuthCode(callback),
});
export default Azure;Add to .gitignore:
.env
tokens.json
*.token
Store credentials as GitHub Secrets or CI/CD environment variables and pass them explicitly to the library:
const outlook = new Outlook({
clientId: process.env.AZURE_CLIENT_ID,
clientSecret: process.env.AZURE_CLIENT_SECRET,
tenantId: process.env.AZURE_TENANT_ID,
refreshToken: process.env.AZURE_REFRESH_TOKEN,
});The utility automatically sets secure permissions on token files:
- Directory:
0o700(owner only) - File:
0o600(owner read/write only)
Refresh tokens typically expire after 90 days. Set a reminder to rotate them.
Only request the scopes you need in your Azure App Registration.
See get-refresh-token.js for an interactive tool to obtain your refresh token.
node get-refresh-token.jsOr see REFRESH_TOKEN_SETUP.md for manual instructions.
Q: What's the recommended way to use this library?
A: Export a configured global instance - this is the best practice for 95% of use cases:
// config/azure.ts - Configure once
import Azure from 'ms-graph-devtools';
Azure.config({...});
export default Azure;
// everywhere else - just import and use
import Azure from './config/azure';
await Azure.outlook.sendMail({...});This gives you:
- Single source of truth
- No config duplication
- Clean imports everywhere
- Easy to maintain
Q: Do I need to call Azure.config() every time I use a service?
A: No! That's the beauty of the global instance pattern. Call Azure.config() once in a config file, export the instance, then import and use it everywhere.
// config/azure.ts - Call config() ONCE
Azure.config({...});
export default Azure;
// other files - NO config() needed, just import
import Azure from './config/azure';
await Azure.outlook.sendMail({...});
await Azure.teams.postMessage({...});Q: What if I call config() multiple times with different tokens?
A: Each call to config() replaces the previous configuration and resets all service instances. Use Azure.reset() first for clarity.
Q: Where are tokens stored?
A: Platform-specific secure locations:
- Windows:
%LOCALAPPDATA%\ms-graph-devtools\tokens.json - Mac/Linux:
~/.config/ms-graph-devtools/tokens.json
Q: Can I use this in serverless/Lambda?
A: Yes! Use a custom tokenProvider that fetches from your secret store (AWS Secrets Manager, etc.).
Q: Will this work with my organization's SSO (Okta, 1Password, etc.)?
A: Yes! You obtain the refresh token once through your SSO, then the utility handles everything automatically.
Q: How do I update the token?
A: Either:
Azure.reset()thenAzure.config({ refreshToken: 'new-token' })- Delete the storage file and call
Azure.config()with new credentials
Solution: Provide a token via Azure.config():
Azure.config({ refreshToken: 'xxx' })
// OR
Azure.config({ tokenProvider: async (callback) => getAuthCode(callback) })
// OR
Azure.config({ accessToken: 'xxx' }) // Light modeYour refresh token has expired (typically 90 days). Get a new one:
node get-refresh-token.jsYour Azure App Registration needs API permissions. Go to: Azure Portal → App Registrations → API permissions → Add permissions
Normal on first run. The file is created when you first provide a token.
When publishing to npm:
- Remove
Playwrightdependency (user-specific auth) - Include
get-refresh-token.jshelper script - Document in README how users get their token
- Add to
binin package.json:
{
"bin": {
"get-refresh-token": "./get-refresh-token.js"
}
}Users can then run:
npx your-package get-refresh-tokenMIT
Contributions welcome! Please open an issue first for major changes.
Built with ❤️ for automated Microsoft Graph workflows