import { z } from 'zod'; import { os, ORPCError } from '@orpc/server'; import { addGoogleAccount, bindCloudIdentityProfile, bindCloudIdentityProfileWithPayload, deleteCloudIdentityProfileRevision, listCloudAccounts, deleteCloudAccount, getCloudIdentityProfiles, openCloudIdentityStorageFolder, previewGenerateCloudIdentityProfile, refreshAccountQuota, restoreCloudIdentityProfileRevision, restoreCloudBaselineProfile, switchCloudAccount, getAutoSwitchEnabled, setAutoSwitchEnabled, forcePollCloudMonitor, startAuthFlow, listOAuthClients, getActiveOAuthClient, setActiveOAuthClient, exportCloudAccounts, importCloudAccounts, } from './handler'; import { CloudAccountRepo } from '../database/cloudHandler'; import { CloudAccountSchema } from '../../types/cloudAccount'; import { DeviceProfileSchema, DeviceProfilesSnapshotSchema } from '../../types/account'; import { logger } from '../../utils/logger'; import { getSwitchMetricsSnapshot } from '../switchMetrics'; import { getSwitchGuardSnapshot } from '../switchGuard'; import { getDeviceHardeningSnapshot } from '../device/handler'; const switchOwnerSchema = z.enum(['local-account-switch', 'cloud-account-switch']); const switchMetricBucketSchema = z.object({ switchSuccess: z.number(), switchFailure: z.number(), rollbackAttempt: z.number(), rollbackSuccess: z.number(), rollbackFailure: z.number(), failureReasons: z.record(z.string(), z.number()), lastFailure: z .object({ reason: z.string(), message: z.string(), occurredAt: z.number(), }) .nullable(), }); const switchMetricsSnapshotSchema = z.object({ local: switchMetricBucketSchema, cloud: switchMetricBucketSchema, }); const switchGuardSnapshotSchema = z.object({ activeOwner: switchOwnerSchema.nullable(), pendingOwners: z.array(switchOwnerSchema), pendingCount: z.number(), }); const switchStatusSnapshotSchema = z.object({ metrics: switchMetricsSnapshotSchema, guard: switchGuardSnapshotSchema, hardening: z.object({ consecutiveApplyFailures: z.number(), safeModeActive: z.boolean(), safeModeUntil: z.number().nullable(), lastFailureReason: z.string().nullable(), lastFailureStage: z.string().nullable(), lastFailureAt: z.number().nullable(), }), }); function extractErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message; } return String(error); } export function toSyncLocalAccountORPCError(error: unknown): ORPCError { const message = extractErrorMessage(error); const normalized = message.toLowerCase(); if ( normalized.includes('unauthenticated') || normalized.includes('unauthorized') || normalized.includes('token may be expired') || normalized.includes('re-login in geminiNexus ide') ) { return new ORPCError('UNAUTHORIZED', { message }); } if ( normalized.includes('no cloud account found in ide') || normalized.includes('no oauth token found in ide state') || normalized.includes('geminiNexus database not found') ) { return new ORPCError('BAD_REQUEST', { message }); } return new ORPCError('INTERNAL_SERVER_ERROR', { message }); } export const cloudRouter = os.router({ addGoogleAccount: os .input(z.object({ authCode: z.string(), oauthClientKey: z.string().optional() })) .output(CloudAccountSchema) .handler(async ({ input }) => { return addGoogleAccount(input.authCode, input.oauthClientKey); }), listCloudAccounts: os.output(z.array(CloudAccountSchema)).handler(async () => { return listCloudAccounts(); }), deleteCloudAccount: os .input(z.object({ accountId: z.string() })) .output(z.void()) .handler(async ({ input }) => { await deleteCloudAccount(input.accountId); }), refreshAccountQuota: os .input(z.object({ accountId: z.string() })) .output(CloudAccountSchema) .handler(async ({ input }) => { return refreshAccountQuota(input.accountId); }), switchCloudAccount: os .input(z.object({ accountId: z.string() })) .output(z.void()) .handler(async ({ input }) => { await switchCloudAccount(input.accountId); }), getAutoSwitchEnabled: os.output(z.boolean()).handler(async () => { return getAutoSwitchEnabled(); }), setAutoSwitchEnabled: os .input(z.object({ enabled: z.boolean() })) .output(z.void()) .handler(async ({ input }) => { await setAutoSwitchEnabled(input.enabled); }), forcePollCloudMonitor: os.output(z.void()).handler(async () => { await forcePollCloudMonitor(); }), startAuthFlow: os .input(z.object({ oauthClientKey: z.string().optional() }).optional()) .output(z.void()) .handler(async ({ input }) => { await startAuthFlow(input?.oauthClientKey); }), listOAuthClients: os .output( z.array( z.object({ key: z.string(), label: z.string(), client_id: z.string(), is_active: z.boolean(), is_builtin: z.boolean(), }), ), ) .handler(async () => { return listOAuthClients(); }), getActiveOAuthClient: os.output(z.object({ client_key: z.string() })).handler(async () => { return { client_key: getActiveOAuthClient(), }; }), setActiveOAuthClient: os .input(z.object({ clientKey: z.string() })) .output(z.void()) .handler(async ({ input }) => { setActiveOAuthClient(input.clientKey); }), setAccountProxy: os .input(z.object({ accountId: z.string(), proxyUrl: z.string().nullable() })) .output(z.void()) .handler(async ({ input }) => { try { await CloudAccountRepo.setAccountProxy(input.accountId, input.proxyUrl); } catch (error: any) { logger.error('[ORPC] setAccountProxy error:', error.message, error.stack); throw error; } }), syncLocalAccount: os.output(CloudAccountSchema.nullable()).handler(async () => { try { const result = await CloudAccountRepo.syncFromIDE(); return result; } catch (error: any) { logger.error('[ORPC] syncLocalAccount error:', error.message, error.stack); throw toSyncLocalAccountORPCError(error); } }), getSwitchStatus: os.output(switchStatusSnapshotSchema).handler(async () => { return { metrics: getSwitchMetricsSnapshot(), guard: getSwitchGuardSnapshot(), hardening: getDeviceHardeningSnapshot(), }; }), getIdentityProfiles: os .input(z.object({ accountId: z.string() })) .output(DeviceProfilesSnapshotSchema) .handler(async ({ input }) => { return getCloudIdentityProfiles(input.accountId); }), previewIdentityProfile: os.output(DeviceProfileSchema).handler(async () => { return previewGenerateCloudIdentityProfile(); }), bindIdentityProfile: os .input(z.object({ accountId: z.string(), mode: z.enum(['capture', 'generate']) })) .output(DeviceProfileSchema) .handler(async ({ input }) => { return bindCloudIdentityProfile(input.accountId, input.mode); }), bindIdentityProfileWithPayload: os .input(z.object({ accountId: z.string(), profile: DeviceProfileSchema })) .output(DeviceProfileSchema) .handler(async ({ input }) => { return bindCloudIdentityProfileWithPayload(input.accountId, input.profile); }), restoreIdentityProfileRevision: os .input(z.object({ accountId: z.string(), versionId: z.string() })) .output(DeviceProfileSchema) .handler(async ({ input }) => { return restoreCloudIdentityProfileRevision(input.accountId, input.versionId); }), restoreBaselineProfile: os .input(z.object({ accountId: z.string() })) .output(DeviceProfileSchema) .handler(async ({ input }) => { return restoreCloudBaselineProfile(input.accountId); }), deleteIdentityProfileRevision: os .input(z.object({ accountId: z.string(), versionId: z.string() })) .output(z.void()) .handler(async ({ input }) => { await deleteCloudIdentityProfileRevision(input.accountId, input.versionId); }), openIdentityStorageFolder: os.output(z.void()).handler(async () => { await openCloudIdentityStorageFolder(); }), exportCloudAccounts: os .input(z.object({ stripTokens: z.boolean().default(false) })) .output(z.string()) .handler(async ({ input }) => { try { return await exportCloudAccounts(input.stripTokens); } catch (error: any) { logger.error('[ORPC] exportCloudAccounts error:', error.message, error.stack); throw error; } }), importCloudAccounts: os .input( z.object({ jsonContent: z.string(), strategy: z.enum(['merge', 'overwrite', 'skip-existing']).default('merge'), }), ) .output( z.object({ imported: z.number(), skipped: z.number(), updated: z.number(), errors: z.array(z.string()), }), ) .handler(async ({ input }) => { try { return await importCloudAccounts(input.jsonContent, input.strategy); } catch (error: any) { logger.error('[ORPC] importCloudAccounts error:', error.message, error.stack); throw error; } }), });