Use app's own OAuth credentials for CoreApiClient generation (#19563)
## Summary - **SDK (`dev` & `dev --once`)**: After app registration, the CLI now obtains an `APPLICATION_ACCESS` token via `client_credentials` grant using the app's own `clientId`/`clientSecret`, and uses that token for CoreApiClient schema introspection — instead of the user's `config.accessToken` which returns the full unscoped schema. - **Config**: `oauthClientSecret` is now persisted alongside `oauthClientId` in `~/.twenty/config.json` when creating a new app registration, so subsequent `dev`/`dev --once` runs can obtain fresh app tokens without re-registration. - **CI action**: `spawn-twenty-app-dev-test` now outputs a proper `API_KEY` JWT (signed with the seeded dev workspace secret) instead of the previous hardcoded `ACCESS` token — giving consumers a real API key rather than a user session token. ## Motivation When developing Twenty apps, `yarn twenty dev` was using the CLI user's OAuth token for GraphQL schema introspection during CoreApiClient generation. This token (type `ACCESS`) has no `applicationId` claim, so the server returns the **full workspace schema** — including all objects — rather than the scoped schema the app should see at runtime (filtered by `applicationId`). This caused a discrepancy: the generated CoreApiClient contained fields the app couldn't actually query at runtime with its `APPLICATION_ACCESS` token. By switching to `client_credentials` grant, the SDK now introspects with the same token type the app will use in production, ensuring the generated client accurately reflects the app's runtime capabilities.
This commit is contained in:
@@ -115,23 +115,26 @@ export class ApiClient {
|
||||
async refreshToken(): Promise<string | null> {
|
||||
const config = await this.configService.getConfig();
|
||||
|
||||
if (!config.refreshToken || !config.oauthClientId) {
|
||||
if (
|
||||
!config.twentyCLIRefreshToken ||
|
||||
!config.twentyCLIRegistrationClientId
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const tokenResponse = await axios.post(`${config.apiUrl}/oauth/token`, {
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: config.refreshToken,
|
||||
client_id: config.oauthClientId,
|
||||
refresh_token: config.twentyCLIRefreshToken,
|
||||
client_id: config.twentyCLIRegistrationClientId,
|
||||
});
|
||||
|
||||
const { access_token: newAccessToken, refresh_token: newRefreshToken } =
|
||||
tokenResponse.data;
|
||||
|
||||
await this.configService.setConfig({
|
||||
accessToken: newAccessToken,
|
||||
...(newRefreshToken ? { refreshToken: newRefreshToken } : {}),
|
||||
twentyCLIAccessToken: newAccessToken,
|
||||
...(newRefreshToken ? { twentyCLIRefreshToken: newRefreshToken } : {}),
|
||||
});
|
||||
|
||||
return newAccessToken;
|
||||
@@ -146,9 +149,9 @@ export class ApiClient {
|
||||
}
|
||||
|
||||
const config = await this.configService.getConfig();
|
||||
const accessToken = config.accessToken;
|
||||
const cliToken = config.twentyCLIAccessToken;
|
||||
|
||||
if (accessToken && this.isTokenExpired(accessToken)) {
|
||||
if (cliToken && this.isTokenExpired(cliToken)) {
|
||||
const refreshed = await this.refreshToken();
|
||||
|
||||
if (refreshed) {
|
||||
@@ -156,7 +159,7 @@ export class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
return accessToken ?? config.apiKey;
|
||||
return cliToken ?? config.apiKey;
|
||||
}
|
||||
|
||||
private isTokenExpired(token: string): boolean {
|
||||
|
||||
Reference in New Issue
Block a user