mirror of
https://github.com/codeaashu/claude-code.git
synced 2026-04-09 06:38:46 +03:00
claude-code
This commit is contained in:
282
src/commands/mcp/addCommand.ts
Normal file
282
src/commands/mcp/addCommand.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* MCP add CLI subcommand
|
||||
*
|
||||
* Extracted from main.tsx to enable direct testing.
|
||||
*/
|
||||
import { type Command, Option } from '@commander-js/extra-typings'
|
||||
import { cliError, cliOk } from '../../cli/exit.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
import {
|
||||
readClientSecret,
|
||||
saveMcpClientSecret,
|
||||
} from '../../services/mcp/auth.js'
|
||||
import { addMcpConfig } from '../../services/mcp/config.js'
|
||||
import {
|
||||
describeMcpConfigFilePath,
|
||||
ensureConfigScope,
|
||||
ensureTransport,
|
||||
parseHeaders,
|
||||
} from '../../services/mcp/utils.js'
|
||||
import {
|
||||
getXaaIdpSettings,
|
||||
isXaaEnabled,
|
||||
} from '../../services/mcp/xaaIdpLogin.js'
|
||||
import { parseEnvVars } from '../../utils/envUtils.js'
|
||||
import { jsonStringify } from '../../utils/slowOperations.js'
|
||||
|
||||
/**
|
||||
* Registers the `mcp add` subcommand on the given Commander command.
|
||||
*/
|
||||
export function registerMcpAddCommand(mcp: Command): void {
|
||||
mcp
|
||||
.command('add <name> <commandOrUrl> [args...]')
|
||||
.description(
|
||||
'Add an MCP server to Claude Code.\n\n' +
|
||||
'Examples:\n' +
|
||||
' # Add HTTP server:\n' +
|
||||
' claude mcp add --transport http sentry https://mcp.sentry.dev/mcp\n\n' +
|
||||
' # Add HTTP server with headers:\n' +
|
||||
' claude mcp add --transport http corridor https://app.corridor.dev/api/mcp --header "Authorization: Bearer ..."\n\n' +
|
||||
' # Add stdio server with environment variables:\n' +
|
||||
' claude mcp add -e API_KEY=xxx my-server -- npx my-mcp-server\n\n' +
|
||||
' # Add stdio server with subprocess flags:\n' +
|
||||
' claude mcp add my-server -- my-command --some-flag arg1',
|
||||
)
|
||||
.option(
|
||||
'-s, --scope <scope>',
|
||||
'Configuration scope (local, user, or project)',
|
||||
'local',
|
||||
)
|
||||
.option(
|
||||
'-t, --transport <transport>',
|
||||
'Transport type (stdio, sse, http). Defaults to stdio if not specified.',
|
||||
)
|
||||
.option(
|
||||
'-e, --env <env...>',
|
||||
'Set environment variables (e.g. -e KEY=value)',
|
||||
)
|
||||
.option(
|
||||
'-H, --header <header...>',
|
||||
'Set WebSocket headers (e.g. -H "X-Api-Key: abc123" -H "X-Custom: value")',
|
||||
)
|
||||
.option('--client-id <clientId>', 'OAuth client ID for HTTP/SSE servers')
|
||||
.option(
|
||||
'--client-secret',
|
||||
'Prompt for OAuth client secret (or set MCP_CLIENT_SECRET env var)',
|
||||
)
|
||||
.option(
|
||||
'--callback-port <port>',
|
||||
'Fixed port for OAuth callback (for servers requiring pre-registered redirect URIs)',
|
||||
)
|
||||
.helpOption('-h, --help', 'Display help for command')
|
||||
.addOption(
|
||||
new Option(
|
||||
'--xaa',
|
||||
"Enable XAA (SEP-990) for this server. Requires 'claude mcp xaa setup' first. Also requires --client-id and --client-secret (for the MCP server's AS).",
|
||||
).hideHelp(!isXaaEnabled()),
|
||||
)
|
||||
.action(async (name, commandOrUrl, args, options) => {
|
||||
// Commander.js handles -- natively: it consumes -- and everything after becomes args
|
||||
const actualCommand = commandOrUrl
|
||||
const actualArgs = args
|
||||
|
||||
// If no name is provided, error
|
||||
if (!name) {
|
||||
cliError(
|
||||
'Error: Server name is required.\n' +
|
||||
'Usage: claude mcp add <name> <command> [args...]',
|
||||
)
|
||||
} else if (!actualCommand) {
|
||||
cliError(
|
||||
'Error: Command is required when server name is provided.\n' +
|
||||
'Usage: claude mcp add <name> <command> [args...]',
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const scope = ensureConfigScope(options.scope)
|
||||
const transport = ensureTransport(options.transport)
|
||||
|
||||
// XAA fail-fast: validate at add-time, not auth-time.
|
||||
if (options.xaa && !isXaaEnabled()) {
|
||||
cliError(
|
||||
'Error: --xaa requires CLAUDE_CODE_ENABLE_XAA=1 in your environment',
|
||||
)
|
||||
}
|
||||
const xaa = Boolean(options.xaa)
|
||||
if (xaa) {
|
||||
const missing: string[] = []
|
||||
if (!options.clientId) missing.push('--client-id')
|
||||
if (!options.clientSecret) missing.push('--client-secret')
|
||||
if (!getXaaIdpSettings()) {
|
||||
missing.push(
|
||||
"'claude mcp xaa setup' (settings.xaaIdp not configured)",
|
||||
)
|
||||
}
|
||||
if (missing.length) {
|
||||
cliError(`Error: --xaa requires: ${missing.join(', ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if transport was explicitly provided
|
||||
const transportExplicit = options.transport !== undefined
|
||||
|
||||
// Check if the command looks like a URL (likely incorrect usage)
|
||||
const looksLikeUrl =
|
||||
actualCommand.startsWith('http://') ||
|
||||
actualCommand.startsWith('https://') ||
|
||||
actualCommand.startsWith('localhost') ||
|
||||
actualCommand.endsWith('/sse') ||
|
||||
actualCommand.endsWith('/mcp')
|
||||
|
||||
logEvent('tengu_mcp_add', {
|
||||
type: transport as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
scope:
|
||||
scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
source:
|
||||
'command' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
transport:
|
||||
transport as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
transportExplicit: transportExplicit,
|
||||
looksLikeUrl: looksLikeUrl,
|
||||
})
|
||||
|
||||
if (transport === 'sse') {
|
||||
if (!actualCommand) {
|
||||
cliError('Error: URL is required for SSE transport.')
|
||||
}
|
||||
|
||||
const headers = options.header
|
||||
? parseHeaders(options.header)
|
||||
: undefined
|
||||
|
||||
const callbackPort = options.callbackPort
|
||||
? parseInt(options.callbackPort, 10)
|
||||
: undefined
|
||||
const oauth =
|
||||
options.clientId || callbackPort || xaa
|
||||
? {
|
||||
...(options.clientId ? { clientId: options.clientId } : {}),
|
||||
...(callbackPort ? { callbackPort } : {}),
|
||||
...(xaa ? { xaa: true } : {}),
|
||||
}
|
||||
: undefined
|
||||
|
||||
const clientSecret =
|
||||
options.clientSecret && options.clientId
|
||||
? await readClientSecret()
|
||||
: undefined
|
||||
|
||||
const serverConfig = {
|
||||
type: 'sse' as const,
|
||||
url: actualCommand,
|
||||
headers,
|
||||
oauth,
|
||||
}
|
||||
await addMcpConfig(name, serverConfig, scope)
|
||||
|
||||
if (clientSecret) {
|
||||
saveMcpClientSecret(name, serverConfig, clientSecret)
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
`Added SSE MCP server ${name} with URL: ${actualCommand} to ${scope} config\n`,
|
||||
)
|
||||
if (headers) {
|
||||
process.stdout.write(
|
||||
`Headers: ${jsonStringify(headers, null, 2)}\n`,
|
||||
)
|
||||
}
|
||||
} else if (transport === 'http') {
|
||||
if (!actualCommand) {
|
||||
cliError('Error: URL is required for HTTP transport.')
|
||||
}
|
||||
|
||||
const headers = options.header
|
||||
? parseHeaders(options.header)
|
||||
: undefined
|
||||
|
||||
const callbackPort = options.callbackPort
|
||||
? parseInt(options.callbackPort, 10)
|
||||
: undefined
|
||||
const oauth =
|
||||
options.clientId || callbackPort || xaa
|
||||
? {
|
||||
...(options.clientId ? { clientId: options.clientId } : {}),
|
||||
...(callbackPort ? { callbackPort } : {}),
|
||||
...(xaa ? { xaa: true } : {}),
|
||||
}
|
||||
: undefined
|
||||
|
||||
const clientSecret =
|
||||
options.clientSecret && options.clientId
|
||||
? await readClientSecret()
|
||||
: undefined
|
||||
|
||||
const serverConfig = {
|
||||
type: 'http' as const,
|
||||
url: actualCommand,
|
||||
headers,
|
||||
oauth,
|
||||
}
|
||||
await addMcpConfig(name, serverConfig, scope)
|
||||
|
||||
if (clientSecret) {
|
||||
saveMcpClientSecret(name, serverConfig, clientSecret)
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
`Added HTTP MCP server ${name} with URL: ${actualCommand} to ${scope} config\n`,
|
||||
)
|
||||
if (headers) {
|
||||
process.stdout.write(
|
||||
`Headers: ${jsonStringify(headers, null, 2)}\n`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
options.clientId ||
|
||||
options.clientSecret ||
|
||||
options.callbackPort ||
|
||||
options.xaa
|
||||
) {
|
||||
process.stderr.write(
|
||||
`Warning: --client-id, --client-secret, --callback-port, and --xaa are only supported for HTTP/SSE transports and will be ignored for stdio.\n`,
|
||||
)
|
||||
}
|
||||
|
||||
// Warn if this looks like a URL but transport wasn't explicitly specified
|
||||
if (!transportExplicit && looksLikeUrl) {
|
||||
process.stderr.write(
|
||||
`\nWarning: The command "${actualCommand}" looks like a URL, but is being interpreted as a stdio server as --transport was not specified.\n`,
|
||||
)
|
||||
process.stderr.write(
|
||||
`If this is an HTTP server, use: claude mcp add --transport http ${name} ${actualCommand}\n`,
|
||||
)
|
||||
process.stderr.write(
|
||||
`If this is an SSE server, use: claude mcp add --transport sse ${name} ${actualCommand}\n`,
|
||||
)
|
||||
}
|
||||
|
||||
const env = parseEnvVars(options.env)
|
||||
await addMcpConfig(
|
||||
name,
|
||||
{ type: 'stdio', command: actualCommand, args: actualArgs, env },
|
||||
scope,
|
||||
)
|
||||
|
||||
process.stdout.write(
|
||||
`Added stdio MCP server ${name} with command: ${actualCommand} ${actualArgs.join(' ')} to ${scope} config\n`,
|
||||
)
|
||||
}
|
||||
cliOk(`File modified: ${describeMcpConfigFilePath(scope)}`)
|
||||
} catch (error) {
|
||||
cliError((error as Error).message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
14
src/commands/mcp/index.ts
Normal file
14
src/commands/mcp/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { Command } from '../../commands.js'
|
||||
|
||||
const mcp = {
|
||||
type: 'local-jsx',
|
||||
name: 'mcp',
|
||||
description: 'Manage MCP servers',
|
||||
immediate: true,
|
||||
argumentHint: '[enable|disable [server-name]]',
|
||||
load: () => import('./mcp.js'),
|
||||
} satisfies Command
|
||||
|
||||
export default mcp
|
||||
|
||||
|
||||
86
src/commands/mcp/mcp.tsx
Normal file
86
src/commands/mcp/mcp.tsx
Normal file
File diff suppressed because one or more lines are too long
268
src/commands/mcp/xaaIdpCommand.ts
Normal file
268
src/commands/mcp/xaaIdpCommand.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* `claude mcp xaa` — manage the XAA (SEP-990) IdP connection.
|
||||
*
|
||||
* The IdP connection is user-level: configure once, all XAA-enabled MCP
|
||||
* servers reuse it. Lives in settings.xaaIdp (non-secret) + a keychain slot
|
||||
* keyed by issuer (secret). Separate trust domain from per-server AS secrets.
|
||||
*/
|
||||
import type { Command } from '@commander-js/extra-typings'
|
||||
import { cliError, cliOk } from '../../cli/exit.js'
|
||||
import {
|
||||
acquireIdpIdToken,
|
||||
clearIdpClientSecret,
|
||||
clearIdpIdToken,
|
||||
getCachedIdpIdToken,
|
||||
getIdpClientSecret,
|
||||
getXaaIdpSettings,
|
||||
issuerKey,
|
||||
saveIdpClientSecret,
|
||||
saveIdpIdTokenFromJwt,
|
||||
} from '../../services/mcp/xaaIdpLogin.js'
|
||||
import { errorMessage } from '../../utils/errors.js'
|
||||
import { updateSettingsForSource } from '../../utils/settings/settings.js'
|
||||
|
||||
export function registerMcpXaaIdpCommand(mcp: Command): void {
|
||||
const xaaIdp = mcp
|
||||
.command('xaa')
|
||||
.description('Manage the XAA (SEP-990) IdP connection')
|
||||
|
||||
xaaIdp
|
||||
.command('setup')
|
||||
.description(
|
||||
'Configure the IdP connection (one-time setup for all XAA-enabled servers)',
|
||||
)
|
||||
.requiredOption('--issuer <url>', 'IdP issuer URL (OIDC discovery)')
|
||||
.requiredOption('--client-id <id>', "Claude Code's client_id at the IdP")
|
||||
.option(
|
||||
'--client-secret',
|
||||
'Read IdP client secret from MCP_XAA_IDP_CLIENT_SECRET env var',
|
||||
)
|
||||
.option(
|
||||
'--callback-port <port>',
|
||||
'Fixed loopback callback port (only if IdP does not honor RFC 8252 port-any matching)',
|
||||
)
|
||||
.action(options => {
|
||||
// Validate everything BEFORE any writes. An exit(1) mid-write leaves
|
||||
// settings configured but keychain missing — confusing state.
|
||||
// updateSettingsForSource doesn't schema-check on write; a non-URL
|
||||
// issuer lands on disk and then poisons the whole userSettings source
|
||||
// on next launch (SettingsSchema .url() fails → parseSettingsFile
|
||||
// returns { settings: null }, dropping everything, not just xaaIdp).
|
||||
let issuerUrl: URL
|
||||
try {
|
||||
issuerUrl = new URL(options.issuer)
|
||||
} catch {
|
||||
return cliError(
|
||||
`Error: --issuer must be a valid URL (got "${options.issuer}")`,
|
||||
)
|
||||
}
|
||||
// OIDC discovery + token exchange run against this host. Allow http://
|
||||
// only for loopback (conformance harness mock IdP); anything else leaks
|
||||
// the client secret and authorization code over plaintext.
|
||||
if (
|
||||
issuerUrl.protocol !== 'https:' &&
|
||||
!(
|
||||
issuerUrl.protocol === 'http:' &&
|
||||
(issuerUrl.hostname === 'localhost' ||
|
||||
issuerUrl.hostname === '127.0.0.1' ||
|
||||
issuerUrl.hostname === '[::1]')
|
||||
)
|
||||
) {
|
||||
return cliError(
|
||||
`Error: --issuer must use https:// (got "${issuerUrl.protocol}//${issuerUrl.host}")`,
|
||||
)
|
||||
}
|
||||
const callbackPort = options.callbackPort
|
||||
? parseInt(options.callbackPort, 10)
|
||||
: undefined
|
||||
// callbackPort <= 0 fails Zod's .positive() on next launch — same
|
||||
// settings-poisoning failure mode as the issuer check above.
|
||||
if (
|
||||
callbackPort !== undefined &&
|
||||
(!Number.isInteger(callbackPort) || callbackPort <= 0)
|
||||
) {
|
||||
return cliError('Error: --callback-port must be a positive integer')
|
||||
}
|
||||
const secret = options.clientSecret
|
||||
? process.env.MCP_XAA_IDP_CLIENT_SECRET
|
||||
: undefined
|
||||
if (options.clientSecret && !secret) {
|
||||
return cliError(
|
||||
'Error: --client-secret requires MCP_XAA_IDP_CLIENT_SECRET env var',
|
||||
)
|
||||
}
|
||||
|
||||
// Read old config now (before settings overwrite) so we can clear stale
|
||||
// keychain slots after a successful write. `clear` can't do this after
|
||||
// the fact — it reads the *current* settings.xaaIdp, which by then is
|
||||
// the new one.
|
||||
const old = getXaaIdpSettings()
|
||||
const oldIssuer = old?.issuer
|
||||
const oldClientId = old?.clientId
|
||||
|
||||
// callbackPort MUST be present (even as undefined) — mergeWith deep-merges
|
||||
// and only deletes on explicit `undefined`, not on absent key. A conditional
|
||||
// spread would leak a prior fixed port into a new IdP's config.
|
||||
const { error } = updateSettingsForSource('userSettings', {
|
||||
xaaIdp: {
|
||||
issuer: options.issuer,
|
||||
clientId: options.clientId,
|
||||
callbackPort,
|
||||
},
|
||||
})
|
||||
if (error) {
|
||||
return cliError(`Error writing settings: ${error.message}`)
|
||||
}
|
||||
|
||||
// Clear stale keychain slots only after settings write succeeded —
|
||||
// otherwise a write failure leaves settings pointing at oldIssuer with
|
||||
// its secret already gone. Compare via issuerKey(): trailing-slash or
|
||||
// host-case differences normalize to the same keychain slot.
|
||||
if (oldIssuer) {
|
||||
if (issuerKey(oldIssuer) !== issuerKey(options.issuer)) {
|
||||
clearIdpIdToken(oldIssuer)
|
||||
clearIdpClientSecret(oldIssuer)
|
||||
} else if (oldClientId !== options.clientId) {
|
||||
// Same issuer slot but different OAuth client registration — the
|
||||
// cached id_token's aud claim and the stored secret are both for the
|
||||
// old client. `xaa login` would send {new clientId, old secret} and
|
||||
// fail with opaque `invalid_client`; downstream SEP-990 exchange
|
||||
// would fail aud validation. Keep both when clientId is unchanged:
|
||||
// re-setup without --client-secret means "tweak port, keep secret".
|
||||
clearIdpIdToken(oldIssuer)
|
||||
clearIdpClientSecret(oldIssuer)
|
||||
}
|
||||
}
|
||||
|
||||
if (secret) {
|
||||
const { success, warning } = saveIdpClientSecret(options.issuer, secret)
|
||||
if (!success) {
|
||||
return cliError(
|
||||
`Error: settings written but keychain save failed${warning ? ` — ${warning}` : ''}. ` +
|
||||
`Re-run with --client-secret once keychain is available.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
cliOk(`XAA IdP connection configured for ${options.issuer}`)
|
||||
})
|
||||
|
||||
xaaIdp
|
||||
.command('login')
|
||||
.description(
|
||||
'Cache an IdP id_token so XAA-enabled MCP servers authenticate ' +
|
||||
'silently. Default: run the OIDC browser login. With --id-token: ' +
|
||||
'write a pre-obtained JWT directly (used by conformance/e2e tests ' +
|
||||
'where the mock IdP does not serve /authorize).',
|
||||
)
|
||||
.option(
|
||||
'--force',
|
||||
'Ignore any cached id_token and re-login (useful after IdP-side revocation)',
|
||||
)
|
||||
// TODO(paulc): read the JWT from stdin instead of argv to keep it out of
|
||||
// shell history. Fine for conformance (docker exec uses argv directly,
|
||||
// no shell parser), but a real user would want `echo $TOKEN | ... --stdin`.
|
||||
.option(
|
||||
'--id-token <jwt>',
|
||||
'Write this pre-obtained id_token directly to cache, skipping the OIDC browser login',
|
||||
)
|
||||
.action(async options => {
|
||||
const idp = getXaaIdpSettings()
|
||||
if (!idp) {
|
||||
return cliError(
|
||||
"Error: no XAA IdP connection. Run 'claude mcp xaa setup' first.",
|
||||
)
|
||||
}
|
||||
|
||||
// Direct-inject path: skip cache check, skip OIDC. Writing IS the
|
||||
// operation. Issuer comes from settings (single source of truth), not
|
||||
// a separate flag — one less thing to desync.
|
||||
if (options.idToken) {
|
||||
const expiresAt = saveIdpIdTokenFromJwt(idp.issuer, options.idToken)
|
||||
return cliOk(
|
||||
`id_token cached for ${idp.issuer} (expires ${new Date(expiresAt).toISOString()})`,
|
||||
)
|
||||
}
|
||||
|
||||
if (options.force) {
|
||||
clearIdpIdToken(idp.issuer)
|
||||
}
|
||||
|
||||
const wasCached = getCachedIdpIdToken(idp.issuer) !== undefined
|
||||
if (wasCached) {
|
||||
return cliOk(
|
||||
`Already logged in to ${idp.issuer} (cached id_token still valid). Use --force to re-login.`,
|
||||
)
|
||||
}
|
||||
|
||||
process.stdout.write(`Opening browser for IdP login at ${idp.issuer}…\n`)
|
||||
try {
|
||||
await acquireIdpIdToken({
|
||||
idpIssuer: idp.issuer,
|
||||
idpClientId: idp.clientId,
|
||||
idpClientSecret: getIdpClientSecret(idp.issuer),
|
||||
callbackPort: idp.callbackPort,
|
||||
onAuthorizationUrl: url => {
|
||||
process.stdout.write(
|
||||
`If the browser did not open, visit:\n ${url}\n`,
|
||||
)
|
||||
},
|
||||
})
|
||||
cliOk(
|
||||
`Logged in. MCP servers with --xaa will now authenticate silently.`,
|
||||
)
|
||||
} catch (e) {
|
||||
cliError(`IdP login failed: ${errorMessage(e)}`)
|
||||
}
|
||||
})
|
||||
|
||||
xaaIdp
|
||||
.command('show')
|
||||
.description('Show the current IdP connection config')
|
||||
.action(() => {
|
||||
const idp = getXaaIdpSettings()
|
||||
if (!idp) {
|
||||
return cliOk('No XAA IdP connection configured.')
|
||||
}
|
||||
const hasSecret = getIdpClientSecret(idp.issuer) !== undefined
|
||||
const hasIdToken = getCachedIdpIdToken(idp.issuer) !== undefined
|
||||
process.stdout.write(`Issuer: ${idp.issuer}\n`)
|
||||
process.stdout.write(`Client ID: ${idp.clientId}\n`)
|
||||
if (idp.callbackPort !== undefined) {
|
||||
process.stdout.write(`Callback port: ${idp.callbackPort}\n`)
|
||||
}
|
||||
process.stdout.write(
|
||||
`Client secret: ${hasSecret ? '(stored in keychain)' : '(not set — PKCE-only)'}\n`,
|
||||
)
|
||||
process.stdout.write(
|
||||
`Logged in: ${hasIdToken ? 'yes (id_token cached)' : "no — run 'claude mcp xaa login'"}\n`,
|
||||
)
|
||||
cliOk()
|
||||
})
|
||||
|
||||
xaaIdp
|
||||
.command('clear')
|
||||
.description('Clear the IdP connection config and cached id_token')
|
||||
.action(() => {
|
||||
// Read issuer first so we can clear the right keychain slots.
|
||||
const idp = getXaaIdpSettings()
|
||||
// updateSettingsForSource uses mergeWith: set to undefined (not delete)
|
||||
// to signal key removal.
|
||||
const { error } = updateSettingsForSource('userSettings', {
|
||||
xaaIdp: undefined,
|
||||
})
|
||||
if (error) {
|
||||
return cliError(`Error writing settings: ${error.message}`)
|
||||
}
|
||||
// Clear keychain only after settings write succeeded — otherwise a
|
||||
// write failure leaves settings pointing at the IdP with its secrets
|
||||
// already gone (same pattern as `setup`'s old-issuer cleanup).
|
||||
if (idp) {
|
||||
clearIdpIdToken(idp.issuer)
|
||||
clearIdpClientSecret(idp.issuer)
|
||||
}
|
||||
cliOk('XAA IdP connection cleared')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user