From miro-pack
Installs @mirohq/miro-api Node.js client and configures OAuth 2.0 authentication for Miro REST API v2.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin miro-packThis skill is limited to using the following tools:
Set up the official `@mirohq/miro-api` Node.js client and configure OAuth 2.0 authentication against the Miro REST API v2 (`https://api.miro.com/v2/`).
Provides TypeScript patterns for @mirohq/miro-api SDK and Miro REST API v2, including OAuth multi-user clients, token-based APIs, and type-safe board services.
Configures Figma REST API authentication with personal access tokens or OAuth 2.0, covering token generation, scopes, secure storage, and verification for integrations.
Guides OAuth client setup for API integrations with step-by-step instructions, production-ready code, configurations, and best practices. Auto-activates on 'oauth client setup' phrases.
Share bugs, ideas, or general feedback.
Set up the official @mirohq/miro-api Node.js client and configure OAuth 2.0 authentication against the Miro REST API v2 (https://api.miro.com/v2/).
# Official Miro Node.js client
npm install @mirohq/miro-api
# For Express-based OAuth callback server
npm install express dotenv
# .env (NEVER commit — add to .gitignore)
MIRO_CLIENT_ID=your_client_id
MIRO_CLIENT_SECRET=your_client_secret
MIRO_REDIRECT_URI=http://localhost:3000/auth/miro/callback
MIRO_ACCESS_TOKEN= # Filled after OAuth flow
MIRO_REFRESH_TOKEN= # Filled after OAuth flow
Miro uses standard OAuth 2.0 authorization code flow. Tokens expire in 3599 seconds (approximately 1 hour). Always store and use the refresh token.
// src/auth.ts
import { Miro } from '@mirohq/miro-api';
import express from 'express';
// High-level client handles token management
const miro = new Miro({
clientId: process.env.MIRO_CLIENT_ID!,
clientSecret: process.env.MIRO_CLIENT_SECRET!,
redirectUrl: process.env.MIRO_REDIRECT_URI!,
// Storage adapter for tokens (implement for production)
storage: {
async get(userId: string) {
// Return stored token for user
return getTokenFromDB(userId);
},
async set(userId: string, token) {
// Persist token
await saveTokenToDB(userId, token);
},
},
});
const app = express();
// Step 1: Redirect user to Miro authorization page
app.get('/auth/miro', (req, res) => {
const authUrl = miro.getAuthUrl();
res.redirect(authUrl);
});
// Step 2: Handle OAuth callback
app.get('/auth/miro/callback', async (req, res) => {
const { code } = req.query;
if (!code || typeof code !== 'string') {
return res.status(400).send('Missing authorization code');
}
try {
// Exchange code for access_token + refresh_token
await miro.exchangeCodeForAccessToken('default-user', code);
res.send('Miro connected successfully!');
} catch (err) {
console.error('Token exchange failed:', err);
res.status(500).send('Authentication failed');
}
});
app.listen(3000, () => console.log('OAuth server at http://localhost:3000'));
For scripts and automation where you already have an access token:
// src/client.ts
import { MiroApi } from '@mirohq/miro-api';
// Low-level stateless client — pass token directly
const api = new MiroApi(process.env.MIRO_ACCESS_TOKEN!);
// Verify connection by listing boards
async function verifyConnection() {
const boards = await api.getBoards();
console.log(`Connected! Found ${boards.body.data?.length ?? 0} boards`);
return true;
}
verifyConnection().catch(console.error);
In your Miro app settings (https://developers.miro.com), enable the scopes your app requires:
| Scope | Purpose | Required For |
|---|---|---|
boards:read | Read board data, items, members | GET endpoints |
boards:write | Create/update/delete boards and items | POST/PUT/PATCH/DELETE endpoints |
team:read | Read team info and members | Team management |
team:write | Manage team membership | Team provisioning |
organizations:read | Read org structure | Enterprise features |
identity:read | Read user profile | User identification |
auditlogs:read | Read audit logs | Enterprise compliance |
Token response after successful exchange:
{
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"token_type": "bearer",
"expires_in": 3599,
"scope": "boards:read boards:write",
"user_id": "1234567890",
"team_id": "9876543210"
}
| Error | HTTP Status | Cause | Solution |
|---|---|---|---|
insufficientPermissions | 403 | Missing OAuth scope | Add required scope in app settings and re-authorize |
tokenExpired | 401 | Access token expired | Use refresh token to get new access token |
invalidGrant | 400 | Auth code already used or expired | Restart OAuth flow from the beginning |
invalidClient | 401 | Wrong client_id or client_secret | Verify credentials in Miro app settings |
ENOTFOUND api.miro.com | N/A | DNS/network failure | Check internet and firewall rules |
async function refreshAccessToken(): Promise<string> {
const response = await fetch('https://api.miro.com/v1/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: process.env.MIRO_CLIENT_ID!,
client_secret: process.env.MIRO_CLIENT_SECRET!,
refresh_token: process.env.MIRO_REFRESH_TOKEN!,
}),
});
if (!response.ok) {
throw new Error(`Token refresh failed: ${response.status}`);
}
const data = await response.json();
// Store new tokens
process.env.MIRO_ACCESS_TOKEN = data.access_token;
process.env.MIRO_REFRESH_TOKEN = data.refresh_token;
return data.access_token;
}
After successful auth, proceed to miro-hello-world for your first board and item operations.