Twenty sdk cli oauth (#18638)

<img width="1418" height="804" alt="image"
src="https://github.com/user-attachments/assets/de6c8222-6496-4a71-bc21-7e5e1269d5cb"
/>

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
martmull
2026-03-17 11:43:17 +01:00
committed by GitHub
parent 111debc1ce
commit 731e297147
96 changed files with 3191 additions and 2453 deletions
@@ -4,7 +4,6 @@ import { ConfigService } from '@/cli/utilities/config/config-service';
import { type OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
import { BuildManifestOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/build-manifest-orchestrator-step';
import { CheckServerOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/check-server-orchestrator-step';
import { EnsureValidTokensOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/ensure-valid-tokens-orchestrator-step';
import { GenerateApiClientOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/generate-api-client-orchestrator-step';
import { RegisterAppOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/register-app-orchestrator-step';
import {
@@ -33,7 +32,6 @@ export class DevModeOrchestrator {
private clientService: ClientService;
private skipTypecheck = true;
private checkServerStep: CheckServerOrchestratorStep;
private ensureValidTokensStep: EnsureValidTokensOrchestratorStep;
private buildManifestStep: BuildManifestOrchestratorStep;
private registerAppStep: RegisterAppOrchestratorStep;
private uploadFilesStep: UploadFilesOrchestratorStep;
@@ -55,11 +53,6 @@ export class DevModeOrchestrator {
...stepDeps,
apiService,
});
this.ensureValidTokensStep = new EnsureValidTokensOrchestratorStep({
...stepDeps,
apiService,
configService,
});
this.buildManifestStep = new BuildManifestOrchestratorStep(stepDeps);
this.registerAppStep = new RegisterAppOrchestratorStep({
...stepDeps,
@@ -167,10 +160,6 @@ export class DevModeOrchestrator {
return;
}
await this.ensureValidTokensStep.execute({
applicationId: this.state.steps.resolveApplication.output.applicationId,
});
const buildResult = await this.buildManifestStep.execute({
appPath: this.state.appPath,
});
@@ -244,10 +233,6 @@ export class DevModeOrchestrator {
{ message: 'Application created', status: 'success' },
]);
await this.ensureValidTokensStep.exchangeTokens({
applicationId: createResult.data.id,
});
this.uploadFilesStep.initialize({
appPath: this.state.appPath,
universalIdentifier: manifest.application.universalIdentifier,
@@ -34,7 +34,17 @@ export class CheckServerOrchestratorStep {
step.output = { isReady: false, errorLogged: true };
step.status = 'error';
this.state.applyStepEvents([
{ message: 'Cannot reach server', status: 'error' },
{
message:
'Cannot reach Twenty at localhost:3000.\n\n' +
' Start a local server with Docker:\n' +
' curl -sL https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/docker-compose.yml -o docker-compose.yml\n' +
' docker compose up -d\n\n' +
' Or from the monorepo:\n' +
' yarn start\n\n' +
' Waiting for server...',
status: 'error',
},
]);
this.state.updatePipeline({ status: 'error' });
}
@@ -47,7 +57,11 @@ export class CheckServerOrchestratorStep {
step.output = { isReady: false, errorLogged: true };
step.status = 'error';
this.state.applyStepEvents([
{ message: 'Authentication failed', status: 'error' },
{
message:
'Authentication failed. Run `twenty remote add --local` to authenticate.',
status: 'error',
},
]);
this.state.updatePipeline({ status: 'error' });
}
@@ -1,134 +0,0 @@
import { type ApiService } from '@/cli/utilities/api/api-service';
import { type ConfigService } from '@/cli/utilities/config/config-service';
import { type OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
export class EnsureValidTokensOrchestratorStep {
private apiService: ApiService;
private configService: ConfigService;
private state: OrchestratorState;
private notify: () => void;
constructor({
apiService,
configService,
state,
notify,
}: {
apiService: ApiService;
configService: ConfigService;
state: OrchestratorState;
notify: () => void;
}) {
this.apiService = apiService;
this.configService = configService;
this.state = state;
this.notify = notify;
}
async execute(input: { applicationId: string | null }): Promise<void> {
if (!input.applicationId) {
return;
}
const step = this.state.steps.ensureValidTokens;
step.status = 'in_progress';
this.notify();
const config = await this.configService.getConfig();
if (
config.applicationAccessToken &&
!this.isTokenExpired(config.applicationAccessToken)
) {
step.status = 'done';
this.notify();
return;
}
if (
config.applicationRefreshToken &&
!this.isTokenExpired(config.applicationRefreshToken)
) {
const renewResult = await this.apiService.renewApplicationToken(
config.applicationRefreshToken,
);
if (renewResult.success) {
await this.configService.setConfig({
applicationAccessToken: renewResult.data.applicationAccessToken.token,
applicationRefreshToken:
renewResult.data.applicationRefreshToken.token,
});
this.state.applyStepEvents([
{ message: 'Renewing application tokens', status: 'info' },
{ message: 'Application tokens renewed', status: 'success' },
]);
step.status = 'done';
this.notify();
return;
}
this.state.applyStepEvents([
{ message: 'Renewing application tokens', status: 'info' },
{
message: `Failed to renew application tokens: ${JSON.stringify(renewResult.error, null, 2)}`,
status: 'error',
},
]);
await this.exchangeTokens({ applicationId: input.applicationId });
return;
}
await this.exchangeTokens({ applicationId: input.applicationId });
}
async exchangeTokens(input: { applicationId: string }): Promise<void> {
const tokenResult = await this.apiService.generateApplicationToken(
input.applicationId,
);
if (!tokenResult.success) {
this.state.applyStepEvents([
{ message: 'Generating application tokens', status: 'info' },
{
message: `Failed to generate application tokens: ${JSON.stringify(tokenResult.error, null, 2)}`,
status: 'error',
},
]);
this.state.steps.ensureValidTokens.status = 'error';
this.notify();
return;
}
await this.configService.setConfig({
applicationAccessToken: tokenResult.data.applicationAccessToken.token,
applicationRefreshToken: tokenResult.data.applicationRefreshToken.token,
});
this.state.applyStepEvents([
{ message: 'Generating application tokens', status: 'info' },
{ message: 'Application tokens stored in config', status: 'success' },
]);
this.state.steps.ensureValidTokens.status = 'done';
this.notify();
}
private isTokenExpired(token: string): boolean {
try {
const payload = JSON.parse(
Buffer.from(token.split('.')[1], 'base64').toString(),
);
return Date.now() >= payload.exp * 1000 - 60_000;
} catch {
return true;
}
}
}
@@ -36,7 +36,7 @@ export class GenerateApiClientOrchestratorStep {
await this.clientService.generateCoreClient({
appPath: input.appPath,
authToken: config.applicationAccessToken,
authToken: config.accessToken,
});
step.status = 'done';
@@ -88,7 +88,6 @@ export class RegisterAppOrchestratorStep {
await this.configService.setConfig({
oauthClientId: createResult.data.applicationRegistration.oAuthClientId,
oauthClientSecret: createResult.data.clientSecret,
});
this.state.applyStepEvents([