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([