meetschedify
v0.3.0
Published
Schedule Google Meet and Zoom meetings from your Node.js backend with one simple API. Three provider types: Google Meet (static token), Google Meet (per-user OAuth), and Zoom (Server-to-Server). Full TypeScript support, typed errors, retry/timeout, and st
Maintainers
Readme
meetschedify
Schedule Google Meet and Zoom meetings from your Node.js backend in minutes — with full TypeScript support, typed errors, retry logic, and structured logging.
What problem does meetschedify solve?
You want to schedule a Google Meet or Zoom call from your backend — for an interview, a patient consultation, a class, or a sales demo. You don't want to deal with OAuth flows, token refresh logic, Google Calendar API quirks, or Zoom's credential setup from scratch.
meetschedify gives you one clean API that handles all of it.
// That's it. One call. Any provider.
const meeting = await client.createMeeting({
tenantId: 'org_123',
providerId: 'zoom',
title: 'Technical Interview — Backend Engineer',
time: {
start: '2026-06-15T10:00:00',
end: '2026-06-15T11:00:00',
timezone: 'Asia/Kolkata',
},
primaryParticipant: { email: '[email protected]' },
});
console.log(meeting.conferenceLink); // ✅ https://us05web.zoom.us/j/123456789Installation
npm install meetschedifyRequirements: Node.js ≥ 18. No extra runtime dependencies for Zoom — uses Node's native fetch.
Choose your provider type
There are 3 ways to use meetschedify. Pick the one that fits your use case:
| Type | Class | Auth Mode | Best For |
|------|-------|-----------|----------|
| 1 | GoogleMeetStaticProvider | Static refresh token (one-time setup) | Internal tools, HR, hospitals, schools — one Google account owns all meetings |
| 2 | GoogleMeetProvider | Full per-user OAuth | SaaS apps — each customer connects their own Google account |
| 3 | ZoomProvider | Zoom Server-to-Server OAuth | Zoom meetings with zero user login |
Type 1 — Google Meet with a Static Refresh Token
Best for: recruitment platforms, hospital scheduling, university timetables, internal tools — any system where one Google Workspace account manages all calendar events.
No per-user login required at runtime. A developer runs a one-time script to get a refresh token, stores it securely, and that's it forever.
Step 1 — Create a Google Cloud project
- Go to Google Cloud Console
- Create a new project (or select an existing one)
- Go to APIs & Services → Library → enable Google Calendar API
- Go to APIs & Services → Credentials
- Click Create Credentials → OAuth 2.0 Client ID
- Choose Web application
- Under Authorized redirect URIs, add:
http://localhost:5000/oauth2callback - Copy the Client ID and Client Secret
Step 2 — Get your refresh token (one-time only)
Create this script in your project:
// scripts/get-google-token.mjs
// Run once: node scripts/get-google-token.mjs
// Then save the printed REFRESH_TOKEN to your .env or secrets manager.
import http from 'node:http';
import { google } from 'googleapis';
import open from 'open'; // npm install open --save-dev
const CLIENT_ID = process.env.GOOGLE_CLIENT_ID; // from Step 1
const CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET; // from Step 1
const REDIRECT_URI = 'http://localhost:5000/oauth2callback';
if (!CLIENT_ID || !CLIENT_SECRET) {
console.error('Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in your shell first.');
process.exit(1);
}
const oauth2Client = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI);
const authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
prompt: 'consent', // IMPORTANT: forces Google to always return a refresh_token
scope: ['https://www.googleapis.com/auth/calendar'],
});
const server = http.createServer(async (req, res) => {
const reqUrl = new URL(req.url ?? '/', `http://localhost:5000`);
if (reqUrl.pathname !== '/oauth2callback') { res.end(); return; }
const code = reqUrl.searchParams.get('code');
if (!code) {
res.writeHead(400); res.end('No authorization code received.');
server.close(); return;
}
try {
const { tokens } = await oauth2Client.getToken(code);
console.log('\n✅ Copy this into your .env file:\n');
console.log(`GOOGLE_REFRESH_TOKEN=${tokens.refresh_token}`);
console.log('\n⚠️ Store this securely — treat it like a password.\n');
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h2>Done! Check your terminal for the REFRESH_TOKEN.</h2>');
} catch (err) {
console.error('Token exchange failed:', err);
res.writeHead(500); res.end('Token exchange failed. Check terminal.');
}
setTimeout(() => server.close(), 500);
});
server.listen(5000, () => {
console.log('Opening browser — sign in with the Google account that will own meetings.\n');
open(authUrl);
});Run it:
GOOGLE_CLIENT_ID=xxx GOOGLE_CLIENT_SECRET=xxx node scripts/get-google-token.mjsYour browser will open. Sign in with the Google account that should own all meeting invites. The terminal will print your REFRESH_TOKEN. Save it to your .env file or secrets manager.
Step 3 — Add environment variables
# .env
GOOGLE_CLIENT_ID=123456789-xxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-your-secret-here
GOOGLE_REDIRECT_URI=http://localhost:5000/oauth2callback
GOOGLE_REFRESH_TOKEN=1//0xxxxxxxxxxxxxxxx-xxxxxxxx # from Step 2Step 4 — Use in your backend
// meetingClient.ts — create once, use everywhere (singleton)
import {
MeetSchedifyClient,
GoogleMeetStaticProvider,
consoleLogger,
} from 'meetschedify';
export const meetingClient = new MeetSchedifyClient({
providers: [
// Option A — manual config
new GoogleMeetStaticProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectUri: process.env.GOOGLE_REDIRECT_URI!,
refreshToken: process.env.GOOGLE_REFRESH_TOKEN!,
logger: consoleLogger, // or silentLogger in tests
}),
// Option B — fromEnv() shortcut (reads the same env vars automatically)
// GoogleMeetStaticProvider.fromEnv({ logger: consoleLogger }),
],
});Step 5 — Schedule a Google Meet
import { meetingClient } from './meetingClient';
import { MeetSchedifyError, ErrorCodes } from 'meetschedify';
async function scheduleInterview(candidate: Candidate, interviewer: Staff) {
try {
const meeting = await meetingClient.createMeeting({
tenantId: 'acme-hr', // your stable internal tenant ID
providerId: 'google-meet-static',
title: `Interview — ${candidate.name}`,
description: 'Round 2: System Design. Please join 2 minutes early.',
time: {
start: '2026-06-20T10:00:00',
end: '2026-06-20T11:00:00',
timezone: 'Asia/Kolkata', // any IANA timezone
},
primaryParticipant: {
email: candidate.email,
displayName: candidate.name,
},
additionalParticipants: [
{ email: interviewer.email, displayName: interviewer.name },
],
addConferenceLink: true, // attach a Google Meet link
});
return {
meetLink: meeting.conferenceLink, // https://meet.google.com/abc-defg-hij
calendarUrl: meeting.calendarUrl, // Google Calendar event URL
eventId: meeting.providerEventId, // store this if you need to cancel later
};
} catch (err) {
if (err instanceof MeetSchedifyError) {
if (err.code === ErrorCodes.GOOGLE_AUTH_REVOKED) {
throw new Error('Google token expired — re-run the get-token script.');
}
}
throw err;
}
}Type 2 — Google Meet with Per-User OAuth
Best for: SaaS products where each customer connects their own Google account and meetings appear on their own calendar.
Step 1 — Same Google Cloud setup as Type 1
Follow Type 1 Steps 1 but use your production redirect URI, e.g.:
https://yourapp.com/auth/google/callback
No need to run the get-token script — tokens are collected per-user at runtime.
Step 2 — Implement TokenStore
TokenStore is your storage layer for OAuth tokens. Implement it with any database:
// src/store/GoogleTokenStore.ts
import type { TokenStore, TokenStoreContext, TokenRecord } from 'meetschedify';
import { OAuthToken } from './models/OAuthToken'; // your Mongoose/Sequelize/Prisma model
export class GoogleTokenStore implements TokenStore {
async getTokens(ctx: TokenStoreContext): Promise<TokenRecord | null> {
// ctx.tenantId — your tenant/user/org ID
// ctx.providerId — "google-meet" (always)
const doc = await OAuthToken.findOne({
tenantId: ctx.tenantId,
providerId: ctx.providerId,
});
if (!doc) return null;
return {
accessToken: doc.accessToken, // ← decrypt if you encrypt at rest
refreshToken: doc.refreshToken,
expiryDate: doc.expiryDate,
};
}
async saveTokens(ctx: TokenStoreContext, tokens: TokenRecord): Promise<void> {
// Called automatically after OAuth callback AND after every token auto-refresh
await OAuthToken.findOneAndUpdate(
{ tenantId: ctx.tenantId, providerId: ctx.providerId },
{
accessToken: tokens.accessToken, // ← encrypt before saving
refreshToken: tokens.refreshToken,
expiryDate: tokens.expiryDate,
updatedAt: new Date(),
},
{ upsert: true },
);
}
async deleteTokens(ctx: TokenStoreContext): Promise<void> {
// Implement this to support the "Disconnect Google" flow
await OAuthToken.deleteOne({ tenantId: ctx.tenantId, providerId: ctx.providerId });
}
}Step 3 — Set up the provider
// meetingClient.ts
import {
MeetSchedifyClient,
GoogleMeetProvider,
consoleLogger,
} from 'meetschedify';
import { GoogleTokenStore } from './store/GoogleTokenStore';
export const meetingClient = new MeetSchedifyClient({
providers: [
new GoogleMeetProvider(
{
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectUri: process.env.GOOGLE_REDIRECT_URI!,
logger: consoleLogger,
},
new GoogleTokenStore()
),
],
});Step 4 — Add OAuth connect routes (Express example)
// src/routes/auth.ts
import { Router } from 'express';
import { meetingClient } from '../meetingClient';
import { MeetSchedifyError, ErrorCodes } from 'meetschedify';
const router = Router();
// Step A: User clicks "Connect Google Calendar" → redirect them here
router.get('/auth/google/connect', async (req, res) => {
const tenantId = (req as any).user.tenantId; // from your session or JWT
const { url } = await meetingClient.getAuthorizationUrl({
tenantId,
providerId: 'google-meet',
returnUrl: '/dashboard', // where to send the user after connecting
});
res.redirect(url);
});
// Step B: Google redirects back here after user approves
router.get('/auth/google/callback', async (req, res) => {
const { code, state } = req.query as { code: string; state: string };
try {
// Exchanges code for tokens and saves them via your GoogleTokenStore
await meetingClient.handleOAuthCallback('google-meet', { code, state });
res.redirect('/dashboard?connected=true');
} catch (err) {
res.redirect('/dashboard?error=google_connect_failed');
}
});
// Step C: User disconnects Google Calendar
router.post('/auth/google/disconnect', async (req, res) => {
const tenantId = (req as any).user.tenantId;
await meetingClient.disconnectProvider(tenantId, 'google-meet');
res.json({ disconnected: true });
});
export default router;Step 5 — Check connection and schedule
// Before creating any meeting, check if the user is connected
const status = await meetingClient.getConnectionStatus(tenantId, 'google-meet');
if (!status.connected) {
// Redirect user to /auth/google/connect
return res.status(401).json({
error: 'Google Calendar not connected.',
action: '/auth/google/connect',
});
}
// Once connected, create meetings exactly like Type 1
const meeting = await meetingClient.createMeeting({
tenantId, // must match the tenantId used during OAuth connect
providerId: 'google-meet',
title: 'Product Demo — Acme Corp',
time: {
start: '2026-07-15T14:00:00',
end: '2026-07-15T15:00:00',
timezone: 'America/New_York',
},
primaryParticipant: { email: '[email protected]' },
additionalParticipants: [{ email: '[email protected]' }],
});
console.log(meeting.conferenceLink); // Google Meet linkType 3 — Zoom (Server-to-Server OAuth)
Best for: any backend that needs Zoom meetings with zero user login. Uses Zoom's machine-to-machine OAuth — no OAuth redirect needed.
Works great for: hospitals, schools, tech interviews, legal/finance, SaaS platforms.
Step 1 — Create a Zoom Server-to-Server OAuth app
- Go to marketplace.zoom.us
- Click Develop → Build App
- Choose Server-to-Server OAuth → click Create
- Give the app a name (e.g. "MyApp Meeting Scheduler")
- Go to Scopes → add
meeting:write:meeting - Go to Activation → click Activate your app
- Copy Account ID, Client ID, and Client Secret
Step 2 — Add environment variables
# .env
ZOOM_ACCOUNT_ID=AbCdEfGhIjK1234567890
ZOOM_CLIENT_ID=your_zoom_client_id
ZOOM_CLIENT_SECRET=your_zoom_client_secretStep 3 — Set up the provider
// meetingClient.ts
import {
MeetSchedifyClient,
ZoomProvider,
consoleLogger,
} from 'meetschedify';
export const meetingClient = new MeetSchedifyClient({
providers: [
// Option A — fromEnv() shortcut (reads ZOOM_ACCOUNT_ID, ZOOM_CLIENT_ID, ZOOM_CLIENT_SECRET)
ZoomProvider.fromEnv({ logger: consoleLogger }),
// Option B — manual config with org-specific defaults
// new ZoomProvider({
// accountId: process.env.ZOOM_ACCOUNT_ID!,
// clientId: process.env.ZOOM_CLIENT_ID!,
// clientSecret: process.env.ZOOM_CLIENT_SECRET!,
// meetingDefaults: {
// waitingRoom: true, // great for healthcare / legal
// joinBeforeHost: false,
// muteUponEntry: false,
// hostVideo: true,
// },
// retry: { maxAttempts: 4 }, // extra resilience
// logger: consoleLogger,
// }),
],
});Step 4 — Schedule a Zoom meeting
import { meetingClient } from './meetingClient';
async function createZoomConsultation(doctor: Doctor, patient: Patient) {
const meeting = await meetingClient.createMeeting({
tenantId: `hospital_ward_${doctor.wardId}`,
providerId: 'zoom',
title: `Consultation — Dr. ${doctor.name} × ${patient.name}`,
time: {
start: '2026-08-01T09:00:00',
end: '2026-08-01T09:30:00',
timezone: 'America/Chicago',
},
primaryParticipant: { email: patient.email, displayName: patient.name },
additionalParticipants: [{ email: doctor.email, displayName: `Dr. ${doctor.name}` }],
addConferenceLink: true,
});
return {
joinUrl: meeting.conferenceLink, // https://us05web.zoom.us/j/12345...
meetingId: meeting.providerEventId, // store for cancellation
};
}Step 5 — Cancel a Zoom meeting
// meetingId = providerEventId from when you called createMeeting()
await meetingClient.cancelMeeting('zoom', storedMeetingId, {
tenantId: 'hospital_ward_4',
});Using all three providers together
// meetingClient.ts — register all providers at startup
import {
MeetSchedifyClient,
GoogleMeetStaticProvider,
GoogleMeetProvider,
ZoomProvider,
consoleLogger,
} from 'meetschedify';
import { GoogleTokenStore } from './store/GoogleTokenStore';
export const meetingClient = new MeetSchedifyClient({
providers: [
GoogleMeetStaticProvider.fromEnv({ logger: consoleLogger }),
new GoogleMeetProvider(
{ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, redirectUri: process.env.GOOGLE_REDIRECT_URI! },
new GoogleTokenStore()
),
ZoomProvider.fromEnv({ logger: consoleLogger }),
],
});
// List all registered providers
console.log(meetingClient.listProviders());
// → ["google-meet-static", "google-meet", "zoom"]
// Route to the right one based on org/user preference
const meeting = await meetingClient.createMeeting({
tenantId: user.orgId,
providerId: user.preferredProvider, // 'google-meet-static' | 'google-meet' | 'zoom'
title: 'Team Standup',
time: { start: '...', end: '...', timezone: 'Asia/Dubai' },
});ProviderIds — type-safe constants
Use ProviderIds instead of raw strings to get IDE autocomplete:
import { ProviderIds } from 'meetschedify';
await meetingClient.createMeeting({
providerId: ProviderIds.ZOOM, // 'zoom'
// providerId: ProviderIds.GOOGLE_MEET, // 'google-meet'
// providerId: ProviderIds.GOOGLE_MEET_STATIC // 'google-meet-static'
// ...
});Error handling
Every SDK error is an instance of MeetSchedifyError. Use ErrorCodes for reliable, refactor-safe matching:
import { MeetSchedifyError, GoogleMeetError, ZoomError, ErrorCodes } from 'meetschedify';
try {
const meeting = await meetingClient.createMeeting({ ... });
return res.json({ meetLink: meeting.conferenceLink });
} catch (err) {
if (!(err instanceof MeetSchedifyError)) throw err; // re-throw non-SDK errors
switch (err.code) {
// ── Google Meet ──────────────────────────────────────────────────────────
case ErrorCodes.GOOGLE_NOT_CONNECTED:
// User hasn't connected their Google account yet
return res.status(401).json({
error: 'Google Calendar not connected.',
action: { redirect: '/auth/google/connect' },
});
case ErrorCodes.GOOGLE_AUTH_REVOKED:
// Refresh token was revoked by the user — they must re-authenticate
return res.status(401).json({
error: 'Google authorization was revoked. Please reconnect.',
action: { redirect: '/auth/google/connect' },
});
case ErrorCodes.GOOGLE_RATE_LIMIT:
return res.status(429).json({ error: 'Too many calendar requests. Try again shortly.' });
// ── Zoom ─────────────────────────────────────────────────────────────────
case ErrorCodes.ZOOM_INVALID_CLIENT:
// Zoom credentials are wrong or the app is deactivated → alert your ops team
console.error('[OPS ALERT] Zoom integration broken:', err.message);
return res.status(503).json({ error: 'Video conferencing temporarily unavailable.' });
case ErrorCodes.ZOOM_RATE_LIMIT:
return res.status(429).json({ error: 'Zoom API rate limit. Retry after 30 seconds.' });
case ErrorCodes.ZOOM_NETWORK_ERROR:
return res.status(502).json({ error: 'Could not reach Zoom. Try again.' });
// ── General ──────────────────────────────────────────────────────────────
case ErrorCodes.PROVIDER_NOT_FOUND:
return res.status(400).json({ error: `Unknown provider: ${err.providerId}` });
default:
console.error('Unhandled meeting error:', err.code, err.message);
return res.status(500).json({ error: 'Failed to schedule meeting.' });
}
}Accessing provider-specific error details
if (err instanceof GoogleMeetError) {
err.httpStatus; // 401 | 403 | 429 | ...
err.googleApiMessage; // raw Google API error text
}
if (err instanceof ZoomError) {
err.httpStatus; // 401 | 403 | 429 | ...
err.zoomErrorCode; // e.g. "invalid_client"
err.zoomBody; // full Zoom response body (for debugging)
}Logging
import { consoleLogger, silentLogger, MeetSchedifyLogger } from 'meetschedify';
// Built-in: console output (great for development)
new ZoomProvider({ ..., logger: consoleLogger });
// Built-in: no output (great for tests)
new ZoomProvider({ ..., logger: silentLogger });
// pino adapter
import pino from 'pino';
const raw = pino();
const logger: MeetSchedifyLogger = {
info: (msg, meta) => raw.info(meta ?? {}, msg),
warn: (msg, meta) => raw.warn(meta ?? {}, msg),
error: (msg, meta) => raw.error(meta ?? {}, msg),
debug: (msg, meta) => raw.debug(meta ?? {}, msg),
};
// winston adapter
import winston from 'winston';
const raw = winston.createLogger({ ... });
const logger: MeetSchedifyLogger = {
info: (msg, meta) => raw.info(msg, meta),
warn: (msg, meta) => raw.warn(msg, meta),
error: (msg, meta) => raw.error(msg, meta),
};Retry & Timeout (Zoom)
const provider = new ZoomProvider({
accountId: process.env.ZOOM_ACCOUNT_ID!,
clientId: process.env.ZOOM_CLIENT_ID!,
clientSecret: process.env.ZOOM_CLIENT_SECRET!,
requestTimeoutMs: 20_000, // abort requests after 20s (default: 15s)
retry: {
maxAttempts: 4, // total attempts (default: 3)
initialDelayMs: 500, // delay before retry 2 (default: 500ms)
maxDelayMs: 10_000, // cap at 10s (default: 8s)
factor: 2, // exponential multiplier (default: 2)
},
});Retries automatically on: network errors, 429 rate limits, 500/502/503/504 server errors. Does not retry: 400 bad request, 401 invalid credentials, 403 forbidden.
Industry-specific Zoom configurations
// Healthcare — HIPAA-aligned (waiting room, no early join)
new ZoomProvider({
accountId: '...', clientId: '...', clientSecret: '...',
meetingDefaults: {
waitingRoom: true, // doctor admits each patient individually
joinBeforeHost: false, // patient cannot enter before doctor
muteUponEntry: false,
hostVideo: true,
participantVideo: false,
},
});
// Education — online classroom (muted entry, students gather early)
new ZoomProvider({
accountId: '...', clientId: '...', clientSecret: '...',
meetingDefaults: {
muteUponEntry: true, // students muted when joining
joinBeforeHost: true, // students can gather before teacher joins
waitingRoom: false,
hostVideo: true,
participantVideo: false,
},
});
// Recruiting — technical interview (both cameras on)
new ZoomProvider({
accountId: '...', clientId: '...', clientSecret: '...',
meetingDefaults: {
hostVideo: true,
participantVideo: true, // candidate should be on camera
waitingRoom: true, // interviewer controls session start
joinBeforeHost: false,
},
});Full API reference
MeetSchedifyClient methods
| Method | Returns | Description |
|--------|---------|-------------|
| listProviders() | ProviderId[] | List all registered provider IDs |
| getConnectionStatus(tenantId, providerId) | Promise<ProviderConnectionStatus> | Check if a tenant is connected |
| getAuthorizationUrl(options) | Promise<AuthUrlResult> | Get OAuth URL — Type 2 only |
| handleOAuthCallback(providerId, payload) | Promise<OAuthCallbackResult> | Exchange OAuth code — Type 2 only |
| createMeeting(request) | Promise<CreatedMeeting> | Create a meeting |
| cancelMeeting(providerId, meetingId, ctx) | Promise<void> | Cancel a meeting |
| disconnectProvider(tenantId, providerId) | Promise<void> | Revoke + delete tokens — Type 2 only |
CreateMeetingRequest fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| tenantId | string | ✅ | Your stable tenant/org identifier |
| providerId | ProviderId | ✅ | 'google-meet-static' | 'google-meet' | 'zoom' |
| title | string | ✅ | Meeting title |
| time.start | string | ✅ | ISO 8601 datetime |
| time.end | string | ✅ | ISO 8601 datetime |
| time.timezone | string | — | IANA timezone (default: "UTC") |
| description | string | — | Meeting description |
| primaryParticipant | MeetingParticipant | — | Main attendee (To: line) |
| additionalParticipants | MeetingParticipant[] | — | Extra attendees |
| addConferenceLink | boolean | — | Attach video link (default: true) |
| metadata | Record<string, unknown> | — | Your data, passed through unchanged |
CreatedMeeting fields
| Field | Type | Description |
|-------|------|-------------|
| providerId | ProviderId | Which provider created this |
| providerEventId | string | Google Calendar event ID or Zoom meeting ID — store for cancel |
| conferenceLink | string \| null | The join URL — show this to participants |
| calendarUrl | string \| null | Google Calendar event page URL (null for Zoom) |
| conferenceLinks | MeetingLink[] | All entry points (video, phone, SIP) |
| time | MeetingTimeRange | Start, end, timezone |
| location | MeetingLocation | { type: 'online', value: <join-url> } |
| metadata | Record<string, unknown> | Your metadata, passed back unchanged |
| raw | unknown | Full provider API response for advanced use |
ErrorCodes — all values
import { ErrorCodes } from 'meetschedify';
// Google Meet
ErrorCodes.GOOGLE_NOT_CONNECTED // No tokens found — redirect to OAuth
ErrorCodes.GOOGLE_TOKEN_REFRESH_FAILED // Token refresh failed
ErrorCodes.GOOGLE_AUTH_REVOKED // Refresh token revoked — re-auth required
ErrorCodes.GOOGLE_CALENDAR_ERROR // Calendar API error
ErrorCodes.GOOGLE_RATE_LIMIT // Rate limit exceeded
// Zoom
ErrorCodes.ZOOM_INVALID_CLIENT // Wrong credentials or inactive app
ErrorCodes.ZOOM_TOKEN_ERROR // Could not get access token
ErrorCodes.ZOOM_MEETING_ERROR // Meeting create/cancel failed
ErrorCodes.ZOOM_RATE_LIMIT // Rate limit hit
ErrorCodes.ZOOM_NETWORK_ERROR // Network or timeout failure
ErrorCodes.ZOOM_MAX_RETRIES_EXCEEDED // All retries failed
// General
ErrorCodes.PROVIDER_NOT_FOUND // Provider not registered in client
ErrorCodes.INVALID_CONFIG // Bad provider configuration
ErrorCodes.OPERATION_NOT_SUPPORTED // Provider doesn't support this operationSecurity guidelines
- Never commit refresh tokens, client secrets, or Zoom credentials to source control.
- Use a secrets manager: AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, Azure Key Vault, or
.envfiles with dotenv-vault. - Implement
TokenStore.saveTokens()with encryption at rest — refresh tokens are long-lived credentials. - Scope
tenantIdin your TokenStore — ensure tenant A can never retrieve tenant B's tokens. - The
loggercallbacks only receive sanitized metadata — raw tokens are never passed to it. - Revoke unused Google refresh tokens at: Google Account → Security → Third-party apps.
- Zoom Server-to-Server tokens expire in ~1 hour — the SDK refreshes them automatically.
Changelog
v0.3.0
- Typed error system —
MeetSchedifyError,GoogleMeetError,ZoomError,ErrorCodes - Structured logger —
MeetSchedifyLoggerinterface +consoleLogger+silentLogger - Retry with exponential back-off —
ZoomProviderretries on network/429/5xx - Request timeout —
ZoomProviderrequestTimeoutMsviaAbortController ZoomMeetingDefaults— waiting room, mute-on-entry, host video, per-org defaultsfromEnv()factory —GoogleMeetStaticProvider.fromEnv()andZoomProvider.fromEnv()ProviderIdsconstants — type-safe provider ID objectMeetSchedifyClient.listProviders()— see all registered providersMeetSchedifyClient.cancelMeeting()— cancel via any providerMeetSchedifyClient.disconnectProvider()— revoke OAuth + clear TokenStoreTokenStore.deleteTokens()— optional disconnect supportwithRetry()utility — exported for custom providersmetadataround-trip — pass data throughcreateMeeting, get it back in resultcalendarIdconfig — use any Google Calendar, not just "primary"
v0.2.0
- Added
GoogleMeetStaticProvider(static refresh token, no per-user login) - Added
ZoomProvider(Zoom Server-to-Server OAuth)
v0.1.2
- Initial release:
GoogleMeetProviderwith full per-user OAuth and TokenStore
License
MIT © Raj Kumar Singha
