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:
@@ -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,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,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.
|
||||
*/
|
||||
|
||||
+10
-5
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user