Skip to main content
Glama
whoop-oauth.ts5.59 kB
#!/usr/bin/env tsx /** * Whoop OAuth Setup Script * * This script helps you obtain initial Whoop OAuth tokens. * Run with: npm run whoop:auth * * Requirements: * - WHOOP_CLIENT_ID and WHOOP_CLIENT_SECRET in environment * - WHOOP_REDIRECT_URI (defaults to http://localhost:3000/callback) * - REDIS_URL for storing tokens */ import * as readline from 'readline'; import { storeWhoopTokens, getRedisClient, closeRedis } from '../utils/redis.js'; const WHOOP_AUTH_BASE = 'https://api.prod.whoop.com/oauth/oauth2'; interface TokenResponse { access_token: string; refresh_token: string; expires_in: number; token_type: string; } async function main() { console.log('\n🏋️ Whoop OAuth Setup\n'); // Check required environment variables const clientId = process.env.WHOOP_CLIENT_ID; const clientSecret = process.env.WHOOP_CLIENT_SECRET; const redirectUri = process.env.WHOOP_REDIRECT_URI ?? 'http://localhost:3000/callback'; const redisUrl = process.env.REDIS_URL; if (!clientId || !clientSecret) { console.error('❌ Missing required environment variables:'); if (!clientId) console.error(' - WHOOP_CLIENT_ID'); if (!clientSecret) console.error(' - WHOOP_CLIENT_SECRET'); console.error('\nSet these in your .env file and try again.'); process.exit(1); } if (!redisUrl) { console.error('❌ REDIS_URL is required to store tokens.'); console.error(' Start Redis and set REDIS_URL in your .env file.'); process.exit(1); } // Verify Redis connection try { const client = await getRedisClient(); if (!client) { throw new Error('Failed to connect to Redis'); } console.log('✅ Connected to Redis\n'); } catch (error) { console.error('❌ Failed to connect to Redis:', error); process.exit(1); } // Build authorization URL const { url: authUrl, state } = buildAuthorizationUrl(clientId, redirectUri); console.log('Step 1: Open this URL in your browser to authorize:\n'); console.log(` ${authUrl}\n`); console.log('Step 2: After authorizing, you\'ll be redirected to:'); console.log(` ${redirectUri}?code=AUTHORIZATION_CODE&state=${state}\n`); console.log('Step 3: Copy the authorization code from the URL (the "code" parameter).\n'); // Prompt for authorization code const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const code = await new Promise<string>((resolve) => { rl.question('Enter the authorization code: ', (answer) => { rl.close(); resolve(answer.trim()); }); }); if (!code) { console.error('❌ No authorization code provided.'); await closeRedis(); process.exit(1); } // Exchange code for tokens console.log('\n⏳ Exchanging code for tokens...'); try { const tokens = await exchangeCodeForTokens( code, clientId, clientSecret, redirectUri ); // Validate token response if (!tokens.access_token || !tokens.refresh_token) { console.error('\n❌ Invalid token response from Whoop API'); console.error(' Response:', JSON.stringify(tokens, null, 2)); await closeRedis(); process.exit(1); } // Store tokens in Redis const expiresAt = Date.now() + tokens.expires_in * 1000; const stored = await storeWhoopTokens({ accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresAt, }); if (!stored) { console.error('\n❌ Failed to store tokens in Redis'); await closeRedis(); process.exit(1); } console.log('\n✅ Success! Tokens have been stored in Redis.'); console.log('\n Access token expires in:', Math.round(tokens.expires_in / 60), 'minutes'); console.log(' Refresh token stored permanently.'); console.log('\n You can now start the server - it will use these tokens automatically.\n'); } catch (error) { console.error('\n❌ Failed to exchange code for tokens:', error); await closeRedis(); process.exit(1); } await closeRedis(); } function generateState(): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let state = ''; for (let i = 0; i < 32; i++) { state += chars.charAt(Math.floor(Math.random() * chars.length)); } return state; } function buildAuthorizationUrl(clientId: string, redirectUri: string): { url: string; state: string } { const state = generateState(); const params = new URLSearchParams({ client_id: clientId, redirect_uri: redirectUri, response_type: 'code', scope: 'offline read:recovery read:cycles read:sleep read:workout read:profile read:body_measurement', state, }); return { url: `${WHOOP_AUTH_BASE}/auth?${params.toString()}`, state }; } async function exchangeCodeForTokens( code: string, clientId: string, clientSecret: string, redirectUri: string ): Promise<TokenResponse> { const response = await fetch(`${WHOOP_AUTH_BASE}/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'authorization_code', code, client_id: clientId, client_secret: clientSecret, redirect_uri: redirectUri, }), }); if (!response.ok) { const error = await response.text(); throw new Error(`Token exchange failed: ${response.status} - ${error}`); } return response.json() as Promise<TokenResponse>; } main().catch((error) => { console.error('Unexpected error:', error); process.exit(1); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/gesteves/domestique'

If you have feedback or need assistance with the MCP directory API, please join our Discord server