diff --git a/src/ipc/database/cloudHandler.ts b/src/ipc/database/cloudHandler.ts index 2f59485..a64e934 100644 --- a/src/ipc/database/cloudHandler.ts +++ b/src/ipc/database/cloudHandler.ts @@ -85,6 +85,7 @@ function ensureDatabaseInitialized(dbPath: string): void { created_at INTEGER NOT NULL, last_used INTEGER NOT NULL, status TEXT DEFAULT 'active', + status_reason TEXT, is_active INTEGER DEFAULT 0 ); `); @@ -96,6 +97,7 @@ function ensureDatabaseInitialized(dbPath: string): void { const hasDeviceProfileJson = tableInfo.some((col) => col.name === 'device_profile_json'); const hasDeviceHistoryJson = tableInfo.some((col) => col.name === 'device_history_json'); const hasProxyUrl = tableInfo.some((col) => col.name === 'proxy_url'); + const hasStatusReason = tableInfo.some((col) => col.name === 'status_reason'); if (!hasIsActive) { db.exec('ALTER TABLE accounts ADD COLUMN is_active INTEGER DEFAULT 0'); } @@ -108,6 +110,9 @@ function ensureDatabaseInitialized(dbPath: string): void { if (!hasProxyUrl) { db.exec('ALTER TABLE accounts ADD COLUMN proxy_url TEXT'); } + if (!hasStatusReason) { + db.exec('ALTER TABLE accounts ADD COLUMN status_reason TEXT'); + } // Create index on email for faster lookups // Create index on email for faster lookups @@ -479,6 +484,7 @@ export class CloudAccountRepo { createdAt: account.created_at, lastUsed: account.last_used, status: account.status || 'active', + statusReason: account.status_reason ?? null, isActive: account.is_active ? 1 : 0, proxyUrl: account.proxy_url ?? null, }; @@ -595,6 +601,7 @@ export class CloudAccountRepo { created_at: normalizedRow.createdAt, last_used: normalizedRow.lastUsed, status: (normalizedRow.status as CloudAccount['status']) ?? undefined, + status_reason: normalizedRow.statusReason ?? undefined, is_active: Boolean(normalizedRow.isActive), proxy_url: normalizedRow.proxyUrl ?? undefined, }); @@ -681,13 +688,14 @@ export class CloudAccountRepo { email: normalizedRow.email, name: normalizedRow.name ?? undefined, avatar_url: normalizedRow.avatarUrl ?? undefined, - token: JSON.parse(tokenResult.value), + token: JSON.parse(tokenValue), quota: quotaResult.value ? JSON.parse(quotaResult.value) : undefined, device_profile: parseDeviceProfileColumn(normalizedRow.deviceProfileJson), device_history: parseDeviceHistoryColumn(normalizedRow.deviceHistoryJson), created_at: normalizedRow.createdAt, last_used: normalizedRow.lastUsed, status: (normalizedRow.status as CloudAccount['status']) ?? undefined, + status_reason: normalizedRow.statusReason ?? undefined, is_active: Boolean(normalizedRow.isActive), proxy_url: normalizedRow.proxyUrl ?? undefined, }; @@ -985,6 +993,26 @@ export class CloudAccountRepo { } } + static async setAccountStatus( + id: string, + status: CloudAccount['status'], + reason?: string | null, + ): Promise { + const { raw, orm } = getCloudDb(); + try { + orm + .update(accounts) + .set({ + status, + statusReason: reason?.trim() ? reason.trim() : null, + }) + .where(eq(accounts.id, id)) + .run(); + } finally { + raw.close(); + } + } + static async getAccountByEmail(email: string): Promise { const all = await this.getAccounts(); return all.find((a) => a.email.toLowerCase() === email.toLowerCase()) || null; @@ -1030,10 +1058,35 @@ export class CloudAccountRepo { account.token.access_token, account.token.refresh_token, account.token.expiry_timestamp, + account.token.is_gcp_tos ?? true, ); + const userStatusPayload = ProtobufUtils.createMinimalUserStatusPayload(account.email); + const userStatusEntry = ProtobufUtils.createUnifiedStateEntry( + 'userStatusSentinelKey', + userStatusPayload, + ); + const normalizedProjectId = account.token.project_id?.trim(); orm.transaction((tx) => { this.upsertItemValue(tx, 'antigravityUnifiedStateSync.oauthToken', oauthToken); + this.upsertItemValue(tx, 'antigravityUnifiedStateSync.userStatus', userStatusEntry); + if (normalizedProjectId) { + const projectPayload = ProtobufUtils.createStringValuePayload(normalizedProjectId); + const projectEntry = ProtobufUtils.createUnifiedStateEntry( + 'enterpriseGcpProjectId', + projectPayload, + ); + this.upsertItemValue( + tx, + 'antigravityUnifiedStateSync.enterprisePreferences', + projectEntry, + ); + } else { + tx + .delete(itemTable) + .where(eq(itemTable.key, 'antigravityUnifiedStateSync.enterprisePreferences')) + .run(); + } this.writeAuthStatusAndCleanup(tx, account); }); } @@ -1252,7 +1305,9 @@ export class CloudAccountRepo { private static readTokenInfoFromDb(db: DrizzleExecutor): { accessToken: string; refreshToken: string; + projectId?: string; } { + const enterpriseProjectId = this.readEnterpriseProjectIdFromDb(db); const unifiedValue = this.getItemValue( db, 'antigravityUnifiedStateSync.oauthToken', @@ -1296,12 +1351,16 @@ export class CloudAccountRepo { throw new Error(errorMsg); } - return tokenInfo; + return { + ...tokenInfo, + projectId: enterpriseProjectId, + }; } private static readTokenInfoWithRetry(dbPath: string): { accessToken: string; refreshToken: string; + projectId?: string; } { let lastError: unknown; for (let attempt = 1; attempt <= SQLITE_MAX_RETRIES; attempt += 1) { @@ -1323,6 +1382,41 @@ export class CloudAccountRepo { throw lastError; } + private static readEnterpriseProjectIdFromDb(db: DrizzleExecutor): string | undefined { + const enterprisePreferencesValue = this.getItemValue( + db, + 'antigravityUnifiedStateSync.enterprisePreferences', + 'ide.itemTable.antigravityUnifiedStateSync.enterprisePreferences', + ); + if (!enterprisePreferencesValue) { + return undefined; + } + + try { + const { sentinelKey, payload } = ProtobufUtils.decodeUnifiedStateEntry( + enterprisePreferencesValue, + ); + if (sentinelKey !== 'enterpriseGcpProjectId') { + return undefined; + } + + const projectBytes = ProtobufUtils.getField(payload, 3); + if (!projectBytes) { + return undefined; + } + + const projectId = ProtobufUtils.readString(projectBytes).trim(); + if (projectId === '') { + return undefined; + } + + return projectId; + } catch (error) { + logger.warn('SyncLocal: Failed to parse enterprise project preference', error); + return undefined; + } + } + static async syncFromIDE(): Promise { // Try all possible database paths const dbPaths = getAntigravityDbPaths(); @@ -1372,6 +1466,8 @@ export class CloudAccountRepo { expiry_timestamp: now + 3600, token_type: 'Bearer', email: userInfo.email, + project_id: tokenInfo.projectId, + is_gcp_tos: true, }, created_at: now, last_used: now, @@ -1383,8 +1479,28 @@ export class CloudAccountRepo { const accounts = await this.getAccounts(); const existing = accounts.find((a) => a.email === account.email); if (existing) { + const existingProjectId = existing.token.project_id?.trim(); + account.id = existing.id; // Keep existing ID account.created_at = existing.created_at; + account.name = account.name ?? existing.name; + account.avatar_url = account.avatar_url ?? existing.avatar_url; + account.proxy_url = existing.proxy_url; + account.device_profile = existing.device_profile; + account.device_history = existing.device_history; + account.status = 'active'; + account.status_reason = undefined; + account.token = { + ...existing.token, + access_token: tokenInfo.accessToken, + refresh_token: tokenInfo.refreshToken || existing.token.refresh_token, + expires_in: 3600, + expiry_timestamp: now + 3600, + token_type: 'Bearer', + email: userInfo.email, + project_id: existingProjectId || tokenInfo.projectId, + is_gcp_tos: existing.token.is_gcp_tos ?? true, + }; } await this.addAccount(account); diff --git a/src/ipc/database/schema.ts b/src/ipc/database/schema.ts index c7c04e9..f411204 100644 --- a/src/ipc/database/schema.ts +++ b/src/ipc/database/schema.ts @@ -13,6 +13,7 @@ export const accounts = sqliteTable('accounts', { createdAt: integer('created_at').notNull(), lastUsed: integer('last_used').notNull(), status: text('status', { enum: ['active', 'rate_limited', 'expired'] }).default('active'), + statusReason: text('status_reason'), isActive: integer('is_active').notNull().default(0), proxyUrl: text('proxy_url'), }); diff --git a/src/tests/unit/cloudHandler-sync.test.ts b/src/tests/unit/cloudHandler-sync.test.ts index 6552bad..2757a0f 100644 --- a/src/tests/unit/cloudHandler-sync.test.ts +++ b/src/tests/unit/cloudHandler-sync.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { ProtobufUtils } from '../../utils/protobuf'; import { CloudAccountRepo } from '../../ipc/database/cloudHandler'; import type { UserInfo } from '../../services/GoogleAPIService'; +import { toSyncLocalAccountORPCError } from '../../ipc/cloud/router'; let mockData: Record; let busyOnFirstGet = false; @@ -168,6 +169,34 @@ describe('CloudAccountRepo.syncFromIDE', () => { expect(account?.email).toBe('new@example.com'); }); + it('reads enterprise project preference from IDE unified state when syncing new account', async () => { + const accessToken = 'access-enterprise'; + const refreshToken = 'refresh-enterprise'; + const unifiedB64 = ProtobufUtils.createUnifiedOAuthToken(accessToken, refreshToken, 1700000000); + const projectPayload = ProtobufUtils.createStringValuePayload('enterprise-project-1'); + const enterprisePreferenceB64 = ProtobufUtils.createUnifiedStateEntry( + 'enterpriseGcpProjectId', + projectPayload, + ); + + mockData['antigravityUnifiedStateSync.oauthToken'] = unifiedB64; + mockData['antigravityUnifiedStateSync.enterprisePreferences'] = enterprisePreferenceB64; + + const { GoogleAPIService } = await import('../../services/GoogleAPIService'); + vi.mocked(GoogleAPIService.getUserInfo).mockResolvedValue( + createMockUserInfo('enterprise@example.com', 'Enterprise User'), + ); + + vi.spyOn(CloudAccountRepo, 'getAccounts').mockResolvedValue([]); + vi.spyOn(CloudAccountRepo, 'addAccount').mockResolvedValue(); + + const account = await CloudAccountRepo.syncFromIDE(); + + expect(GoogleAPIService.getUserInfo).toHaveBeenCalledWith(accessToken); + expect(account?.email).toBe('enterprise@example.com'); + expect(account?.token.project_id).toBe('enterprise-project-1'); + }); + it('should fall back to old oauth token when unified is missing', async () => { const accessToken = 'access-old'; const refreshToken = 'refresh-old'; @@ -191,6 +220,191 @@ describe('CloudAccountRepo.syncFromIDE', () => { expect(account?.email).toBe('old@example.com'); }); + it('preserves existing token metadata and proxy settings when syncing existing account', async () => { + const accessToken = 'access-updated'; + const refreshToken = 'refresh-updated'; + const oldB64 = Buffer.from( + ProtobufUtils.createOAuthTokenInfo(accessToken, refreshToken, 1700000000), + ).toString('base64'); + + mockData['jetskiStateSync.agentManagerInitState'] = oldB64; + + const { GoogleAPIService } = await import('../../services/GoogleAPIService'); + vi.mocked(GoogleAPIService.getUserInfo).mockResolvedValue( + createMockUserInfo('existing@example.com', 'Existing User'), + ); + + const existingAccount = { + id: 'existing-id', + provider: 'google' as const, + email: 'existing@example.com', + name: 'Existing User', + avatar_url: 'https://example.com/avatar.png', + token: { + access_token: 'old-access', + refresh_token: 'old-refresh', + expires_in: 3600, + expiry_timestamp: 1699999999, + token_type: 'Bearer', + email: 'existing@example.com', + project_id: 'project-keep', + oauth_client_key: 'custom-client', + session_id: 'session-keep', + upstream_proxy_url: 'http://127.0.0.1:8080', + }, + quota: undefined, + device_profile: undefined, + device_history: undefined, + created_at: 1690000000, + last_used: 1690000100, + status: 'active' as const, + is_active: false, + proxy_url: 'http://127.0.0.1:7890', + }; + + vi.spyOn(CloudAccountRepo, 'getAccounts').mockResolvedValue([existingAccount]); + const addAccountSpy = vi.spyOn(CloudAccountRepo, 'addAccount').mockResolvedValue(); + + const account = await CloudAccountRepo.syncFromIDE(); + + expect(account?.id).toBe('existing-id'); + expect(account?.token.project_id).toBe('project-keep'); + expect(account?.token.oauth_client_key).toBe('custom-client'); + expect(account?.token.session_id).toBe('session-keep'); + expect(account?.token.upstream_proxy_url).toBe('http://127.0.0.1:8080'); + expect(account?.proxy_url).toBe('http://127.0.0.1:7890'); + + expect(addAccountSpy).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'existing-id', + token: expect.objectContaining({ + access_token: accessToken, + refresh_token: refreshToken, + project_id: 'project-keep', + oauth_client_key: 'custom-client', + }), + proxy_url: 'http://127.0.0.1:7890', + }), + ); + }); + + it('should recover project_id from IDE enterprise preferences when existing project_id is blank', async () => { + const accessToken = 'access-blank-project'; + const refreshToken = 'refresh-blank-project'; + const unifiedB64 = ProtobufUtils.createUnifiedOAuthToken( + accessToken, + refreshToken, + 1700000000, + true, + ); + const enterprisePreferenceB64 = ProtobufUtils.createUnifiedStateEntry( + 'enterpriseGcpProjectId', + ProtobufUtils.createStringValuePayload('enterprise-project-recovered'), + ); + + mockData['antigravityUnifiedStateSync.oauthToken'] = unifiedB64; + mockData['antigravityUnifiedStateSync.enterprisePreferences'] = enterprisePreferenceB64; + + const { GoogleAPIService } = await import('../../services/GoogleAPIService'); + vi.mocked(GoogleAPIService.getUserInfo).mockResolvedValue( + createMockUserInfo('existing@example.com', 'Existing User'), + ); + + const existingAccount = { + id: 'existing-id', + provider: 'google' as const, + email: 'existing@example.com', + name: 'Existing User', + avatar_url: 'https://example.com/avatar.png', + token: { + access_token: 'old-access', + refresh_token: 'old-refresh', + expires_in: 3600, + expiry_timestamp: 1699999999, + token_type: 'Bearer', + email: 'existing@example.com', + project_id: ' ', + oauth_client_key: 'custom-client', + }, + quota: undefined, + device_profile: undefined, + device_history: undefined, + created_at: 1690000000, + last_used: 1690000100, + status: 'active' as const, + is_active: false, + proxy_url: 'http://127.0.0.1:7890', + }; + + vi.spyOn(CloudAccountRepo, 'getAccounts').mockResolvedValue([existingAccount]); + const addAccountSpy = vi.spyOn(CloudAccountRepo, 'addAccount').mockResolvedValue(); + + const account = await CloudAccountRepo.syncFromIDE(); + + expect(account?.token.project_id).toBe('enterprise-project-recovered'); + expect(addAccountSpy).toHaveBeenCalledWith( + expect.objectContaining({ + token: expect.objectContaining({ + project_id: 'enterprise-project-recovered', + }), + }), + ); + }); + + it('should reset stale blocked status after successful IDE resync', async () => { + const accessToken = 'access-status-reset'; + const refreshToken = 'refresh-status-reset'; + const oldB64 = Buffer.from( + ProtobufUtils.createOAuthTokenInfo(accessToken, refreshToken, 1700000000), + ).toString('base64'); + + mockData['jetskiStateSync.agentManagerInitState'] = oldB64; + + const { GoogleAPIService } = await import('../../services/GoogleAPIService'); + vi.mocked(GoogleAPIService.getUserInfo).mockResolvedValue( + createMockUserInfo('existing@example.com', 'Existing User'), + ); + + const existingAccount = { + id: 'existing-id', + provider: 'google' as const, + email: 'existing@example.com', + name: 'Existing User', + avatar_url: 'https://example.com/avatar.png', + token: { + access_token: 'old-access', + refresh_token: 'old-refresh', + expires_in: 3600, + expiry_timestamp: 1699999999, + token_type: 'Bearer', + email: 'existing@example.com', + }, + quota: undefined, + device_profile: undefined, + device_history: undefined, + created_at: 1690000000, + last_used: 1690000100, + status: 'rate_limited' as const, + status_reason: 'RESOURCE_EXHAUSTED', + is_active: false, + proxy_url: 'http://127.0.0.1:7890', + }; + + vi.spyOn(CloudAccountRepo, 'getAccounts').mockResolvedValue([existingAccount]); + const addAccountSpy = vi.spyOn(CloudAccountRepo, 'addAccount').mockResolvedValue(); + + const account = await CloudAccountRepo.syncFromIDE(); + + expect(account?.status).toBe('active'); + expect(account?.status_reason).toBeUndefined(); + expect(addAccountSpy).toHaveBeenCalledWith( + expect.objectContaining({ + status: 'active', + status_reason: undefined, + }), + ); + }); + it('should retry when sqlite is busy', async () => { busyOnFirstGet = true; const accessToken = 'access-retry'; @@ -268,6 +482,20 @@ describe('CloudAccountRepo.syncFromIDE', () => { }); }); +describe('syncLocalAccount ORPC error mapping', () => { + it('preserves re-login guidance as an actionable unauthorized error', () => { + const error = toSyncLocalAccountORPCError( + new Error( + 'Failed to validate token with Google API. The token may be expired. Please re-login in Antigravity IDE.', + ), + ); + + expect(error.code).toBe('UNAUTHORIZED'); + expect(error.status).toBe(401); + expect(error.message).toContain('Please re-login in Antigravity IDE'); + }); +}); + describe('cloud switch fail-fast path', () => { afterEach(() => { vi.resetModules(); @@ -386,3 +614,172 @@ describe('cloud switch fail-fast path', () => { expect(recordSwitchSuccessMock).not.toHaveBeenCalled(); }); }); + +describe('cloud oauth client key backfill', () => { + afterEach(() => { + vi.resetModules(); + vi.restoreAllMocks(); + }); + + it('backfills missing oauth_client_key with active non-enterprise client', async () => { + const updateTokenMock = vi.fn(async () => undefined); + const setSettingMock = vi.fn(); + const accounts = [ + { + id: 'acc-1', + provider: 'google' as const, + email: 'legacy@test.dev', + token: { + access_token: 'access', + refresh_token: 'refresh', + expires_in: 3600, + expiry_timestamp: Math.floor(Date.now() / 1000) + 3600, + token_type: 'Bearer', + email: 'legacy@test.dev', + }, + created_at: Math.floor(Date.now() / 1000), + last_used: Math.floor(Date.now() / 1000), + }, + ]; + + vi.doMock('../../ipc/database/cloudHandler', () => ({ + CloudAccountRepo: { + getAccounts: vi.fn(async () => accounts), + updateToken: updateTokenMock, + getSetting: vi.fn((key: string, defaultValue: unknown) => { + if (key === 'oauth_client_key_backfill_v1_done') { + return false; + } + if (key === 'active_oauth_client_key') { + return 'custom_a'; + } + return defaultValue; + }), + setSetting: setSettingMock, + }, + })); + + const setActiveOAuthClientKeyMock = vi.fn(); + const getActiveOAuthClientKeyMock = vi.fn(() => 'custom_a'); + vi.doMock('../../services/GoogleAPIService', () => ({ + GoogleAPIService: { + setActiveOAuthClientKey: setActiveOAuthClientKeyMock, + getActiveOAuthClientKey: getActiveOAuthClientKeyMock, + }, + })); + + vi.doMock('../../utils/logger', () => ({ + logger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }, + })); + vi.doMock('../../ipc/tray/handler', () => ({ updateTrayMenu: vi.fn() })); + vi.doMock('../../ipc/device/handler', () => ({ + ensureGlobalOriginalFromCurrentStorage: vi.fn(), + generateDeviceProfile: vi.fn(), + getStorageDirectoryPath: vi.fn(() => ''), + isIdentityProfileApplyEnabled: vi.fn(() => false), + loadGlobalOriginalProfile: vi.fn(), + readCurrentDeviceProfile: vi.fn(), + saveGlobalOriginalProfile: vi.fn(), + })); + vi.doMock('../../utils/paths', () => ({ getAntigravityDbPaths: () => [] })); + vi.doMock('../../ipc/switchGuard', () => ({ + runWithSwitchGuard: async (_owner: string, fn: () => Promise) => fn(), + })); + vi.doMock('../../ipc/switchFlow', () => ({ executeSwitchFlow: vi.fn() })); + vi.doMock('electron', () => ({ shell: { openExternal: vi.fn() } })); + + const { listCloudAccounts } = await import('../../ipc/cloud/handler'); + await listCloudAccounts(); + + expect(setActiveOAuthClientKeyMock).toHaveBeenCalledWith('custom_a'); + expect(updateTokenMock).toHaveBeenCalledWith( + 'acc-1', + expect.objectContaining({ + oauth_client_key: 'custom_a', + }), + ); + expect(setSettingMock).toHaveBeenCalledWith('oauth_client_key_backfill_v1_done', true); + }); + + it('skips enterprise backfill for legacy account without project_id', async () => { + const updateTokenMock = vi.fn(async () => undefined); + const setSettingMock = vi.fn(); + const accounts = [ + { + id: 'acc-legacy', + provider: 'google' as const, + email: 'legacy-enterprise@test.dev', + token: { + access_token: 'access', + refresh_token: 'refresh', + expires_in: 3600, + expiry_timestamp: Math.floor(Date.now() / 1000) + 3600, + token_type: 'Bearer', + email: 'legacy-enterprise@test.dev', + }, + created_at: Math.floor(Date.now() / 1000), + last_used: Math.floor(Date.now() / 1000), + }, + ]; + + vi.doMock('../../ipc/database/cloudHandler', () => ({ + CloudAccountRepo: { + getAccounts: vi.fn(async () => accounts), + updateToken: updateTokenMock, + getSetting: vi.fn((key: string, defaultValue: unknown) => { + if (key === 'oauth_client_key_backfill_v1_done') { + return false; + } + if (key === 'active_oauth_client_key') { + return 'antigravity_enterprise'; + } + return defaultValue; + }), + setSetting: setSettingMock, + }, + })); + + vi.doMock('../../services/GoogleAPIService', () => ({ + GoogleAPIService: { + setActiveOAuthClientKey: vi.fn(), + getActiveOAuthClientKey: vi.fn(() => 'antigravity_enterprise'), + }, + })); + + vi.doMock('../../utils/logger', () => ({ + logger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }, + })); + vi.doMock('../../ipc/tray/handler', () => ({ updateTrayMenu: vi.fn() })); + vi.doMock('../../ipc/device/handler', () => ({ + ensureGlobalOriginalFromCurrentStorage: vi.fn(), + generateDeviceProfile: vi.fn(), + getStorageDirectoryPath: vi.fn(() => ''), + isIdentityProfileApplyEnabled: vi.fn(() => false), + loadGlobalOriginalProfile: vi.fn(), + readCurrentDeviceProfile: vi.fn(), + saveGlobalOriginalProfile: vi.fn(), + })); + vi.doMock('../../utils/paths', () => ({ getAntigravityDbPaths: () => [] })); + vi.doMock('../../ipc/switchGuard', () => ({ + runWithSwitchGuard: async (_owner: string, fn: () => Promise) => fn(), + })); + vi.doMock('../../ipc/switchFlow', () => ({ executeSwitchFlow: vi.fn() })); + vi.doMock('electron', () => ({ shell: { openExternal: vi.fn() } })); + + const { listCloudAccounts } = await import('../../ipc/cloud/handler'); + await listCloudAccounts(); + + expect(updateTokenMock).not.toHaveBeenCalled(); + expect(setSettingMock).toHaveBeenCalledWith('oauth_client_key_backfill_v1_done', true); + }); +}); diff --git a/src/types/cloudAccount.ts b/src/types/cloudAccount.ts index 3c2dc78..9d16a56 100644 --- a/src/types/cloudAccount.ts +++ b/src/types/cloudAccount.ts @@ -14,8 +14,10 @@ export interface CloudTokenData { token_type: string; email?: string; project_id?: string; + oauth_client_key?: string; session_id?: string; upstream_proxy_url?: string; + is_gcp_tos?: boolean; } export interface CloudQuotaModelInfo { @@ -53,6 +55,7 @@ export interface CloudAccount { created_at: number; last_used: number; // Unix timestamp status?: 'active' | 'rate_limited' | 'expired'; + status_reason?: string; is_active?: boolean; proxy_url?: string; } @@ -66,8 +69,10 @@ export const CloudTokenDataSchema = z.object({ token_type: z.string(), email: z.string().optional(), project_id: z.string().optional(), + oauth_client_key: z.string().optional(), session_id: z.string().optional(), upstream_proxy_url: z.string().optional(), + is_gcp_tos: z.boolean().optional(), }); export const CloudQuotaModelInfoSchema = z.object({ @@ -105,6 +110,7 @@ export const CloudAccountSchema = z.object({ created_at: z.number(), last_used: z.number(), status: z.enum(['active', 'rate_limited', 'expired']).optional(), + status_reason: z.string().optional(), is_active: z.boolean().optional(), proxy_url: z.string().optional(), }); @@ -123,6 +129,8 @@ export const CloudAccountExportSchema = z.object({ device_profile: z.any().optional(), device_history: z.any().optional(), proxy_url: z.string().optional().nullable(), + status: z.enum(['active', 'rate_limited', 'expired']).optional(), + status_reason: z.string().optional(), }), ), });