From 8785c3e09daae2a723f3331a3c19972bbbba0e28 Mon Sep 17 00:00:00 2001 From: Matt Brooker Date: Mon, 20 Apr 2026 11:13:04 -0400 Subject: [PATCH] feat: switch to CIMD metadata URL as client_id for multi-region support Uses the canonical CIMD metadata URL (https://us.posthog.com/api/oauth/wizard/client-metadata) as client_id instead of a static string. When EU receives this URL, it auto-creates the OAuthApplication by fetching the metadata - no manual admin setup needed. Also makes provisioning region-aware so EU requests go directly to eu.posthog.com. Depends on PostHog/posthog#55286 (CIMD provisioning auth support). Closes #401 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/utils/__tests__/provisioning.test.ts | 18 ++++++++++++-- src/utils/provisioning.ts | 30 ++++++++++++++---------- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/utils/__tests__/provisioning.test.ts b/src/utils/__tests__/provisioning.test.ts index da640834..f3d39089 100644 --- a/src/utils/__tests__/provisioning.test.ts +++ b/src/utils/__tests__/provisioning.test.ts @@ -6,6 +6,11 @@ jest.mock('../debug', () => ({ logToFile: jest.fn() })); jest.mock('../analytics', () => ({ analytics: { captureException: jest.fn() }, })); +jest.mock('../../lib/constants', () => ({ + ...jest.requireActual('../../lib/constants'), + IS_DEV: false, + WIZARD_USER_AGENT: 'posthog-wizard/test', +})); const mockedAxios = axios as jest.Mocked; @@ -88,7 +93,9 @@ describe('provisionNewAccount', () => { expect( (accountCall[1] as Record).code_challenge, ).toBeTruthy(); - expect((accountCall[1] as Record).client_id).toBeTruthy(); + expect((accountCall[1] as Record).client_id).toBe( + 'https://us.posthog.com/api/oauth/wizard/client-metadata', + ); // Verify token exchange includes code_verifier const tokenCall = mockedAxios.post.mock.calls[1]; @@ -158,7 +165,7 @@ describe('provisionNewAccount', () => { ); }); - it('sends correct region parameter', async () => { + it('routes EU requests to eu.posthog.com', async () => { mockedAxios.post .mockResolvedValueOnce({ data: { id: 'req_5', type: 'oauth', oauth: { code: 'code_5' } }, @@ -188,10 +195,17 @@ describe('provisionNewAccount', () => { const result = await provisionNewAccount('eu@example.com', '', 'EU'); const accountCall = mockedAxios.post.mock.calls[0]; + expect(accountCall[0]).toContain('https://eu.posthog.com'); expect((accountCall[1] as Record).configuration).toEqual({ region: 'EU', }); expect(result.host).toBe('https://eu.posthog.com'); + + const tokenCall = mockedAxios.post.mock.calls[1]; + expect(tokenCall[0]).toContain('https://eu.posthog.com'); + + const resourceCall = mockedAxios.post.mock.calls[2]; + expect(resourceCall[0]).toContain('https://eu.posthog.com'); }); it('sends project name in resources configuration', async () => { diff --git a/src/utils/provisioning.ts b/src/utils/provisioning.ts index a2330c8f..d7e9794b 100644 --- a/src/utils/provisioning.ts +++ b/src/utils/provisioning.ts @@ -10,21 +10,23 @@ import * as crypto from 'node:crypto'; import axios from 'axios'; import { z } from 'zod'; -import { - IS_DEV, - POSTHOG_DEV_CLIENT_ID, - POSTHOG_US_CLIENT_ID, - WIZARD_USER_AGENT, -} from '../lib/constants'; +import { IS_DEV, WIZARD_USER_AGENT } from '../lib/constants'; import { logToFile } from './debug'; import { analytics } from './analytics'; -const WIZARD_CLIENT_ID = IS_DEV ? POSTHOG_DEV_CLIENT_ID : POSTHOG_US_CLIENT_ID; +const WIZARD_CLIENT_ID = + 'https://us.posthog.com/api/oauth/wizard/client-metadata'; const API_VERSION = '0.1d'; -const PROVISIONING_BASE_URL = IS_DEV - ? 'http://localhost:8010' - : 'https://us.posthog.com'; +const REGION_URLS: Record = { + US: 'https://us.posthog.com', + EU: 'https://eu.posthog.com', +}; + +function getBaseUrl(region: 'US' | 'EU'): string { + if (IS_DEV) return 'http://localhost:8010'; + return REGION_URLS[region]; +} function generateCodeVerifier(): string { return crypto.randomBytes(32).toString('base64url'); @@ -105,11 +107,13 @@ export async function provisionNewAccount( const codeVerifier = generateCodeVerifier(); const codeChallenge = generateCodeChallenge(codeVerifier); + const baseUrl = getBaseUrl(region); + logToFile('[provisioning] starting account creation'); // Step 1: Create account const accountRes = await axios.post( - `${PROVISIONING_BASE_URL}/api/agentic/provisioning/account_requests`, + `${baseUrl}/api/agentic/provisioning/account_requests`, { id: crypto.randomUUID(), email, @@ -158,7 +162,7 @@ export async function provisionNewAccount( // Step 2: Exchange code for tokens const tokenRes = await axios.post( - `${PROVISIONING_BASE_URL}/api/agentic/oauth/token`, + `${baseUrl}/api/agentic/oauth/token`, new URLSearchParams({ grant_type: 'authorization_code', code, @@ -180,7 +184,7 @@ export async function provisionNewAccount( // Step 3: Provision resources const resourceRes = await axios.post( - `${PROVISIONING_BASE_URL}/api/agentic/provisioning/resources`, + `${baseUrl}/api/agentic/provisioning/resources`, { service_id: 'analytics', ...(opts?.projectName