UNPKG

@posthog/wizard

Version:

The PostHog wizard helps you to configure your project

250 lines (248 loc) 10.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.performOAuthFlow = performOAuthFlow; const crypto = __importStar(require("node:crypto")); const http = __importStar(require("node:http")); const axios_1 = __importDefault(require("axios")); const chalk_1 = __importDefault(require("chalk")); const opn_1 = __importDefault(require("opn")); const zod_1 = require("zod"); const clack_1 = __importDefault(require("./clack")); const constants_1 = require("../lib/constants"); const clack_utils_1 = require("./clack-utils"); const analytics_1 = require("./analytics"); const urls_1 = require("./urls"); const OAUTH_CALLBACK_STYLES = ` <style> * { font-family: monospace; background-color: #1b0a00; color: #F7A502; font-weight: medium; font-size: 24px; margin: .25rem; } .blink { animation: blink-animation 1s steps(2, start) infinite; } @keyframes blink-animation { to { opacity: 0; } } </style> `; const OAuthTokenResponseSchema = zod_1.z.object({ access_token: zod_1.z.string(), expires_in: zod_1.z.number(), token_type: zod_1.z.string(), scope: zod_1.z.string(), refresh_token: zod_1.z.string(), scoped_teams: zod_1.z.array(zod_1.z.number()).optional(), scoped_organizations: zod_1.z.array(zod_1.z.string()).optional(), }); function generateCodeVerifier() { return crypto.randomBytes(32).toString('base64url'); } function generateCodeChallenge(verifier) { return crypto.createHash('sha256').update(verifier).digest('base64url'); } async function startCallbackServer(authUrl, signupUrl) { return new Promise((resolve, reject) => { let callbackResolve; let callbackReject; const waitForCallback = () => new Promise((res, rej) => { callbackResolve = res; callbackReject = rej; }); const server = http.createServer((req, res) => { if (!req.url) { res.writeHead(400); res.end(); return; } const url = new URL(req.url, `http://localhost:${constants_1.OAUTH_PORT}`); if (url.pathname === '/authorize') { const isSignup = url.searchParams.get('signup') === 'true'; const redirectUrl = isSignup ? signupUrl : authUrl; res.writeHead(302, { Location: redirectUrl }); res.end(); return; } const code = url.searchParams.get('code'); const error = url.searchParams.get('error'); if (error) { const isAccessDenied = error === 'access_denied'; res.writeHead(isAccessDenied ? 200 : 400, { 'Content-Type': 'text/html; charset=utf-8', }); res.end(` <html> <head> <meta charset="UTF-8"> <title>PostHog wizard - Authorization ${isAccessDenied ? 'cancelled' : 'failed'}</title> ${OAUTH_CALLBACK_STYLES} </head> <body> <p>${isAccessDenied ? 'Authorization cancelled.' : `Authorization failed.`}</p> <p>Return to your terminal. This window will close automatically.</p> <script>window.close();</script> </body> </html> `); callbackReject(new Error(`OAuth error: ${error}`)); return; } if (code) { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(` <html> <head> <meta charset="UTF-8"> <title>PostHog wizard is ready</title> ${OAUTH_CALLBACK_STYLES} </head> <body> <p>PostHog login complete!</p> <p>Return to your terminal: the wizard is hard at work on your project<span class="blink">█</span></p> <script>window.close();</script> </body> </html> `); callbackResolve(code); } else { res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(` <html> <head> <meta charset="UTF-8"> <title>PostHog wizard - Invalid request</title> ${OAUTH_CALLBACK_STYLES} </head> <body> <p>Invalid request - no authorization code received.</p> <p>You can close this window.</p> </body> </html> `); } }); server.listen(constants_1.OAUTH_PORT, () => { resolve({ server, waitForCallback }); }); server.on('error', reject); }); } async function exchangeCodeForToken(code, codeVerifier, config) { const cloudUrl = (0, urls_1.getCloudUrlFromRegion)(config.cloudRegion); const response = await axios_1.default.post(`${cloudUrl}/oauth/token`, { grant_type: 'authorization_code', code, redirect_uri: `http://localhost:${constants_1.OAUTH_PORT}/callback`, client_id: (0, urls_1.getOauthClientIdFromRegion)(config.cloudRegion), code_verifier: codeVerifier, }, { headers: { 'Content-Type': 'application/json', }, }); return OAuthTokenResponseSchema.parse(response.data); } async function performOAuthFlow(config) { const cloudUrl = (0, urls_1.getCloudUrlFromRegion)(config.cloudRegion); const codeVerifier = generateCodeVerifier(); const codeChallenge = generateCodeChallenge(codeVerifier); const authUrl = new URL(`${cloudUrl}/oauth/authorize`); authUrl.searchParams.set('client_id', (0, urls_1.getOauthClientIdFromRegion)(config.cloudRegion)); authUrl.searchParams.set('redirect_uri', `http://localhost:${constants_1.OAUTH_PORT}/callback`); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set('code_challenge', codeChallenge); authUrl.searchParams.set('code_challenge_method', 'S256'); authUrl.searchParams.set('scope', config.scopes.join(' ')); authUrl.searchParams.set('required_access_level', 'project'); const signupUrl = new URL(`${cloudUrl}/signup?next=${encodeURIComponent(authUrl.toString())}`); const localSignupUrl = `http://localhost:${constants_1.OAUTH_PORT}/authorize?signup=true`; const localLoginUrl = `http://localhost:${constants_1.OAUTH_PORT}/authorize`; const urlToOpen = config.signup ? localSignupUrl : localLoginUrl; const { server, waitForCallback } = await startCallbackServer(authUrl.toString(), signupUrl.toString()); clack_1.default.log.info(`${chalk_1.default.bold("If the browser window didn't open automatically, please open the following link to be redirected to PostHog:")}\n\n${chalk_1.default.cyan(urlToOpen)}${config.signup ? `\n\nIf you already have an account, you can use this link:\n\n${chalk_1.default.cyan(localLoginUrl)}` : ``}`); if (process.env.NODE_ENV !== 'test') { (0, opn_1.default)(urlToOpen, { wait: false }).catch(() => { // opn throws in environments without a browser }); } const loginSpinner = clack_1.default.spinner(); loginSpinner.start('Waiting for authorization...'); try { const code = await Promise.race([ waitForCallback(), new Promise((_, reject) => setTimeout(() => reject(new Error('Authorization timed out')), 60_000)), ]); const token = await exchangeCodeForToken(code, codeVerifier, config); server.close(); loginSpinner.stop('Authorization complete!'); return token; } catch (e) { loginSpinner.stop('Authorization failed.'); server.close(); const error = e instanceof Error ? e : new Error('Unknown error'); if (error.message.includes('timeout')) { clack_1.default.log.error('Authorization timed out. Please try again.'); } else if (error.message.includes('access_denied')) { clack_1.default.log.info(`${chalk_1.default.yellow('Authorization was cancelled.')}\n\nYou denied access to PostHog. To use the wizard, you need to authorize access to your PostHog account.\n\n${chalk_1.default.dim('You can try again by re-running the wizard.')}`); } else { clack_1.default.log.error(`${chalk_1.default.red('Authorization failed:')}\n\n${error.message}\n\n${chalk_1.default.dim(`If you think this is a bug in the PostHog wizard, please create an issue:\n${constants_1.ISSUES_URL}`)}`); } analytics_1.analytics.captureException(error, { step: 'oauth_flow', cloud_region: config.cloudRegion, }); await (0, clack_utils_1.abort)(); throw error; } } //# sourceMappingURL=oauth.js.map