Skip to content

Commit 462d98f

Browse files
committed
fix: recover Verboo auth sessions and harden release bundle
1 parent a621647 commit 462d98f

11 files changed

Lines changed: 1477 additions & 766 deletions

File tree

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ jobs:
6565
run: |
6666
bun test --max-concurrency=1 \
6767
src/commands/effort/effort.verboo.test.ts \
68+
src/commands/logout/logoutState.test.ts \
6869
src/services/api/openaiShim.test.ts \
6970
src/services/api/verbooCheckout.test.ts \
7071
src/services/api/verbooModels.test.ts \

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@verboo/code",
3-
"version": "0.13.0",
3+
"version": "0.13.1",
44
"description": "Verboo Code — coding agent for the Verboo platform",
55
"type": "module",
66
"bin": {

scripts/build.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,21 @@ if (!result.success) {
453453
process.exitCode = 1
454454
} else {
455455
console.log(`✓ Built verboo v${version} → dist/cli.mjs`) // VERBOO-BRAND
456+
457+
// Bun can report a successful bundle even when an interrupted/corrupted
458+
// artifact would fail to load under the Node runtime used by npm installs.
459+
// Parse the exact distributable before continuing to the publishable SDK.
460+
console.log('Validating CLI bundle syntax...')
461+
const syntaxCheck = Bun.spawnSync(['node', '--check', './dist/cli.mjs'], {
462+
stdout: 'ignore',
463+
stderr: 'inherit',
464+
})
465+
if (syntaxCheck.exitCode !== 0) {
466+
console.error('CLI bundle syntax validation failed.')
467+
process.exitCode = 1
468+
} else {
469+
console.log('✓ CLI bundle: Node.js syntax check passed')
470+
}
456471
}
457472

458473
// ── SDK Bundle Build ──────────────────────────────────────────────────────

src/cli/handlers/auth.ts

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ import {
5050
buildAccountProperties,
5151
buildAPIProviderProperties,
5252
} from '../../utils/status.js'
53-
import { getActiveScopes, getOauthConfig, isVerbooMode } from '../../constants/oauth.js'
53+
import {
54+
getActiveScopes,
55+
getOauthConfig,
56+
isVerbooMode,
57+
} from '../../constants/oauth.js'
5458
import {
5559
generateCodeChallenge,
5660
generateCodeVerifier,
@@ -103,14 +107,14 @@ export async function installOAuthTokens(tokens: OAuthTokens): Promise<void> {
103107

104108
// Roles and first-token-date may fail for limited-scope tokens (e.g.
105109
// inference-only from setup-token). They're not required for core auth.
106-
await fetchAndStoreUserRoles(tokens.accessToken).catch(err =>
110+
await fetchAndStoreUserRoles(tokens.accessToken).catch((err) =>
107111
logForDebugging(String(err), { level: 'error' }),
108112
)
109113

110114
const isVerbooOAuth = isVerbooMode()
111115

112116
if (!isVerbooOAuth && shouldUseClaudeAIAuth(tokens.scopes)) {
113-
await fetchAndStoreClaudeCodeFirstTokenDate().catch(err =>
117+
await fetchAndStoreClaudeCodeFirstTokenDate().catch((err) =>
114118
logForDebugging(String(err), { level: 'error' }),
115119
)
116120
} else if (!isVerbooOAuth) {
@@ -156,7 +160,7 @@ export async function runOAuthLoginFlow(
156160
})
157161

158162
return await oauthService.startOAuthFlow(
159-
async url => {
163+
async (url) => {
160164
if (opts.onAuthUrl) {
161165
await opts.onAuthUrl(url)
162166
} else {
@@ -232,7 +236,7 @@ export async function authLogin({
232236

233237
// Mark onboarding complete — interactive paths handle this via
234238
// the Onboarding component, but the env var path skips it.
235-
saveGlobalConfig(current => {
239+
saveGlobalConfig((current) => {
236240
if (current.hasCompletedOnboarding) return current
237241
return { ...current, hasCompletedOnboarding: true }
238242
})
@@ -394,14 +398,26 @@ export async function authStatus(opts: {
394398
}
395399

396400
export async function authLogout(): Promise<void> {
401+
let result
397402
try {
398-
await performLogout({ clearOnboarding: false })
403+
result = await performLogout({ clearOnboarding: false })
399404
} catch {
400405
process.stderr.write('Failed to log out.\n')
401406
process.exit(1)
402407
}
403-
// VERBOO-BRAND
404-
process.stdout.write('Successfully logged out from your Verboo account.\n')
408+
process.stdout.write(
409+
'Successfully logged out from your Verboo account locally.\n',
410+
)
411+
if (result.remoteRevocation === 'unconfirmed') {
412+
process.stderr.write(
413+
'Could not confirm server-side session revocation. Your local credentials were removed.\n',
414+
)
415+
}
416+
if (result.externalTokenSource) {
417+
process.stderr.write(
418+
`A credential from ${result.externalTokenSource} is still active in this shell. Remove it before starting Verboo again.\n`,
419+
)
420+
}
405421
process.exit(0)
406422
}
407423

@@ -412,25 +428,27 @@ const HEADLESS_REDIRECT_URI = 'https://code.verboo.ai/pt/cli-auth/manual'
412428
export async function authLoginHeadless(): Promise<void> {
413429
const config = getOauthConfig()
414430

415-
const codeVerifier = generateCodeVerifier()
431+
const codeVerifier = generateCodeVerifier()
416432
const codeChallenge = await generateCodeChallenge(codeVerifier)
417-
const state = generateState()
433+
const state = generateState()
418434

419435
const params = new URLSearchParams({
420-
client_id: config.CLIENT_ID,
421-
redirect_uri: HEADLESS_REDIRECT_URI,
422-
response_type: 'code',
423-
code_challenge: codeChallenge,
436+
client_id: config.CLIENT_ID,
437+
redirect_uri: HEADLESS_REDIRECT_URI,
438+
response_type: 'code',
439+
code_challenge: codeChallenge,
424440
code_challenge_method: 'S256',
425441
state,
426-
scope: getActiveScopes().join(' '),
442+
scope: getActiveScopes().join(' '),
427443
})
428444

429445
const url = `${config.CONSOLE_AUTHORIZE_URL}?${params}`
430446

431447
process.stdout.write('\nAbra este link em qualquer navegador:\n\n')
432448
process.stdout.write(` ${url}\n\n`)
433-
process.stdout.write('Após autorizar, cole o código exibido e pressione Enter: ')
449+
process.stdout.write(
450+
'Após autorizar, cole o código exibido e pressione Enter: ',
451+
)
434452

435453
const code = (await readOneLine()).trim()
436454
if (!code) {
@@ -439,14 +457,18 @@ export async function authLoginHeadless(): Promise<void> {
439457
}
440458

441459
try {
442-
const tokenResponse = await exchangeCodeForTokensWithUri(code, codeVerifier, HEADLESS_REDIRECT_URI)
460+
const tokenResponse = await exchangeCodeForTokensWithUri(
461+
code,
462+
codeVerifier,
463+
HEADLESS_REDIRECT_URI,
464+
)
443465
const tokens: OAuthTokens = {
444-
accessToken: tokenResponse.access_token,
445-
refreshToken: tokenResponse.refresh_token ?? null,
446-
expiresAt: Date.now() + (tokenResponse.expires_in ?? 900) * 1000,
447-
scopes: tokenResponse.scope?.split(' ').filter(Boolean) ?? [],
466+
accessToken: tokenResponse.access_token,
467+
refreshToken: tokenResponse.refresh_token ?? null,
468+
expiresAt: Date.now() + (tokenResponse.expires_in ?? 900) * 1000,
469+
scopes: tokenResponse.scope?.split(' ').filter(Boolean) ?? [],
448470
subscriptionType: null,
449-
rateLimitTier: null,
471+
rateLimitTier: null,
450472
}
451473
await installOAuthTokens(tokens)
452474
process.stdout.write('Login realizado com sucesso.\n')
@@ -458,8 +480,14 @@ export async function authLoginHeadless(): Promise<void> {
458480
}
459481

460482
function readOneLine(): Promise<string> {
461-
return new Promise(resolve => {
462-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
463-
rl.once('line', line => { rl.close(); resolve(line) })
483+
return new Promise((resolve) => {
484+
const rl = readline.createInterface({
485+
input: process.stdin,
486+
output: process.stdout,
487+
})
488+
rl.once('line', (line) => {
489+
rl.close()
490+
resolve(line)
491+
})
464492
})
465493
}

src/commands/logout/logout.tsx

Lines changed: 104 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,136 @@
1-
import * as React from 'react';
2-
import { clearTrustedDeviceTokenCache } from '../../bridge/trustedDevice.js';
3-
import { Text } from '../../ink.js';
4-
import { refreshGrowthBookAfterAuthChange } from '../../services/analytics/growthbook.js';
5-
import { getGroveNoticeConfig, getGroveSettings } from '../../services/api/grove.js';
6-
import { clearPolicyLimitsCache } from '../../services/policyLimits/index.js';
7-
import { clearRemoteManagedSettingsCache } from '../../services/remoteManagedSettings/index.js';
1+
import * as React from 'react'
2+
import { clearTrustedDeviceTokenCache } from '../../bridge/trustedDevice.js'
3+
import { Text } from '../../ink.js'
4+
import { refreshGrowthBookAfterAuthChange } from '../../services/analytics/growthbook.js'
5+
import {
6+
getGroveNoticeConfig,
7+
getGroveSettings,
8+
} from '../../services/api/grove.js'
9+
import { clearPolicyLimitsCache } from '../../services/policyLimits/index.js'
10+
import { clearRemoteManagedSettingsCache } from '../../services/remoteManagedSettings/index.js'
811
import { isVerbooMode } from '../../constants/oauth.js'
912
import { clearVerbooModelsCache } from '../../services/api/verbooModels.js'
1013
import { resetVerbooSessionValidation } from '../../services/oauth/verbooStartupAuth.js'
11-
import { getClaudeAIOAuthTokens, removeApiKey } from '../../utils/auth.js';
12-
import { clearBetasCaches } from '../../utils/betas.js';
13-
import { saveGlobalConfig } from '../../utils/config.js';
14-
import { gracefulShutdownSync } from '../../utils/gracefulShutdown.js';
15-
import { getSecureStorage } from '../../utils/secureStorage/index.js';
16-
import { clearToolSchemaCache } from '../../utils/toolSchemaCache.js';
17-
import { resetUserCache } from '../../utils/user.js';
14+
import { revokeVerbooRefreshToken } from '../../services/oauth/client.js'
15+
import {
16+
getAuthTokenSource,
17+
getClaudeAIOAuthTokens,
18+
removeApiKey,
19+
} from '../../utils/auth.js'
20+
import { clearBetasCaches } from '../../utils/betas.js'
21+
import { saveGlobalConfig } from '../../utils/config.js'
22+
import { gracefulShutdownSync } from '../../utils/gracefulShutdown.js'
23+
import { getSecureStorage } from '../../utils/secureStorage/index.js'
24+
import { clearToolSchemaCache } from '../../utils/toolSchemaCache.js'
25+
import { resetUserCache } from '../../utils/user.js'
26+
import { removeStoredVerbooOauth } from './logoutState.js'
27+
export type LogoutResult = {
28+
localCleared: boolean
29+
remoteRevocation: 'revoked' | 'unconfirmed' | 'not_applicable'
30+
externalTokenSource?: string
31+
}
32+
33+
function isExternallyInjectedTokenSource(source: string): boolean {
34+
return [
35+
'CLAUDE_CODE_OAUTH_TOKEN',
36+
'CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR',
37+
'CCR_OAUTH_TOKEN_FILE',
38+
'ANTHROPIC_AUTH_TOKEN',
39+
].includes(source)
40+
}
41+
1842
export async function performLogout({
19-
clearOnboarding = false
20-
}): Promise<void> {
21-
await removeApiKey();
43+
clearOnboarding = false,
44+
}): Promise<LogoutResult> {
45+
const authTokenSource = getAuthTokenSource().source
46+
const refreshToken = removeStoredVerbooOauth(getSecureStorage())
47+
const remoteRevocation = refreshToken
48+
? (await revokeVerbooRefreshToken(refreshToken))
49+
? ('revoked' as const)
50+
: ('unconfirmed' as const)
51+
: ('not_applicable' as const)
52+
53+
// Verboo does not use the generic Anthropic API-key slot. Preserve it and
54+
// credentials for MCP, Codex, and plugins when the user signs out of Verboo.
55+
if (!isVerbooMode()) {
56+
await removeApiKey()
57+
}
2258

2359
if (isVerbooMode()) {
24-
resetVerbooSessionValidation();
25-
clearVerbooModelsCache();
60+
resetVerbooSessionValidation()
61+
clearVerbooModelsCache()
2662
}
2763

28-
// Wipe all secure storage data on logout
29-
const secureStorage = getSecureStorage();
30-
secureStorage.delete();
31-
await clearAuthRelatedCaches();
32-
saveGlobalConfig(current => {
64+
await clearAuthRelatedCaches()
65+
saveGlobalConfig((current) => {
3366
const updated = {
34-
...current
35-
};
67+
...current,
68+
}
3669
if (clearOnboarding) {
37-
updated.hasCompletedOnboarding = false;
38-
updated.subscriptionNoticeCount = 0;
39-
updated.hasAvailableSubscription = false;
70+
updated.hasCompletedOnboarding = false
71+
updated.subscriptionNoticeCount = 0
72+
updated.hasAvailableSubscription = false
4073
if (updated.customApiKeyResponses?.approved) {
4174
updated.customApiKeyResponses = {
4275
...updated.customApiKeyResponses,
43-
approved: []
44-
};
76+
approved: [],
77+
}
4578
}
4679
}
47-
updated.oauthAccount = undefined;
48-
return updated;
49-
});
80+
updated.oauthAccount = undefined
81+
return updated
82+
})
83+
84+
return {
85+
localCleared: true,
86+
remoteRevocation,
87+
...(isExternallyInjectedTokenSource(authTokenSource)
88+
? { externalTokenSource: authTokenSource }
89+
: {}),
90+
}
5091
}
5192

5293
// clearing anything memoized that must be invalidated when user/session/auth changes
5394
export async function clearAuthRelatedCaches(): Promise<void> {
5495
// Clear the OAuth token cache
55-
getClaudeAIOAuthTokens.cache?.clear?.();
56-
clearTrustedDeviceTokenCache();
57-
clearBetasCaches();
58-
clearToolSchemaCache();
96+
getClaudeAIOAuthTokens.cache?.clear?.()
97+
clearTrustedDeviceTokenCache()
98+
clearBetasCaches()
99+
clearToolSchemaCache()
59100

60101
// Clear user data cache BEFORE GrowthBook refresh so it picks up fresh credentials
61-
resetUserCache();
62-
refreshGrowthBookAfterAuthChange();
102+
resetUserCache()
103+
refreshGrowthBookAfterAuthChange()
63104

64105
// Clear Grove config cache
65-
getGroveNoticeConfig.cache?.clear?.();
66-
getGroveSettings.cache?.clear?.();
106+
getGroveNoticeConfig.cache?.clear?.()
107+
getGroveSettings.cache?.clear?.()
67108

68109
// Clear remotely managed settings cache
69-
await clearRemoteManagedSettingsCache();
110+
await clearRemoteManagedSettingsCache()
70111

71112
// Clear policy limits cache
72-
await clearPolicyLimitsCache();
113+
await clearPolicyLimitsCache()
73114
}
74115
export async function call(): Promise<React.ReactNode> {
75-
await performLogout({
76-
clearOnboarding: true
77-
});
78-
const message = isVerbooMode()
79-
? <Text>Saiu da conta Verboo com sucesso.</Text>
80-
: <Text>Successfully logged out from your Anthropic account.</Text>;
116+
const result = await performLogout({
117+
clearOnboarding: true,
118+
})
119+
const message = isVerbooMode() ? (
120+
<Text>
121+
Saiu da conta Verboo localmente.
122+
{result.remoteRevocation === 'unconfirmed'
123+
? ' Não foi possível confirmar a revogação da sessão no servidor.'
124+
: ''}
125+
{result.externalTokenSource
126+
? ` ${result.externalTokenSource} ainda fornece uma credencial neste ambiente.`
127+
: ''}
128+
</Text>
129+
) : (
130+
<Text>Successfully logged out from your Anthropic account.</Text>
131+
)
81132
setTimeout(() => {
82-
gracefulShutdownSync(0, 'logout');
83-
}, 200);
84-
return message;
133+
gracefulShutdownSync(0, 'logout')
134+
}, 200)
135+
return message
85136
}

0 commit comments

Comments
 (0)