mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-20 18:19:21 +00:00
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kaitranntt/ccs",
|
||||
"version": "7.49.0",
|
||||
"version": "7.49.0-dev.2",
|
||||
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
|
||||
"keywords": [
|
||||
"cli",
|
||||
|
||||
@@ -165,9 +165,18 @@ export function warnCrossProviderDuplicates(provider: CLIProxyProvider): boolean
|
||||
|
||||
console.error('');
|
||||
console.error(warn('Account safety: cross-provider duplicate detected'));
|
||||
console.error(' Same Google account across providers risks account bans (ref: #509).');
|
||||
console.error(
|
||||
' If provider requests start returning 403/Forbidden, treat it as a possible ban.'
|
||||
' Same Google account across "ccs gemini" + "ccs agy" is a known suspension/ban risk (ref: #509).'
|
||||
);
|
||||
console.error(' This risk applies to both CLI sessions and accounts added from "ccs config".');
|
||||
console.error(
|
||||
' If provider requests start returning 403/Forbidden, treat it as a possible account disable/ban.'
|
||||
);
|
||||
console.error(
|
||||
' If you want to keep Google AI access on this account, do not continue this shared-account setup.'
|
||||
);
|
||||
console.error(
|
||||
' CCS is provided as-is and cannot take responsibility for suspension/ban/access-loss decisions.'
|
||||
);
|
||||
console.error(` Details: ${ISSUE_509_URL}`);
|
||||
console.error('');
|
||||
@@ -177,8 +186,8 @@ export function warnCrossProviderDuplicates(provider: CLIProxyProvider): boolean
|
||||
}
|
||||
|
||||
console.error('');
|
||||
console.error(' Fix: pause duplicate with "ccs <provider> --pause <account>"');
|
||||
console.error(' or use separate Google accounts per provider.');
|
||||
console.error(' Immediate action: pause duplicate account and use separate Google accounts.');
|
||||
console.error(' Fix command: "ccs cliproxy pause <account> --provider <provider>"');
|
||||
console.error('');
|
||||
|
||||
return true;
|
||||
@@ -196,8 +205,19 @@ export function warnNewAccountConflict(
|
||||
console.error(
|
||||
` ${maskEmail(email)} is also registered under: ${conflictingProviders.join(', ')}`
|
||||
);
|
||||
console.error(' Concurrent usage may cause Google to ban your account.');
|
||||
console.error(
|
||||
' Reusing one Google account between "ccs gemini" and "ccs agy" can trigger bans.'
|
||||
);
|
||||
console.error(
|
||||
' This applies to both CLI auth and "ccs config" dashboard auth for these providers.'
|
||||
);
|
||||
console.error(' 403/Forbidden responses can be an early sign of account disablement.');
|
||||
console.error(
|
||||
' If you want to keep Google AI access, do not continue with this shared-account setup.'
|
||||
);
|
||||
console.error(
|
||||
' CCS is provided as-is and cannot take responsibility for suspension/ban/access-loss decisions.'
|
||||
);
|
||||
console.error(' Consider pausing the duplicate or using a different account.');
|
||||
console.error(` Details: ${ISSUE_509_URL}`);
|
||||
console.error('');
|
||||
@@ -215,15 +235,18 @@ export function warnOAuthBanRisk(provider: CLIProxyProvider): void {
|
||||
|
||||
shownBanWarnings.add(provider);
|
||||
console.error('');
|
||||
console.error(warn('Account safety warning (#509)'));
|
||||
console.error(warn('Account safety warning (#509 - read before continuing)'));
|
||||
console.error(
|
||||
' Using the same Google account in both "ccs gemini" and "ccs agy" can trigger suspension.'
|
||||
' Known risk: one Google account shared by "ccs gemini" + "ccs agy" can be disabled/banned.'
|
||||
);
|
||||
console.error(
|
||||
' If you see 403/Forbidden during provider calls, treat it as likely account disable/ban.'
|
||||
' This risk applies whether auth was done from CLI or from "ccs config" dashboard.'
|
||||
);
|
||||
console.error(
|
||||
' Use separate Google accounts per provider and stop retrying blocked accounts.'
|
||||
' If you want to keep Google AI access, do not continue with this shared-account setup.'
|
||||
);
|
||||
console.error(
|
||||
' CCS is provided as-is and cannot take responsibility for suspension/ban/access-loss decisions.'
|
||||
);
|
||||
console.error(` Details: ${ISSUE_509_URL}`);
|
||||
console.error('');
|
||||
@@ -247,11 +270,16 @@ export function warnPossible403Ban(provider: CLIProxyProvider, errorMessage: str
|
||||
}
|
||||
|
||||
console.error('');
|
||||
console.error(warn(`Account safety: ${provider} returned 403/Forbidden`));
|
||||
console.error(warn(`Account safety: ${provider} returned 403/Forbidden (possible disable/ban)`));
|
||||
console.error(
|
||||
' For gemini/agy flows this often means the Google account was blocked/disabled.'
|
||||
' For gemini/agy flows this often means Google blocked or disabled the account.'
|
||||
);
|
||||
console.error(
|
||||
' If you want to keep Google AI access, stop using this account/provider pairing immediately.'
|
||||
);
|
||||
console.error(
|
||||
' CCS is provided as-is and cannot take responsibility for suspension/ban/access-loss decisions.'
|
||||
);
|
||||
console.error(' Stop retries for this account and switch to a different account/provider.');
|
||||
console.error(` Details: ${ISSUE_509_URL}`);
|
||||
console.error(` Error: "${truncate(errorMessage, 160)}"`);
|
||||
console.error('');
|
||||
|
||||
@@ -30,6 +30,7 @@ interface RemoteAuthFile {
|
||||
export interface RemoteAccountInfo {
|
||||
id: string;
|
||||
email: string;
|
||||
provider: CLIProxyProvider;
|
||||
isDefault: boolean;
|
||||
status: 'active' | 'disabled' | 'unavailable';
|
||||
}
|
||||
@@ -124,6 +125,8 @@ function transformRemoteAuthFiles(files: RemoteAuthFile[]): RemoteAuthStatus[] {
|
||||
const accounts: RemoteAccountInfo[] = providerFiles.map((f, idx) => ({
|
||||
id: f.id,
|
||||
email: f.email || f.name || 'Unknown',
|
||||
// Keep provider on each account so UI account rendering can infer capabilities safely.
|
||||
provider,
|
||||
isDefault: idx === 0,
|
||||
status: f.status,
|
||||
}));
|
||||
|
||||
@@ -166,6 +166,9 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
'Zero-config OAuth authentication via CLIProxy Plus',
|
||||
'First run: Browser opens for authentication, then model selection',
|
||||
'Settings: ~/.ccs/{provider}.settings.json (created after auth)',
|
||||
'Safety: do not reuse one Google account across "ccs gemini" and "ccs agy" (issue #509)',
|
||||
'If you want to keep Google AI access, do not continue this shared-account setup',
|
||||
'CCS is as-is and does not take responsibility for account bans/access loss',
|
||||
],
|
||||
[
|
||||
['ccs gemini', 'Google Gemini (gemini-2.5-pro or 3-pro)'],
|
||||
|
||||
@@ -184,13 +184,7 @@ router.get('/accounts', async (_req: Request, res: Response): Promise<void> => {
|
||||
const target = getProxyTarget();
|
||||
if (target.isRemote) {
|
||||
const authStatus = await fetchRemoteAuthStatus(target);
|
||||
// Transform RemoteAuthStatus[] to account summary format
|
||||
const accounts = authStatus.flatMap((status) =>
|
||||
status.accounts.map((acc) => ({
|
||||
provider: status.provider,
|
||||
...acc,
|
||||
}))
|
||||
);
|
||||
const accounts = authStatus.flatMap((status) => status.accounts);
|
||||
res.json({ accounts, source: 'remote' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import { fetchRemoteAuthStatus } from '../../../src/cliproxy/remote-auth-fetcher';
|
||||
import type { ProxyTarget } from '../../../src/cliproxy/proxy-target-resolver';
|
||||
|
||||
describe('remote-auth-fetcher', () => {
|
||||
let originalFetch: typeof fetch;
|
||||
|
||||
const remoteTarget: ProxyTarget = {
|
||||
host: 'remote.example.com',
|
||||
port: 8317,
|
||||
protocol: 'https',
|
||||
authToken: 'token',
|
||||
isRemote: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
originalFetch = global.fetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it('includes provider on each remote account', async () => {
|
||||
global.fetch = mock((url: string) => {
|
||||
expect(url).toBe('https://remote.example.com:8317/v0/management/auth-files');
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
files: [
|
||||
{
|
||||
id: 'acc-codex',
|
||||
name: 'codex-main',
|
||||
type: 'oauth',
|
||||
provider: 'codex',
|
||||
email: 'codex@example.com',
|
||||
status: 'active',
|
||||
source: 'file',
|
||||
},
|
||||
],
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
);
|
||||
}) as typeof fetch;
|
||||
|
||||
const result = await fetchRemoteAuthStatus(remoteTarget);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]?.provider).toBe('codex');
|
||||
expect(result[0]?.accounts).toHaveLength(1);
|
||||
expect(result[0]?.accounts[0]?.provider).toBe('codex');
|
||||
expect(result[0]?.accounts[0]?.email).toBe('codex@example.com');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import { AlertTriangle, ExternalLink } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface AccountSafetyWarningCardProps {
|
||||
className?: string;
|
||||
showAcknowledgement?: boolean;
|
||||
acknowledged?: boolean;
|
||||
onAcknowledgedChange?: (value: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function AccountSafetyWarningCard({
|
||||
className,
|
||||
showAcknowledgement = false,
|
||||
acknowledged = false,
|
||||
onAcknowledgedChange,
|
||||
disabled = false,
|
||||
}: AccountSafetyWarningCardProps) {
|
||||
return (
|
||||
<section
|
||||
role="alert"
|
||||
className={cn(
|
||||
'relative overflow-hidden rounded-xl border border-amber-500/30 bg-gradient-to-br from-amber-50 via-background to-rose-50/70 shadow-sm dark:from-amber-950/20 dark:to-rose-950/20',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="absolute inset-x-0 top-0 h-0.5 bg-gradient-to-r from-amber-500 via-orange-500 to-rose-500" />
|
||||
|
||||
<div className="space-y-3 p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-start gap-2.5">
|
||||
<div className="mt-0.5 inline-flex h-7 w-7 items-center justify-center rounded-md bg-amber-500/15 text-amber-700 dark:text-amber-400">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold leading-5">Account Safety Warning</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Issue #509 · Shared Gemini + AGY account risk
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-amber-500/40 text-amber-700 dark:text-amber-300"
|
||||
>
|
||||
High Risk
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-sm leading-relaxed">
|
||||
<p>
|
||||
Using one Google account for both <code className="font-mono">ccs gemini</code> and{' '}
|
||||
<code className="font-mono">ccs agy</code> can trigger account disable/ban.
|
||||
</p>
|
||||
<p className="font-medium text-amber-900 dark:text-amber-200">
|
||||
If you want to keep Google AI access, do not continue this shared-account setup.
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
CCS is provided as-is and does not take responsibility for suspension, bans, or access
|
||||
loss from upstream providers.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<a
|
||||
href="https://github.com/kaitranntt/ccs/issues/509"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-1.5 rounded-md border border-amber-500/30 bg-amber-500/10 px-2.5 py-1 text-xs font-medium text-amber-800 transition-colors hover:bg-amber-500/15 dark:text-amber-200"
|
||||
>
|
||||
Read issue #509
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
<span className="rounded-md border border-border/70 bg-muted/60 px-2.5 py-1 text-xs text-muted-foreground">
|
||||
Applies to CLI and dashboard auth
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{showAcknowledgement && onAcknowledgedChange && (
|
||||
<div className="rounded-lg border border-amber-500/25 bg-amber-500/5 p-2.5">
|
||||
<div className="flex items-start gap-2">
|
||||
<Checkbox
|
||||
id="account-risk-ack"
|
||||
checked={acknowledged}
|
||||
onCheckedChange={(checked) => onAcknowledgedChange(Boolean(checked))}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Label htmlFor="account-risk-ack" className="text-xs leading-5">
|
||||
I understand this risk and that CCS takes no responsibility if I continue this
|
||||
setup.
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import { Loader2, ExternalLink, User, Download, Copy, Check } from 'lucide-react
|
||||
import { useKiroImport } from '@/hooks/use-cliproxy';
|
||||
import { useCliproxyAuthFlow } from '@/hooks/use-cliproxy-auth-flow';
|
||||
import { applyDefaultPreset } from '@/lib/preset-utils';
|
||||
import { AccountSafetyWarningCard } from '@/components/account/account-safety-warning-card';
|
||||
import {
|
||||
DEFAULT_KIRO_AUTH_METHOD,
|
||||
getKiroAuthMethodOption,
|
||||
@@ -59,12 +60,14 @@ export function AddAccountDialog({
|
||||
const [callbackUrl, setCallbackUrl] = useState('');
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
const [acknowledgedRisk, setAcknowledgedRisk] = useState(false);
|
||||
const [kiroAuthMethod, setKiroAuthMethod] = useState<KiroAuthMethod>(DEFAULT_KIRO_AUTH_METHOD);
|
||||
const wasAuthenticatingRef = useRef(false);
|
||||
const authFlow = useCliproxyAuthFlow();
|
||||
const kiroImportMutation = useKiroImport();
|
||||
|
||||
const isKiro = provider === 'kiro';
|
||||
const requiresSafetyAcknowledgement = provider === 'gemini' || provider === 'agy';
|
||||
const defaultDeviceCode = isDeviceCodeProvider(provider);
|
||||
const requiresNickname = isNicknameRequiredProvider(provider);
|
||||
const kiroMethodOption = getKiroAuthMethodOption(kiroAuthMethod);
|
||||
@@ -78,11 +81,19 @@ export function AddAccountDialog({
|
||||
setCallbackUrl('');
|
||||
setCopied(false);
|
||||
setLocalError(null);
|
||||
setAcknowledgedRisk(false);
|
||||
setKiroAuthMethod(DEFAULT_KIRO_AUTH_METHOD);
|
||||
wasAuthenticatingRef.current = false;
|
||||
onClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setAcknowledgedRisk(false);
|
||||
setLocalError(null);
|
||||
}
|
||||
}, [provider, open]);
|
||||
|
||||
// When authFlow completes successfully (polling detected success), apply preset and close
|
||||
useEffect(() => {
|
||||
if (!authFlow.isAuthenticating && !authFlow.error && authFlow.provider === null && open) {
|
||||
@@ -131,6 +142,12 @@ export function AddAccountDialog({
|
||||
* - Authorization code providers use /start-url and polling.
|
||||
*/
|
||||
const handleAuthenticate = () => {
|
||||
if (requiresSafetyAcknowledgement && !acknowledgedRisk) {
|
||||
setLocalError(
|
||||
'Please acknowledge the account safety warning before authenticating this provider.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (requiresNickname && !nicknameTrimmed) {
|
||||
setLocalError(`Nickname is required for ${displayName} accounts.`);
|
||||
return;
|
||||
@@ -187,6 +204,18 @@ export function AddAccountDialog({
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
{requiresSafetyAcknowledgement && !showAuthUI && (
|
||||
<AccountSafetyWarningCard
|
||||
showAcknowledgement
|
||||
acknowledged={acknowledgedRisk}
|
||||
onAcknowledgedChange={(value) => {
|
||||
setAcknowledgedRisk(value);
|
||||
setLocalError(null);
|
||||
}}
|
||||
disabled={isPending}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Kiro auth method */}
|
||||
{isKiro && !showAuthUI && (
|
||||
<div className="space-y-2">
|
||||
@@ -374,7 +403,11 @@ export function AddAccountDialog({
|
||||
{!showAuthUI && (
|
||||
<Button
|
||||
onClick={handleAuthenticate}
|
||||
disabled={isPending || (requiresNickname && !nicknameTrimmed)}
|
||||
disabled={
|
||||
isPending ||
|
||||
(requiresNickname && !nicknameTrimmed) ||
|
||||
(requiresSafetyAcknowledgement && !acknowledgedRisk)
|
||||
}
|
||||
>
|
||||
<ExternalLink className="w-4 h-4 mr-2" />
|
||||
Authenticate
|
||||
|
||||
@@ -105,7 +105,16 @@ export function ProviderEditor({
|
||||
missingRequiredFields,
|
||||
} = useProviderEditor(provider);
|
||||
|
||||
const accounts = authStatus.accounts || [];
|
||||
// Defensive normalization: remote/legacy payloads may omit account.provider.
|
||||
// Fallback to current editor provider to avoid runtime crashes in account UI.
|
||||
const accounts = useMemo(
|
||||
() =>
|
||||
(authStatus.accounts || []).map((account) => ({
|
||||
...account,
|
||||
provider: account.provider || baseProvider || provider,
|
||||
})),
|
||||
[authStatus.accounts, baseProvider, provider]
|
||||
);
|
||||
|
||||
// Fetch effective API key for presets (uses configured value, not hardcoded)
|
||||
const { data: authTokens } = useQuery<{ apiKey: { value: string } }>({
|
||||
|
||||
@@ -8,12 +8,12 @@ import { useState, useMemo } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Check, X, RefreshCw, Sparkles, Zap, GitBranch, Trash2, AlertTriangle } from 'lucide-react';
|
||||
import { Check, X, RefreshCw, Sparkles, Zap, GitBranch, Trash2 } from 'lucide-react';
|
||||
import { QuickSetupWizard } from '@/components/quick-setup-wizard';
|
||||
import { AddAccountDialog } from '@/components/account/add-account-dialog';
|
||||
import { AccountSafetyWarningCard } from '@/components/account/account-safety-warning-card';
|
||||
import { ProviderEditor } from '@/components/cliproxy/provider-editor';
|
||||
import { ProviderLogo } from '@/components/cliproxy/provider-logo';
|
||||
import { ProxyStatusWidget } from '@/components/monitoring/proxy-status-widget';
|
||||
@@ -394,29 +394,7 @@ export function CliproxyPage() {
|
||||
|
||||
{/* Right Panel */}
|
||||
<div className="flex-1 flex flex-col min-w-0 bg-background">
|
||||
{showAccountSafetyWarning && (
|
||||
<div className="px-4 pt-4">
|
||||
<Alert variant="warning" className="py-2">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>Account Safety Warning (Issue #509)</AlertTitle>
|
||||
<AlertDescription>
|
||||
Using the same Google account in both <code className="font-mono">ccs gemini</code>{' '}
|
||||
and <code className="font-mono">ccs agy</code> can trigger suspension. If CLI shows{' '}
|
||||
<code className="font-mono">403 Forbidden</code>, treat it as likely account
|
||||
disable/ban and switch accounts.{' '}
|
||||
<a
|
||||
href="https://github.com/kaitranntt/ccs/issues/509"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline font-medium"
|
||||
>
|
||||
Read issue #509
|
||||
</a>
|
||||
.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
{showAccountSafetyWarning && <AccountSafetyWarningCard className="mx-4 mt-4" />}
|
||||
|
||||
{selectedVariantData && parentAuthForVariant ? (
|
||||
// Variant selected - show ProviderEditor with variant profile name
|
||||
|
||||
Reference in New Issue
Block a user