fix(sso): removed provider specific OIDC logic from SSO registration & deregistration scripts (#2896)

* fix(sso): updated registration & deregistration script for explicit support for Entra ID

* cleanup

* ack PR comment

* ack PR comment

* tested edge cases, ack'd PR comments

* remove trailing slash
This commit is contained in:
Waleed
2026-01-19 19:23:50 -08:00
committed by GitHub
parent 69614d2d93
commit ac991d4b54
6 changed files with 204 additions and 117 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
const logger = createLogger('SSO-Providers')
const logger = createLogger('SSOProvidersRoute')
export async function GET() {
try {
+107 -42
View File
@@ -6,7 +6,7 @@ import { hasSSOAccess } from '@/lib/billing'
import { env } from '@/lib/core/config/env'
import { REDACTED_MARKER } from '@/lib/core/security/redaction'
const logger = createLogger('SSO-Register')
const logger = createLogger('SSORegisterRoute')
const mappingSchema = z
.object({
@@ -43,6 +43,10 @@ const ssoRegistrationSchema = z.discriminatedUnion('providerType', [
])
.default(['openid', 'profile', 'email']),
pkce: z.boolean().default(true),
authorizationEndpoint: z.string().url().optional(),
tokenEndpoint: z.string().url().optional(),
userInfoEndpoint: z.string().url().optional(),
jwksEndpoint: z.string().url().optional(),
}),
z.object({
providerType: z.literal('saml'),
@@ -64,12 +68,10 @@ const ssoRegistrationSchema = z.discriminatedUnion('providerType', [
export async function POST(request: NextRequest) {
try {
// SSO plugin must be enabled in Better Auth
if (!env.SSO_ENABLED) {
return NextResponse.json({ error: 'SSO is not enabled' }, { status: 400 })
}
// Check plan access (enterprise) or env var override
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
@@ -116,7 +118,16 @@ export async function POST(request: NextRequest) {
}
if (providerType === 'oidc') {
const { clientId, clientSecret, scopes, pkce } = body
const {
clientId,
clientSecret,
scopes,
pkce,
authorizationEndpoint,
tokenEndpoint,
userInfoEndpoint,
jwksEndpoint,
} = body
const oidcConfig: any = {
clientId,
@@ -127,50 +138,104 @@ export async function POST(request: NextRequest) {
pkce: pkce ?? true,
}
// Add manual endpoints for providers that might need them
// Common patterns for OIDC providers that don't support discovery properly
if (
issuer.includes('okta.com') ||
issuer.includes('auth0.com') ||
issuer.includes('identityserver')
) {
const baseUrl = issuer.includes('/oauth2/default')
? issuer.replace('/oauth2/default', '')
: issuer.replace('/oauth', '').replace('/v2.0', '').replace('/oauth2', '')
oidcConfig.authorizationEndpoint = authorizationEndpoint
oidcConfig.tokenEndpoint = tokenEndpoint
oidcConfig.userInfoEndpoint = userInfoEndpoint
oidcConfig.jwksEndpoint = jwksEndpoint
// Okta-style endpoints
if (issuer.includes('okta.com')) {
oidcConfig.authorizationEndpoint = `${baseUrl}/oauth2/default/v1/authorize`
oidcConfig.tokenEndpoint = `${baseUrl}/oauth2/default/v1/token`
oidcConfig.userInfoEndpoint = `${baseUrl}/oauth2/default/v1/userinfo`
oidcConfig.jwksEndpoint = `${baseUrl}/oauth2/default/v1/keys`
}
// Auth0-style endpoints
else if (issuer.includes('auth0.com')) {
oidcConfig.authorizationEndpoint = `${baseUrl}/authorize`
oidcConfig.tokenEndpoint = `${baseUrl}/oauth/token`
oidcConfig.userInfoEndpoint = `${baseUrl}/userinfo`
oidcConfig.jwksEndpoint = `${baseUrl}/.well-known/jwks.json`
}
// Generic OIDC endpoints (IdentityServer, etc.)
else {
oidcConfig.authorizationEndpoint = `${baseUrl}/connect/authorize`
oidcConfig.tokenEndpoint = `${baseUrl}/connect/token`
oidcConfig.userInfoEndpoint = `${baseUrl}/connect/userinfo`
oidcConfig.jwksEndpoint = `${baseUrl}/.well-known/jwks`
}
const needsDiscovery =
!oidcConfig.authorizationEndpoint || !oidcConfig.tokenEndpoint || !oidcConfig.jwksEndpoint
logger.info('Using manual OIDC endpoints for provider', {
if (needsDiscovery) {
const discoveryUrl = `${issuer.replace(/\/$/, '')}/.well-known/openid-configuration`
try {
logger.info('Fetching OIDC discovery document for missing endpoints', {
discoveryUrl,
hasAuthEndpoint: !!oidcConfig.authorizationEndpoint,
hasTokenEndpoint: !!oidcConfig.tokenEndpoint,
hasJwksEndpoint: !!oidcConfig.jwksEndpoint,
})
const discoveryResponse = await fetch(discoveryUrl, {
headers: { Accept: 'application/json' },
})
if (!discoveryResponse.ok) {
logger.error('Failed to fetch OIDC discovery document', {
status: discoveryResponse.status,
statusText: discoveryResponse.statusText,
})
return NextResponse.json(
{
error: `Failed to fetch OIDC discovery document from ${discoveryUrl}. Status: ${discoveryResponse.status}. Provide all endpoints explicitly or verify the issuer URL.`,
},
{ status: 400 }
)
}
const discovery = await discoveryResponse.json()
oidcConfig.authorizationEndpoint =
oidcConfig.authorizationEndpoint || discovery.authorization_endpoint
oidcConfig.tokenEndpoint = oidcConfig.tokenEndpoint || discovery.token_endpoint
oidcConfig.userInfoEndpoint = oidcConfig.userInfoEndpoint || discovery.userinfo_endpoint
oidcConfig.jwksEndpoint = oidcConfig.jwksEndpoint || discovery.jwks_uri
logger.info('Merged OIDC endpoints (user-provided + discovery)', {
providerId,
issuer,
authorizationEndpoint: oidcConfig.authorizationEndpoint,
tokenEndpoint: oidcConfig.tokenEndpoint,
userInfoEndpoint: oidcConfig.userInfoEndpoint,
jwksEndpoint: oidcConfig.jwksEndpoint,
})
} catch (error) {
logger.error('Error fetching OIDC discovery document', {
error: error instanceof Error ? error.message : 'Unknown error',
discoveryUrl,
})
return NextResponse.json(
{
error: `Failed to fetch OIDC discovery document from ${discoveryUrl}. Please verify the issuer URL is correct or provide all endpoints explicitly.`,
},
{ status: 400 }
)
}
} else {
logger.info('Using explicitly provided OIDC endpoints (all present)', {
providerId,
provider: issuer.includes('okta.com')
? 'Okta'
: issuer.includes('auth0.com')
? 'Auth0'
: 'Generic',
authEndpoint: oidcConfig.authorizationEndpoint,
issuer,
authorizationEndpoint: oidcConfig.authorizationEndpoint,
tokenEndpoint: oidcConfig.tokenEndpoint,
userInfoEndpoint: oidcConfig.userInfoEndpoint,
jwksEndpoint: oidcConfig.jwksEndpoint,
})
}
if (
!oidcConfig.authorizationEndpoint ||
!oidcConfig.tokenEndpoint ||
!oidcConfig.jwksEndpoint
) {
const missing: string[] = []
if (!oidcConfig.authorizationEndpoint) missing.push('authorizationEndpoint')
if (!oidcConfig.tokenEndpoint) missing.push('tokenEndpoint')
if (!oidcConfig.jwksEndpoint) missing.push('jwksEndpoint')
logger.error('Missing required OIDC endpoints after discovery merge', {
missing,
authorizationEndpoint: oidcConfig.authorizationEndpoint,
tokenEndpoint: oidcConfig.tokenEndpoint,
jwksEndpoint: oidcConfig.jwksEndpoint,
})
return NextResponse.json(
{
error: `Missing required OIDC endpoints: ${missing.join(', ')}. Please provide these explicitly or verify the issuer supports OIDC discovery.`,
},
{ status: 400 }
)
}
providerConfig.oidcConfig = oidcConfig
} else if (providerType === 'saml') {
const {
@@ -6,6 +6,7 @@ import {
ClientToolCallState,
} from '@/lib/copilot/tools/client/base-tool'
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getInputFormatExample } from '@/lib/workflows/operations/deployment-utils'
import { useCopilotStore } from '@/stores/panel/copilot/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -36,7 +37,6 @@ export class DeployApiClientTool extends BaseClientTool {
const action = params?.action || 'deploy'
// Check if workflow is already deployed
const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId
const isAlreadyDeployed = workflowId
? useWorkflowRegistry.getState().getWorkflowDeploymentStatus(workflowId)?.isDeployed
@@ -89,7 +89,6 @@ export class DeployApiClientTool extends BaseClientTool {
getDynamicText: (params, state) => {
const action = params?.action === 'undeploy' ? 'undeploy' : 'deploy'
// Check if workflow is already deployed
const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId
const isAlreadyDeployed = workflowId
? useWorkflowRegistry.getState().getWorkflowDeploymentStatus(workflowId)?.isDeployed
@@ -231,10 +230,7 @@ export class DeployApiClientTool extends BaseClientTool {
}
if (action === 'deploy') {
const appUrl =
typeof window !== 'undefined'
? window.location.origin
: process.env.NEXT_PUBLIC_APP_URL || 'https://app.sim.ai'
const appUrl = getBaseUrl()
const apiEndpoint = `${appUrl}/api/workflows/${workflowId}/execute`
const apiKeyPlaceholder = '$SIM_API_KEY'
-38
View File
@@ -1,11 +1,7 @@
import { createLogger } from '@sim/logger'
import { isTest } from '@/lib/core/config/feature-flags'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { transformTable } from '@/tools/shared/table'
import type { TableRow } from '@/tools/types'
const logger = createLogger('HTTPRequestUtils')
/**
* Creates a set of default headers used in HTTP requests
* @param customHeaders Additional user-provided headers to include
@@ -30,7 +26,6 @@ export const getDefaultHeaders = (
...customHeaders,
}
// Add Host header if not provided and URL is valid
if (url) {
try {
const hostname = new URL(url).host
@@ -57,26 +52,21 @@ export const processUrl = (
pathParams?: Record<string, string>,
queryParams?: TableRow[] | null
): string => {
// Strip any surrounding quotes
if ((url.startsWith('"') && url.endsWith('"')) || (url.startsWith("'") && url.endsWith("'"))) {
url = url.slice(1, -1)
}
// Replace path parameters
if (pathParams) {
Object.entries(pathParams).forEach(([key, value]) => {
url = url.replace(`:${key}`, encodeURIComponent(value))
})
}
// Handle query parameters
if (queryParams) {
const queryParamsObj = transformTable(queryParams)
// Verify if URL already has query params to use proper separator
const separator = url.includes('?') ? '&' : '?'
// Build query string manually to avoid double-encoding issues
const queryParts: string[] = []
for (const [key, value] of Object.entries(queryParamsObj)) {
@@ -92,31 +82,3 @@ export const processUrl = (
return url
}
// Check if a URL needs proxy to avoid CORS/method restrictions
export const shouldUseProxy = (url: string): boolean => {
// Skip proxying in test environment
if (isTest) {
return false
}
// Only consider proxying in browser environment
if (typeof window === 'undefined') {
return false
}
try {
const _urlObj = new URL(url)
const currentOrigin = window.location.origin
// Don't proxy same-origin or localhost requests
if (url.startsWith(currentOrigin) || url.includes('localhost')) {
return false
}
return true // Proxy all cross-origin requests for consistency
} catch (e) {
logger.warn('URL parsing failed:', e)
return false
}
}
@@ -18,7 +18,6 @@ import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import { ssoProvider, user } from '../schema'
// Simple console logger
const logger = {
info: (message: string, meta?: any) => {
const timestamp = new Date().toISOString()
@@ -43,7 +42,6 @@ const logger = {
},
}
// Get database URL from environment
const CONNECTION_STRING = process.env.POSTGRES_URL ?? process.env.DATABASE_URL
if (!CONNECTION_STRING) {
console.error('❌ POSTGRES_URL or DATABASE_URL environment variable is required')
@@ -88,7 +86,6 @@ async function deregisterSSOProvider(): Promise<boolean> {
return false
}
// Get user
const targetUser = await getUser(userEmail)
if (!targetUser) {
return false
@@ -96,7 +93,6 @@ async function deregisterSSOProvider(): Promise<boolean> {
logger.info(`Found user: ${targetUser.email} (ID: ${targetUser.id})`)
// Get SSO providers for this user
const providers = await db
.select()
.from(ssoProvider)
@@ -112,11 +108,9 @@ async function deregisterSSOProvider(): Promise<boolean> {
logger.info(` - Provider ID: ${provider.providerId}, Domain: ${provider.domain}`)
}
// Check if specific provider ID was requested
const specificProviderId = process.env.SSO_PROVIDER_ID
if (specificProviderId) {
// Delete specific provider
const providerToDelete = providers.find((p) => p.providerId === specificProviderId)
if (!providerToDelete) {
logger.error(`Provider '${specificProviderId}' not found for user ${targetUser.email}`)
@@ -133,7 +127,6 @@ async function deregisterSSOProvider(): Promise<boolean> {
`✅ Successfully deleted SSO provider '${specificProviderId}' for user ${targetUser.email}`
)
} else {
// Delete all providers for this user
await db.delete(ssoProvider).where(eq(ssoProvider.userId, targetUser.id))
logger.info(
@@ -171,7 +164,6 @@ async function main() {
}
}
// Handle script execution
main().catch((error) => {
logger.error('Script execution failed:', { error })
process.exit(1)
+94 -22
View File
@@ -38,7 +38,6 @@ import postgres from 'postgres'
import { v4 as uuidv4 } from 'uuid'
import { ssoProvider, user } from '../schema'
// Self-contained SSO types (matching Better Auth's structure)
interface SSOMapping {
id: string
email: string
@@ -108,7 +107,6 @@ interface SSOProviderConfig {
samlConfig?: SAMLConfig
}
// Simple console logger (no dependencies)
const logger = {
info: (message: string, meta?: any) => {
const timestamp = new Date().toISOString()
@@ -133,14 +131,12 @@ const logger = {
},
}
// Get database URL from environment
const CONNECTION_STRING = process.env.POSTGRES_URL ?? process.env.DATABASE_URL
if (!CONNECTION_STRING) {
console.error('❌ POSTGRES_URL or DATABASE_URL environment variable is required')
process.exit(1)
}
// Initialize database connection (following migration script pattern)
const postgresClient = postgres(CONNECTION_STRING, {
prepare: false,
idle_timeout: 20,
@@ -161,7 +157,6 @@ interface SSOProviderData {
organizationId?: string
}
// Self-contained configuration builder (no external dependencies)
function buildSSOConfigFromEnv(): SSOProviderConfig | null {
const enabled = process.env.SSO_ENABLED === 'true'
if (!enabled) return null
@@ -182,7 +177,6 @@ function buildSSOConfigFromEnv(): SSOProviderConfig | null {
providerType,
}
// Build field mapping
config.mapping = {
id:
process.env.SSO_MAPPING_ID ||
@@ -202,7 +196,6 @@ function buildSSOConfigFromEnv(): SSOProviderConfig | null {
image: process.env.SSO_MAPPING_IMAGE || (providerType === 'oidc' ? 'picture' : undefined),
}
// Build provider-specific configuration
if (providerType === 'oidc') {
const clientId = process.env.SSO_OIDC_CLIENT_ID
const clientSecret = process.env.SSO_OIDC_CLIENT_SECRET
@@ -225,7 +218,8 @@ function buildSSOConfigFromEnv(): SSOProviderConfig | null {
userInfoEndpoint: process.env.SSO_OIDC_USERINFO_ENDPOINT,
jwksEndpoint: process.env.SSO_OIDC_JWKS_ENDPOINT,
discoveryEndpoint:
process.env.SSO_OIDC_DISCOVERY_ENDPOINT || `${issuer}/.well-known/openid-configuration`,
process.env.SSO_OIDC_DISCOVERY_ENDPOINT ||
`${issuer.replace(/\/$/, '')}/.well-known/openid-configuration`,
}
} else if (providerType === 'saml') {
const entryPoint = process.env.SSO_SAML_ENTRY_POINT
@@ -237,7 +231,6 @@ function buildSSOConfigFromEnv(): SSOProviderConfig | null {
const callbackUrl = process.env.SSO_SAML_CALLBACK_URL || `${issuer}/callback`
// Use custom metadata if provided, otherwise generate default
let spMetadata = process.env.SSO_SAML_SP_METADATA
if (!spMetadata) {
spMetadata = `<?xml version="1.0" encoding="UTF-8"?>
@@ -263,7 +256,6 @@ function buildSSOConfigFromEnv(): SSOProviderConfig | null {
entityID: issuer,
},
}
// Optionally include IDP metadata if provided
const idpMetadata = process.env.SSO_SAML_IDP_METADATA
if (idpMetadata) {
config.samlConfig.idpMetadata = {
@@ -275,7 +267,6 @@ function buildSSOConfigFromEnv(): SSOProviderConfig | null {
return config
}
// Self-contained example environment variables function
function getExampleEnvVars(
providerType: 'oidc' | 'saml',
provider?: string
@@ -358,7 +349,6 @@ async function getAdminUser(): Promise<{ id: string; email: string } | null> {
async function registerSSOProvider(): Promise<boolean> {
try {
// Build configuration from environment variables
const ssoConfig = buildSSOConfigFromEnv()
if (!ssoConfig) {
@@ -381,7 +371,6 @@ async function registerSSOProvider(): Promise<boolean> {
return false
}
// Get admin user
const adminUser = await getAdminUser()
if (!adminUser) {
return false
@@ -394,7 +383,6 @@ async function registerSSOProvider(): Promise<boolean> {
adminUser: adminUser.email,
})
// Validate issuer URL (same as Better Auth does)
try {
new URL(ssoConfig.issuer)
} catch {
@@ -402,7 +390,97 @@ async function registerSSOProvider(): Promise<boolean> {
return false
}
// Check if provider already exists
if (ssoConfig.providerType === 'oidc' && ssoConfig.oidcConfig) {
const needsDiscovery =
!ssoConfig.oidcConfig.authorizationEndpoint ||
!ssoConfig.oidcConfig.tokenEndpoint ||
!ssoConfig.oidcConfig.jwksEndpoint
if (needsDiscovery) {
const discoveryUrl =
ssoConfig.oidcConfig.discoveryEndpoint ||
`${ssoConfig.issuer.replace(/\/$/, '')}/.well-known/openid-configuration`
logger.info('Fetching OIDC discovery document for missing endpoints...', {
discoveryUrl,
hasAuthEndpoint: !!ssoConfig.oidcConfig.authorizationEndpoint,
hasTokenEndpoint: !!ssoConfig.oidcConfig.tokenEndpoint,
hasJwksEndpoint: !!ssoConfig.oidcConfig.jwksEndpoint,
})
try {
const response = await fetch(discoveryUrl, {
headers: { Accept: 'application/json' },
})
if (!response.ok) {
logger.error('Failed to fetch OIDC discovery document', {
status: response.status,
statusText: response.statusText,
})
logger.error(
'Provide all endpoints explicitly via SSO_OIDC_AUTHORIZATION_ENDPOINT, SSO_OIDC_TOKEN_ENDPOINT, SSO_OIDC_JWKS_ENDPOINT'
)
return false
}
const discovery = await response.json()
ssoConfig.oidcConfig.authorizationEndpoint =
ssoConfig.oidcConfig.authorizationEndpoint || discovery.authorization_endpoint
ssoConfig.oidcConfig.tokenEndpoint =
ssoConfig.oidcConfig.tokenEndpoint || discovery.token_endpoint
ssoConfig.oidcConfig.userInfoEndpoint =
ssoConfig.oidcConfig.userInfoEndpoint || discovery.userinfo_endpoint
ssoConfig.oidcConfig.jwksEndpoint =
ssoConfig.oidcConfig.jwksEndpoint || discovery.jwks_uri
logger.info('Merged OIDC endpoints (user-provided + discovery)', {
authorizationEndpoint: ssoConfig.oidcConfig.authorizationEndpoint,
tokenEndpoint: ssoConfig.oidcConfig.tokenEndpoint,
userInfoEndpoint: ssoConfig.oidcConfig.userInfoEndpoint,
jwksEndpoint: ssoConfig.oidcConfig.jwksEndpoint,
})
} catch (error) {
logger.error('Error fetching OIDC discovery document', {
error: error instanceof Error ? error.message : 'Unknown error',
discoveryUrl,
})
logger.error(
'Please provide explicit endpoints via SSO_OIDC_AUTHORIZATION_ENDPOINT, SSO_OIDC_TOKEN_ENDPOINT, SSO_OIDC_JWKS_ENDPOINT'
)
return false
}
} else {
logger.info('Using explicitly provided OIDC endpoints (all present)', {
authorizationEndpoint: ssoConfig.oidcConfig.authorizationEndpoint,
tokenEndpoint: ssoConfig.oidcConfig.tokenEndpoint,
userInfoEndpoint: ssoConfig.oidcConfig.userInfoEndpoint,
jwksEndpoint: ssoConfig.oidcConfig.jwksEndpoint,
})
}
if (
!ssoConfig.oidcConfig.authorizationEndpoint ||
!ssoConfig.oidcConfig.tokenEndpoint ||
!ssoConfig.oidcConfig.jwksEndpoint
) {
const missing: string[] = []
if (!ssoConfig.oidcConfig.authorizationEndpoint)
missing.push('SSO_OIDC_AUTHORIZATION_ENDPOINT')
if (!ssoConfig.oidcConfig.tokenEndpoint) missing.push('SSO_OIDC_TOKEN_ENDPOINT')
if (!ssoConfig.oidcConfig.jwksEndpoint) missing.push('SSO_OIDC_JWKS_ENDPOINT')
logger.error('Missing required OIDC endpoints after discovery merge', {
missing,
authorizationEndpoint: ssoConfig.oidcConfig.authorizationEndpoint,
tokenEndpoint: ssoConfig.oidcConfig.tokenEndpoint,
jwksEndpoint: ssoConfig.oidcConfig.jwksEndpoint,
})
logger.error(`Please provide: ${missing.join(', ')}`)
return false
}
}
const existingProviders = await db
.select()
.from(ssoProvider)
@@ -413,9 +491,8 @@ async function registerSSOProvider(): Promise<boolean> {
logger.info('Updating existing provider...')
}
// Build provider data (following Better Auth's exact structure)
const providerData: SSOProviderData = {
id: uuidv4(), // Generate unique ID
id: uuidv4(),
issuer: ssoConfig.issuer,
domain: ssoConfig.domain,
userId: adminUser.id,
@@ -423,7 +500,6 @@ async function registerSSOProvider(): Promise<boolean> {
organizationId: process.env.SSO_ORGANIZATION_ID || undefined,
}
// Build OIDC config (same as Better Auth endpoint)
if (ssoConfig.providerType === 'oidc' && ssoConfig.oidcConfig) {
const oidcConfig = {
issuer: ssoConfig.issuer,
@@ -445,7 +521,6 @@ async function registerSSOProvider(): Promise<boolean> {
providerData.oidcConfig = JSON.stringify(oidcConfig)
}
// Build SAML config (same as Better Auth endpoint)
if (ssoConfig.providerType === 'saml' && ssoConfig.samlConfig) {
const samlConfig = {
issuer: ssoConfig.issuer,
@@ -467,7 +542,6 @@ async function registerSSOProvider(): Promise<boolean> {
providerData.samlConfig = JSON.stringify(samlConfig)
}
// Insert or update the SSO provider record
if (existingProviders.length > 0) {
await db
.update(ssoProvider)
@@ -521,7 +595,6 @@ async function main() {
console.log('This script directly inserts SSO provider records into the database.')
console.log("It follows Better Auth's exact registerSSOProvider logic.\n")
// Register the SSO provider using direct database access
const success = await registerSSOProvider()
if (success) {
@@ -538,7 +611,6 @@ async function main() {
}
}
// Handle script execution
main().catch((error) => {
logger.error('Script execution failed:', { error })
process.exit(1)