Clear stale SDK config on uninstall and invalid client (#19608)
## Summary - After a successful `uninstall`, clear all app registration config (`appRegistrationId`, `appRegistrationClientId`, `appAccessToken`, `appRefreshToken`) so the next `dev` run doesn't hit "app not found" errors from leftover credentials - On app re-registration (`ensureAppRegistration`), clear stale `appAccessToken`/`appRefreshToken` that belonged to the previous registration - On OAuth token refresh failure, only clear config when the server returns `invalid_client` (registration was deleted), not on transient failures like network issues - Extract a shared `parse-server-error` utility for consistently parsing both GraphQL error codes (`extensions.code`/`subCode`) and OAuth error responses (`error`/`error_description`)
This commit is contained in:
@@ -16,6 +16,7 @@ const innerAppUninstall = async (
|
||||
ConfigService.setActiveRemote(options.remote);
|
||||
}
|
||||
|
||||
const configService = new ConfigService();
|
||||
const apiService = new ApiService();
|
||||
const manifest = await readManifestFromFile(options.appPath);
|
||||
|
||||
@@ -48,6 +49,13 @@ const innerAppUninstall = async (
|
||||
};
|
||||
}
|
||||
|
||||
await configService.setConfig({
|
||||
appRegistrationId: undefined,
|
||||
appRegistrationClientId: undefined,
|
||||
appAccessToken: undefined,
|
||||
appRefreshToken: undefined,
|
||||
});
|
||||
|
||||
return { success: true, data: undefined };
|
||||
};
|
||||
|
||||
|
||||
+18
@@ -1,4 +1,5 @@
|
||||
import { type ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import { isOAuthInvalidClientError } from '@/cli/utilities/error/parse-server-error';
|
||||
|
||||
import { exchangeCredentialsForTokens } from './exchange-credentials-for-tokens';
|
||||
|
||||
@@ -50,6 +51,23 @@ export const ensureAppAccessTokenIsValidOrRefresh = async (
|
||||
|
||||
return data.access_token;
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await response.json();
|
||||
|
||||
if (isOAuthInvalidClientError(body)) {
|
||||
await configService.setConfig({
|
||||
appRegistrationId: undefined,
|
||||
appRegistrationClientId: undefined,
|
||||
appAccessToken: undefined,
|
||||
appRefreshToken: undefined,
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
} catch {
|
||||
// Non-JSON error response (e.g. proxy 502) — fall through to credential exchange
|
||||
}
|
||||
}
|
||||
|
||||
if (credentials) {
|
||||
|
||||
@@ -1,19 +1,6 @@
|
||||
import { type ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { type ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import { isDefined, isPlainObject } from 'twenty-shared/utils';
|
||||
|
||||
const isAlreadyClaimedError = (error: unknown): boolean => {
|
||||
if (!isPlainObject(error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { extensions } = error as { extensions?: { subCode?: string } };
|
||||
|
||||
return (
|
||||
isDefined(extensions) &&
|
||||
extensions.subCode === 'UNIVERSAL_IDENTIFIER_ALREADY_CLAIMED'
|
||||
);
|
||||
};
|
||||
import { hasGraphQLErrorSubCode } from '@/cli/utilities/error/parse-server-error';
|
||||
|
||||
export const ensureAppRegistration = async (
|
||||
apiService: ApiService,
|
||||
@@ -35,6 +22,8 @@ export const ensureAppRegistration = async (
|
||||
await configService.setConfig({
|
||||
appRegistrationId: applicationRegistration.id,
|
||||
appRegistrationClientId: applicationRegistration.oAuthClientId,
|
||||
appAccessToken: undefined,
|
||||
appRefreshToken: undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -44,7 +33,12 @@ export const ensureAppRegistration = async (
|
||||
};
|
||||
}
|
||||
|
||||
if (!isAlreadyClaimedError(createResult.error)) {
|
||||
const isAlreadyClaimed = hasGraphQLErrorSubCode(
|
||||
createResult.error,
|
||||
'UNIVERSAL_IDENTIFIER_ALREADY_CLAIMED',
|
||||
);
|
||||
|
||||
if (!isAlreadyClaimed) {
|
||||
const errorDetail =
|
||||
createResult.error instanceof Error
|
||||
? createResult.error.message
|
||||
@@ -70,6 +64,8 @@ export const ensureAppRegistration = async (
|
||||
await configService.setConfig({
|
||||
appRegistrationId: registration.id,
|
||||
appRegistrationClientId: registration.oAuthClientId,
|
||||
appAccessToken: undefined,
|
||||
appRefreshToken: undefined,
|
||||
});
|
||||
|
||||
const rotateResult =
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { isPlainObject } from 'twenty-shared/utils';
|
||||
|
||||
type GraphQLErrorEntry = {
|
||||
message?: string;
|
||||
extensions?: {
|
||||
code?: string;
|
||||
subCode?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type OAuthErrorBody = {
|
||||
error?: string;
|
||||
error_description?: string;
|
||||
};
|
||||
|
||||
const asGraphQLErrorEntry = (value: unknown): GraphQLErrorEntry | undefined => {
|
||||
if (!isPlainObject(value)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return value as GraphQLErrorEntry;
|
||||
};
|
||||
|
||||
export const getGraphQLErrorCode = (error: unknown): string | undefined => {
|
||||
return asGraphQLErrorEntry(error)?.extensions?.code;
|
||||
};
|
||||
|
||||
export const getGraphQLErrorSubCode = (error: unknown): string | undefined => {
|
||||
return asGraphQLErrorEntry(error)?.extensions?.subCode;
|
||||
};
|
||||
|
||||
export const hasGraphQLErrorSubCode = (
|
||||
error: unknown,
|
||||
subCode: string,
|
||||
): boolean => {
|
||||
return getGraphQLErrorSubCode(error) === subCode;
|
||||
};
|
||||
|
||||
export const isGraphQLNotFoundError = (error: unknown): boolean => {
|
||||
return getGraphQLErrorCode(error) === 'NOT_FOUND';
|
||||
};
|
||||
|
||||
export const getOAuthError = (body: unknown): string | undefined => {
|
||||
if (!isPlainObject(body)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { error } = body as OAuthErrorBody;
|
||||
|
||||
return typeof error === 'string' ? error : undefined;
|
||||
};
|
||||
|
||||
export const isOAuthInvalidClientError = (body: unknown): boolean => {
|
||||
return getOAuthError(body) === 'invalid_client';
|
||||
};
|
||||
Reference in New Issue
Block a user