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.
This commit is contained in:
Charles Bochet
2026-04-11 12:26:18 +02:00
committed by GitHub
parent 300be990b0
commit 53065f241f
8 changed files with 72 additions and 19 deletions
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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": {
+1 -1
View File
@@ -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",
@@ -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', () => ({
@@ -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,
});
}
@@ -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
}
}
`;
@@ -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.
*/
@@ -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([