From 53065f241f901c174b4e8b808efab22ff11d2dd8 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Sat, 11 Apr 2026 12:26:18 +0200 Subject: [PATCH] Exchange clientSecret for tokens after app registration + bump canary (#19582) ## Summary - **Fix `createApplicationRegistration` flow**: The server's `createApplicationRegistration` mutation returns a `clientSecret`, not `accessToken`/`refreshToken` directly. The SDK now correctly requests `clientSecret` and immediately performs an OAuth `client_credentials` exchange to obtain `appAccessToken` and `appRefreshToken`, then stores them in config. - **New `exchangeCredentialsForTokens` helper**: Shared by both `dev` and `dev --once` flows. Takes `clientId` + `clientSecret`, calls `/oauth/token` with `client_credentials` grant, and persists the resulting tokens. - **Bump `twenty-sdk`, `twenty-client-sdk`, `create-twenty-app` to `1.22.0-canary.2`** ## Context The `1.22.0-canary.1` SDK release expected `createApplicationRegistration` to return `accessToken`/`refreshToken` directly, but the `v1.22.0` server returns `clientSecret`. This caused `yarn twenty dev` and `yarn twenty dev --once` to fail with "No registration found" errors. --- packages/create-twenty-app/package.json | 2 +- packages/twenty-client-sdk/package.json | 2 +- packages/twenty-sdk/package.json | 2 +- .../integration/utils/setup-app-dev-mocks.ts | 7 +++- .../twenty-sdk/src/cli/operations/dev-once.ts | 15 ++++--- .../src/cli/utilities/api/application-api.ts | 6 +-- .../auth/resolve-app-access-token.ts | 42 +++++++++++++++++++ .../steps/register-app-orchestrator-step.ts | 15 ++++--- 8 files changed, 72 insertions(+), 19 deletions(-) diff --git a/packages/create-twenty-app/package.json b/packages/create-twenty-app/package.json index f911e7b47c..ede710b30d 100644 --- a/packages/create-twenty-app/package.json +++ b/packages/create-twenty-app/package.json @@ -1,6 +1,6 @@ { "name": "create-twenty-app", - "version": "1.22.0-canary.1", + "version": "1.22.0-canary.2", "description": "Command-line interface to create Twenty application", "main": "dist/cli.cjs", "bin": "dist/cli.cjs", diff --git a/packages/twenty-client-sdk/package.json b/packages/twenty-client-sdk/package.json index f7de8c3186..29bfaec45f 100644 --- a/packages/twenty-client-sdk/package.json +++ b/packages/twenty-client-sdk/package.json @@ -1,6 +1,6 @@ { "name": "twenty-client-sdk", - "version": "1.22.0-canary.1", + "version": "1.22.0-canary.2", "sideEffects": false, "license": "AGPL-3.0", "scripts": { diff --git a/packages/twenty-sdk/package.json b/packages/twenty-sdk/package.json index 49e8423465..9ccbc7ff44 100644 --- a/packages/twenty-sdk/package.json +++ b/packages/twenty-sdk/package.json @@ -1,6 +1,6 @@ { "name": "twenty-sdk", - "version": "1.22.0-canary.1", + "version": "1.22.0-canary.2", "main": "dist/index.cjs", "module": "dist/index.mjs", "types": "dist/sdk/index.d.ts", diff --git a/packages/twenty-sdk/src/cli/__tests__/integration/utils/setup-app-dev-mocks.ts b/packages/twenty-sdk/src/cli/__tests__/integration/utils/setup-app-dev-mocks.ts index 67f96d6b70..32da37b988 100644 --- a/packages/twenty-sdk/src/cli/__tests__/integration/utils/setup-app-dev-mocks.ts +++ b/packages/twenty-sdk/src/cli/__tests__/integration/utils/setup-app-dev-mocks.ts @@ -20,8 +20,7 @@ const mockApiService = { id: 'mock-registration-id', oAuthClientId: 'mock-client-id', }, - accessToken: 'mock-app-access-token', - refreshToken: 'mock-app-refresh-token', + clientSecret: 'mock-client-secret', }, }), createDevelopmentApplication: vi.fn().mockResolvedValue({ @@ -61,6 +60,10 @@ vi.mock('@/cli/utilities/auth/resolve-app-access-token', () => ({ ensureValidAppAccessTokenOrRefresh: vi .fn() .mockResolvedValue('mock-app-access-token'), + exchangeCredentialsForTokens: vi.fn().mockResolvedValue({ + accessToken: 'mock-app-access-token', + refreshToken: 'mock-app-refresh-token', + }), })); vi.mock('@/cli/utilities/client/client-service', () => ({ diff --git a/packages/twenty-sdk/src/cli/operations/dev-once.ts b/packages/twenty-sdk/src/cli/operations/dev-once.ts index 6167014e0f..26cb56de0b 100644 --- a/packages/twenty-sdk/src/cli/operations/dev-once.ts +++ b/packages/twenty-sdk/src/cli/operations/dev-once.ts @@ -7,7 +7,10 @@ import { runTypecheck } from '@/cli/utilities/build/common/typecheck-plugin'; import { buildAndValidateManifest } from '@/cli/utilities/build/manifest/build-and-validate-manifest'; import { manifestUpdateChecksums } from '@/cli/utilities/build/manifest/manifest-update-checksums'; import { writeManifestToOutput } from '@/cli/utilities/build/manifest/manifest-writer'; -import { ensureValidAppAccessTokenOrRefresh } from '@/cli/utilities/auth/resolve-app-access-token'; +import { + ensureValidAppAccessTokenOrRefresh, + exchangeCredentialsForTokens, +} from '@/cli/utilities/auth/resolve-app-access-token'; import { ClientService } from '@/cli/utilities/client/client-service'; import { ConfigService } from '@/cli/utilities/config/config-service'; import { formatSyncErrorEvents } from '@/cli/utilities/dev/orchestrator/steps/format-sync-error-events'; @@ -140,14 +143,16 @@ const innerAppDevOnce = async ( }; } - const { applicationRegistration, accessToken, refreshToken } = - createResult.data; + const { applicationRegistration, clientSecret } = createResult.data; await configService.setConfig({ appRegistrationId: applicationRegistration.id, appRegistrationClientId: applicationRegistration.oAuthClientId, - appAccessToken: accessToken, - appRefreshToken: refreshToken, + }); + + await exchangeCredentialsForTokens(configService, { + clientId: applicationRegistration.oAuthClientId, + clientSecret, }); } diff --git a/packages/twenty-sdk/src/cli/utilities/api/application-api.ts b/packages/twenty-sdk/src/cli/utilities/api/application-api.ts index 0da089b74f..aac5f1dc39 100644 --- a/packages/twenty-sdk/src/cli/utilities/api/application-api.ts +++ b/packages/twenty-sdk/src/cli/utilities/api/application-api.ts @@ -109,8 +109,7 @@ export class ApplicationApi { universalIdentifier: string; oAuthClientId: string; }; - accessToken: string; - refreshToken: string; + clientSecret: string; }> > { try { @@ -122,8 +121,7 @@ export class ApplicationApi { universalIdentifier oAuthClientId } - accessToken - refreshToken + clientSecret } } `; diff --git a/packages/twenty-sdk/src/cli/utilities/auth/resolve-app-access-token.ts b/packages/twenty-sdk/src/cli/utilities/auth/resolve-app-access-token.ts index 16cd7ab1f8..b7b9bc7107 100644 --- a/packages/twenty-sdk/src/cli/utilities/auth/resolve-app-access-token.ts +++ b/packages/twenty-sdk/src/cli/utilities/auth/resolve-app-access-token.ts @@ -14,6 +14,48 @@ const isTokenExpired = (token: string): boolean => { } }; +/** + * Exchanges an app registration's clientId + clientSecret for access/refresh + * tokens via the OAuth client_credentials grant, then persists them in config. + */ +export const exchangeCredentialsForTokens = async ( + configService: ConfigService, + params: { clientId: string; clientSecret: string }, +): Promise<{ accessToken: string; refreshToken?: string }> => { + const config = await configService.getConfig(); + + const response = await fetch(`${config.apiUrl}/oauth/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + grant_type: 'client_credentials', + client_id: params.clientId, + client_secret: params.clientSecret, + }), + }); + + if (!response.ok) { + throw new Error( + `Token exchange failed: ${response.status} ${response.statusText}`, + ); + } + + const data = (await response.json()) as { + access_token: string; + refresh_token?: string; + }; + + await configService.setConfig({ + appAccessToken: data.access_token, + ...(data.refresh_token ? { appRefreshToken: data.refresh_token } : {}), + }); + + return { + accessToken: data.access_token, + refreshToken: data.refresh_token, + }; +}; + /** * Returns a valid appAccessToken from config, refreshing it first if expired. */ diff --git a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/register-app-orchestrator-step.ts b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/register-app-orchestrator-step.ts index 27a0486850..0b7f8a2c95 100644 --- a/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/register-app-orchestrator-step.ts +++ b/packages/twenty-sdk/src/cli/utilities/dev/orchestrator/steps/register-app-orchestrator-step.ts @@ -1,5 +1,8 @@ import { type ApiService } from '@/cli/utilities/api/api-service'; -import { ensureValidAppAccessTokenOrRefresh } from '@/cli/utilities/auth/resolve-app-access-token'; +import { + ensureValidAppAccessTokenOrRefresh, + exchangeCredentialsForTokens, +} from '@/cli/utilities/auth/resolve-app-access-token'; import { type ConfigService } from '@/cli/utilities/config/config-service'; import { type OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state'; import { type Manifest } from 'twenty-shared/application'; @@ -55,14 +58,16 @@ export class RegisterAppOrchestratorStep { return; } - const { applicationRegistration, accessToken, refreshToken } = - createResult.data; + const { applicationRegistration, clientSecret } = createResult.data; await this.configService.setConfig({ appRegistrationId: applicationRegistration.id, appRegistrationClientId: applicationRegistration.oAuthClientId, - appAccessToken: accessToken, - appRefreshToken: refreshToken, + }); + + await exchangeCredentialsForTokens(this.configService, { + clientId: applicationRegistration.oAuthClientId, + clientSecret, }); this.state.applyStepEvents([