
Security News
Node.js Drops Bug Bounty Rewards After Funding Dries Up
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.
ms-graph-devtools
Advanced tools
TypeScript utility for Microsoft Graph API automation - Outlook, Teams, SharePoint, and Calendar with fluent builders and automatic token management
A TypeScript utility for Microsoft Graph API operations with automatic token management and cross-platform storage support.
Azure.service pattern (e.g., Azure.outlook.sendMail())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:
Critical Design Decision:
expiredAt - only set when Azure provides itPurpose: Production automation, scheduled tasks, single-tenant apps
API:
Azure.config({
refreshToken: "xxx",
clientId: "...",
clientSecret: "...",
tenantId: "...",
});
await Azure.outlook.sendMail({...});
Key Features:
Storage Strategy:
refreshToken, accessToken, clientId, tenantId, expiresAtclientSecret (security best practice)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:
npm install ms-graph-devtools
Create 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?
Azure.config(config): voidSet 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 sitesAzure.reset(): voidReset global configuration and clear all service instances. Useful for testing or switching accounts.
Azure.reset();
Azure.config({ refreshToken: "new-token" });
Azure.listStoredCredentials(): Promise<Array>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
// ]
Azure.clearStoredCredentials(tenantId?, clientId?): Promise<void>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:
init() configtokenProvider callbackTokens are automatically saved to platform-specific locations with multi-tenant support:
Storage Directory:
C:\Users\{user}\AppData\Local\ms-graph-devtools\~/.config/ms-graph-devtools/File Naming (Automatic):
tokens.{tenantId}.{clientId}.jsontokens.jsonExample:
~/.config/ms-graph-devtools/
├── tokens.abc123-tenant.def456-client.json
├── tokens.xyz789-tenant.app2-client.json
└── tokens.json (fallback)
Benefits:
// 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:
Azure.outlook, Azure.teams, etc. after calling Azure.config()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 builderEmail 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 emailTeams 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 builderAdaptive 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 cardSharePoint 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 itemCalendar 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 config
import { 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:
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:
0o700 (owner only)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.js
Or 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:
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:
%LOCALAPPDATA%\ms-graph-devtools\tokens.json~/.config/ms-graph-devtools/tokens.jsonQ: 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() then Azure.config({ refreshToken: 'new-token' })Azure.config() with new credentialsSolution: Provide a token via Azure.config():
Azure.config({ refreshToken: 'xxx' })
// OR
Azure.config({ tokenProvider: async (callback) => getAuthCode(callback) })
// OR
Azure.config({ accessToken: 'xxx' }) // Light mode
Your refresh token has expired (typically 90 days). Get a new one:
node get-refresh-token.js
Your 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:
Playwright dependency (user-specific auth)get-refresh-token.js helper scriptbin in package.json:{
"bin": {
"get-refresh-token": "./get-refresh-token.js"
}
}
Users can then run:
npx your-package get-refresh-token
MIT
Contributions welcome! Please open an issue first for major changes.
Built with ❤️ for automated Microsoft Graph workflows
FAQs
TypeScript utility for Microsoft Graph API automation - Outlook, Teams, SharePoint, and Calendar with fluent builders and automatic token management
The npm package ms-graph-devtools receives a total of 0 weekly downloads. As such, ms-graph-devtools popularity was classified as not popular.
We found that ms-graph-devtools demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.

Security News
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.