feat: complete account-bound profile switching and hardening (#78)

This commit is contained in:
vin authored and GitHub committed 2026-02-08 01:10:07 +08:00
1 parent ee641799c2
commit a93c6d0cea
31 files changed
+3612 -197

No files matched your search

+265 -23
View File
@@ -3,15 +3,66 @@ import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { getAccountsFilePath, getBackupsDir } from '../../utils/paths';
import { logger } from '../../utils/logger';
import { Account, AccountBackupData } from '../../types/account';
import {
Account,
AccountBackupData,
DeviceProfile,
DeviceProfilesSnapshot,
DeviceProfileVersion,
} from '../../types/account';
import {
backupAccount as dbBackup,
restoreAccount as dbRestore,
getCurrentAccountInfo,
} from '../database/handler';
import { closeAntigravity, startAntigravity } from '../process/handler';
import {
applyDeviceProfile,
ensureGlobalOriginalFromCurrentStorage,
generateDeviceProfile,
isIdentityProfileApplyEnabled,
loadGlobalOriginalProfile,
readCurrentDeviceProfile,
saveGlobalOriginalProfile,
getStorageDirectoryPath,
} from '../device/handler';
import { runWithSwitchGuard } from '../switchGuard';
import { executeSwitchFlow } from '../switchFlow';
import { shell } from 'electron';
type AccountIndex = Record<string, Account>;
const SWITCH_EXIT_TIMEOUT_MS = 10000;
function getDeviceHistory(account: Account): DeviceProfileVersion[] {
if (!account.deviceHistory) {
account.deviceHistory = [];
}
return account.deviceHistory;
}
function bindDeviceProfileToAccount(
account: Account,
profile: DeviceProfile,
label: string,
addHistory: boolean,
): void {
account.deviceProfile = profile;
if (!addHistory) {
return;
}
const history = getDeviceHistory(account);
for (const version of history) {
version.isCurrent = false;
}
history.push({
id: uuidv4(),
createdAt: Math.floor(Date.now() / 1000),
label,
profile,
isCurrent: true,
});
}
/**
* Loads the accounts index from the file system.
@@ -141,6 +192,7 @@ export async function addAccountSnapshot(): Promise<Account> {
name: accountName,
email: info.email,
backup_file: backupPath,
deviceHistory: [],
created_at: now,
last_used: now,
};
@@ -171,7 +223,184 @@ export async function addAccountSnapshot(): Promise<Account> {
* @throws {Error} If the account cannot be found or the backup file cannot be found.
*/
export async function switchAccount(accountId: string): Promise<void> {
logger.info(`Switching to account: ${accountId}`);
await runWithSwitchGuard('local-account-switch', async () => {
logger.info(`Switching to account: ${accountId}`);
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
// NOTE Get backup file path from account data
const backupPath = account.backup_file || path.join(getBackupsDir(), `${accountId}.json`);
if (!fs.existsSync(backupPath)) {
throw new Error(`Backup file not found: ${backupPath}`);
}
ensureGlobalOriginalFromCurrentStorage();
if (!account.deviceProfile) {
const generated = generateDeviceProfile();
saveGlobalOriginalProfile(generated);
bindDeviceProfileToAccount(account, generated, 'auto_generated', true);
}
await executeSwitchFlow({
scope: 'local',
targetProfile: account.deviceProfile || null,
applyFingerprint: isIdentityProfileApplyEnabled(),
processExitTimeoutMs: SWITCH_EXIT_TIMEOUT_MS,
performSwitch: async () => {
// NOTE Load backup file
const backupContent = fs.readFileSync(backupPath, 'utf-8');
const backupData: AccountBackupData = JSON.parse(backupContent);
// NOTE Restore data to DB
dbRestore(backupData);
// NOTE Update last used
account.last_used = new Date().toISOString();
saveAccountsIndex(accounts);
},
});
});
}
export async function previewGenerateIdentityProfile(): Promise<DeviceProfile> {
return generateDeviceProfile();
}
export async function getIdentityProfiles(accountId: string): Promise<DeviceProfilesSnapshot> {
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
let currentStorage: DeviceProfile | undefined;
try {
currentStorage = readCurrentDeviceProfile();
} catch (error) {
logger.warn('Failed to read current storage device profile', error);
}
return {
currentStorage,
boundProfile: account.deviceProfile,
history: account.deviceHistory || [],
baseline: loadGlobalOriginalProfile() || undefined,
};
}
export async function bindIdentityProfile(
accountId: string,
mode: 'capture' | 'generate',
): Promise<DeviceProfile> {
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
let profile: DeviceProfile;
if (mode === 'capture') {
profile = readCurrentDeviceProfile();
} else {
profile = generateDeviceProfile();
}
ensureGlobalOriginalFromCurrentStorage();
saveGlobalOriginalProfile(profile);
applyDeviceProfile(profile);
bindDeviceProfileToAccount(account, profile, mode, true);
saveAccountsIndex(accounts);
return profile;
}
export async function bindIdentityProfileWithPayload(
accountId: string,
profile: DeviceProfile,
): Promise<DeviceProfile> {
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
ensureGlobalOriginalFromCurrentStorage();
saveGlobalOriginalProfile(profile);
applyDeviceProfile(profile);
bindDeviceProfileToAccount(account, profile, 'generated', true);
saveAccountsIndex(accounts);
return profile;
}
export async function applyBoundIdentityProfile(accountId: string): Promise<DeviceProfile> {
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
if (!account.deviceProfile) {
throw new Error('Account has no bound device profile');
}
applyDeviceProfile(account.deviceProfile);
account.last_used = new Date().toISOString();
saveAccountsIndex(accounts);
return account.deviceProfile;
}
export async function restoreIdentityProfileRevision(
accountId: string,
versionId: string,
): Promise<DeviceProfile> {
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
let targetProfile: DeviceProfile | null = null;
if (versionId === 'baseline') {
targetProfile = loadGlobalOriginalProfile();
if (!targetProfile) {
throw new Error('Global original profile not found');
}
for (const version of getDeviceHistory(account)) {
version.isCurrent = false;
}
} else if (versionId === 'current') {
targetProfile = account.deviceProfile || null;
if (!targetProfile) {
throw new Error('No currently bound profile');
}
} else {
const history = getDeviceHistory(account);
const targetVersion = history.find((version) => version.id === versionId);
if (!targetVersion) {
throw new Error('Device profile version not found');
}
targetProfile = targetVersion.profile;
for (const version of history) {
version.isCurrent = version.id === versionId;
}
}
applyDeviceProfile(targetProfile);
account.deviceProfile = targetProfile;
saveAccountsIndex(accounts);
return targetProfile;
}
export async function deleteIdentityProfileRevision(
accountId: string,
versionId: string,
): Promise<void> {
if (versionId === 'baseline') {
throw new Error('Original profile cannot be deleted');
}
const accounts = loadAccountsIndex();
const account = accounts[accountId];
@@ -179,34 +408,47 @@ export async function switchAccount(accountId: string): Promise<void> {
throw new Error(`Account not found: ${accountId}`);
}
// NOTE Get backup file path from account data
const backupPath = account.backup_file || path.join(getBackupsDir(), `${accountId}.json`);
if (!fs.existsSync(backupPath)) {
throw new Error(`Backup file not found: ${backupPath}`);
const history = getDeviceHistory(account);
if (history.some((version) => version.id === versionId && version.isCurrent)) {
throw new Error('Currently bound profile cannot be deleted');
}
// NOTE Close Antigravity (with error handling)
try {
await closeAntigravity();
} catch (error) {
// NOTE Continue even if close fails
logger.warn('Unable to close Antigravity, attempting forced restore...', error);
const before = history.length;
account.deviceHistory = history.filter((version) => version.id !== versionId);
if (account.deviceHistory.length === before) {
throw new Error('Historical device profile not found');
}
// NOTE Load backup file
const backupContent = fs.readFileSync(backupPath, 'utf-8');
const backupData: AccountBackupData = JSON.parse(backupContent);
saveAccountsIndex(accounts);
}
// NOTE Restore data to DB
dbRestore(backupData);
export async function restoreBaselineProfile(accountId: string): Promise<DeviceProfile> {
const accounts = loadAccountsIndex();
const account = accounts[accountId];
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
// NOTE Update last used
account.last_used = new Date().toISOString();
const baseline = loadGlobalOriginalProfile();
if (!baseline) {
throw new Error('Global original profile not found');
}
account.deviceProfile = baseline;
for (const version of getDeviceHistory(account)) {
version.isCurrent = false;
}
saveAccountsIndex(accounts);
// NOTE Start Antigravity
await startAntigravity();
return baseline;
}
export async function openIdentityStorageFolder(): Promise<void> {
const directory = getStorageDirectoryPath();
const result = await shell.openPath(directory);
if (result) {
throw new Error(`Failed to open device folder: ${result}`);
}
}
/**
+77 -2
View File
@@ -1,7 +1,25 @@
import { z } from 'zod';
import { os } from '@orpc/server';
import { listAccountsData, addAccountSnapshot, switchAccount, deleteAccount } from './handler';
import { AccountSchema } from '../../types/account';
import {
applyBoundIdentityProfile,
addAccountSnapshot,
bindIdentityProfile,
bindIdentityProfileWithPayload,
deleteAccount,
deleteIdentityProfileRevision,
getIdentityProfiles,
listAccountsData,
previewGenerateIdentityProfile,
restoreBaselineProfile,
restoreIdentityProfileRevision,
switchAccount,
openIdentityStorageFolder,
} from './handler';
import {
AccountSchema,
DeviceProfileSchema,
DeviceProfilesSnapshotSchema,
} from '../../types/account';
export const accountRouter = os.router({
listAccounts: os.output(z.array(AccountSchema)).handler(async () => {
@@ -25,4 +43,61 @@ export const accountRouter = os.router({
.handler(async ({ input }) => {
await deleteAccount(input.accountId);
}),
previewGenerateIdentityProfile: os.output(DeviceProfileSchema).handler(async () => {
return previewGenerateIdentityProfile();
}),
getIdentityProfiles: os
.input(z.object({ accountId: z.string() }))
.output(DeviceProfilesSnapshotSchema)
.handler(async ({ input }) => {
return getIdentityProfiles(input.accountId);
}),
bindIdentityProfile: os
.input(z.object({ accountId: z.string(), mode: z.enum(['capture', 'generate']) }))
.output(DeviceProfileSchema)
.handler(async ({ input }) => {
return bindIdentityProfile(input.accountId, input.mode);
}),
bindIdentityProfileWithPayload: os
.input(z.object({ accountId: z.string(), profile: DeviceProfileSchema }))
.output(DeviceProfileSchema)
.handler(async ({ input }) => {
return bindIdentityProfileWithPayload(input.accountId, input.profile);
}),
applyBoundIdentityProfile: os
.input(z.object({ accountId: z.string() }))
.output(DeviceProfileSchema)
.handler(async ({ input }) => {
return applyBoundIdentityProfile(input.accountId);
}),
restoreIdentityProfileRevision: os
.input(z.object({ accountId: z.string(), versionId: z.string() }))
.output(DeviceProfileSchema)
.handler(async ({ input }) => {
return restoreIdentityProfileRevision(input.accountId, input.versionId);
}),
deleteIdentityProfileRevision: os
.input(z.object({ accountId: z.string(), versionId: z.string() }))
.output(z.void())
.handler(async ({ input }) => {
await deleteIdentityProfileRevision(input.accountId, input.versionId);
}),
restoreBaselineProfile: os
.input(z.object({ accountId: z.string() }))
.output(DeviceProfileSchema)
.handler(async ({ input }) => {
return restoreBaselineProfile(input.accountId);
}),
openIdentityStorageFolder: os.output(z.void()).handler(async () => {
await openIdentityStorageFolder();
}),
});
+173 -60
View File
@@ -6,9 +6,20 @@ import { logger } from '../../utils/logger';
import { shell } from 'electron';
import fs from 'fs';
import { closeAntigravity, startAntigravity, _waitForProcessExit } from '../../ipc/process/handler';
import { updateTrayMenu } from '../../ipc/tray/handler';
import {
ensureGlobalOriginalFromCurrentStorage,
generateDeviceProfile,
getStorageDirectoryPath,
isIdentityProfileApplyEnabled,
loadGlobalOriginalProfile,
readCurrentDeviceProfile,
saveGlobalOriginalProfile,
} from '../../ipc/device/handler';
import { getAntigravityDbPaths } from '../../utils/paths';
import { runWithSwitchGuard } from '../../ipc/switchGuard';
import { executeSwitchFlow } from '../../ipc/switchFlow';
import type { DeviceProfile, DeviceProfilesSnapshot } from '../../types/account';
// Fallback constants if service constants are not available or for direct usage
const CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
@@ -169,74 +180,176 @@ export async function refreshAccountQuota(accountId: string): Promise<CloudAccou
}
export async function switchCloudAccount(accountId: string): Promise<void> {
try {
const account = await CloudAccountRepo.getAccount(accountId);
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
logger.info(`Switching to cloud account: ${account.email} (${account.id})`);
// 1. Ensure token is fresh before injecting
const now = Math.floor(Date.now() / 1000);
if (account.token.expiry_timestamp < now + 300) {
logger.info(`Token for ${account.email} near expiry, refreshing before switch...`);
try {
const newTokenData = await GoogleAPIService.refreshAccessToken(account.token.refresh_token);
account.token.access_token = newTokenData.access_token;
account.token.expires_in = newTokenData.expires_in;
account.token.expiry_timestamp = now + newTokenData.expires_in;
await CloudAccountRepo.updateToken(account.id, account.token);
} catch (e) {
logger.warn('Failed to refresh token before switch, trying with existing token', e);
}
}
// 2. Stop Antigravity Process
await closeAntigravity();
await runWithSwitchGuard('cloud-account-switch', async () => {
try {
await _waitForProcessExit(10000); // Wait up to 10s for it to truly vanish
} catch (e) {
logger.warn('Process did not exit cleanly within timeout, but proceeding...', e);
}
// 3. Backup Database (New Logic)
const dbPaths = getAntigravityDbPaths();
// Find the valid DB path
let dbPath: string | null = null;
for (const p of dbPaths) {
if (fs.existsSync(p)) {
dbPath = p;
break;
const account = await CloudAccountRepo.getAccount(accountId);
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
}
if (dbPath) {
try {
const backupPath = `${dbPath}.backup`;
fs.copyFileSync(dbPath, backupPath);
logger.info(`Backed up database to ${backupPath}`);
} catch (e) {
logger.error('Failed to backup database', e);
logger.info(`Switching to cloud account: ${account.email} (${account.id})`);
ensureGlobalOriginalFromCurrentStorage();
if (!account.device_profile) {
const generated = generateDeviceProfile();
CloudAccountRepo.setDeviceBinding(account.id, generated, 'auto_generated');
saveGlobalOriginalProfile(generated);
account.device_profile = generated;
}
// 1. Ensure token is fresh before injecting
const now = Math.floor(Date.now() / 1000);
if (account.token.expiry_timestamp < now + 300) {
logger.info(`Token for ${account.email} near expiry, refreshing before switch...`);
try {
const newTokenData = await GoogleAPIService.refreshAccessToken(account.token.refresh_token);
account.token.access_token = newTokenData.access_token;
account.token.expires_in = newTokenData.expires_in;
account.token.expiry_timestamp = now + newTokenData.expires_in;
await CloudAccountRepo.updateToken(account.id, account.token);
} catch (e) {
logger.warn('Failed to refresh token before switch, trying with existing token', e);
}
}
await executeSwitchFlow({
scope: 'cloud',
targetProfile: account.device_profile || null,
applyFingerprint: isIdentityProfileApplyEnabled(),
processExitTimeoutMs: 10000,
performSwitch: async () => {
// 3. Backup Database (New Logic)
const dbPaths = getAntigravityDbPaths();
// Find the valid DB path
let dbPath: string | null = null;
for (const p of dbPaths) {
if (fs.existsSync(p)) {
dbPath = p;
break;
}
}
if (dbPath) {
try {
const backupPath = `${dbPath}.backup`;
fs.copyFileSync(dbPath, backupPath);
logger.info(`Backed up database to ${backupPath}`);
} catch (e) {
logger.error('Failed to backup database', e);
}
}
// 4. Inject Token
// injectedCloudToken uses direct DB/FS access which is sync better-sqlite3.
CloudAccountRepo.injectCloudToken(account);
// 5. Update usage and active status
CloudAccountRepo.updateLastUsed(account.id);
CloudAccountRepo.setActive(account.id);
logger.info(`Successfully switched to cloud account: ${account.email}`);
notifyTrayUpdate(account);
},
});
} catch (err: any) {
logger.error('Failed to switch cloud account', err);
throw new Error(`Switch failed: ${err.message || 'Unknown error'}`);
}
});
}
// 3. Inject Token
// injectedCloudToken uses direct DB/FS access which is sync better-sqlite3.
CloudAccountRepo.injectCloudToken(account);
export async function getCloudIdentityProfiles(accountId: string): Promise<DeviceProfilesSnapshot> {
const account = await CloudAccountRepo.getAccount(accountId);
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
// 4. Update usage and active status
CloudAccountRepo.updateLastUsed(account.id);
CloudAccountRepo.setActive(account.id);
let currentStorage: DeviceProfile | undefined;
try {
currentStorage = readCurrentDeviceProfile();
} catch (error) {
logger.warn('Failed to read current storage device profile', error);
}
// 5. Restart Process
await startAntigravity();
return {
currentStorage,
boundProfile: account.device_profile,
history: account.device_history || [],
baseline: loadGlobalOriginalProfile() || undefined,
};
}
logger.info(`Successfully switched to cloud account: ${account.email}`);
notifyTrayUpdate(account);
} catch (err: any) {
logger.error('Failed to switch cloud account', err);
throw new Error(`Switch failed: ${err.message || 'Unknown error'}`);
export async function previewGenerateCloudIdentityProfile(): Promise<DeviceProfile> {
return generateDeviceProfile();
}
export async function bindCloudIdentityProfile(
accountId: string,
mode: 'capture' | 'generate',
): Promise<DeviceProfile> {
const account = await CloudAccountRepo.getAccount(accountId);
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
let profile: DeviceProfile;
if (mode === 'capture') {
profile = readCurrentDeviceProfile();
} else {
profile = generateDeviceProfile();
}
ensureGlobalOriginalFromCurrentStorage();
saveGlobalOriginalProfile(profile);
CloudAccountRepo.setDeviceBinding(account.id, profile, mode);
return profile;
}
export async function bindCloudIdentityProfileWithPayload(
accountId: string,
profile: DeviceProfile,
): Promise<DeviceProfile> {
const account = await CloudAccountRepo.getAccount(accountId);
if (!account) {
throw new Error(`Account not found: ${accountId}`);
}
ensureGlobalOriginalFromCurrentStorage();
saveGlobalOriginalProfile(profile);
CloudAccountRepo.setDeviceBinding(account.id, profile, 'generated');
return profile;
}
export async function restoreCloudIdentityProfileRevision(
accountId: string,
versionId: string,
): Promise<DeviceProfile> {
const baseline = loadGlobalOriginalProfile();
return CloudAccountRepo.restoreDeviceVersion(accountId, versionId, baseline);
}
export async function restoreCloudBaselineProfile(accountId: string): Promise<DeviceProfile> {
const baseline = loadGlobalOriginalProfile();
if (!baseline) {
throw new Error('Global original profile not found');
}
return CloudAccountRepo.restoreDeviceVersion(accountId, 'baseline', baseline);
}
export async function deleteCloudIdentityProfileRevision(
accountId: string,
versionId: string,
): Promise<void> {
CloudAccountRepo.deleteDeviceVersion(accountId, versionId);
}
export async function openCloudIdentityStorageFolder(): Promise<void> {
const directory = getStorageDirectoryPath();
const result = await shell.openPath(directory);
if (result) {
throw new Error(`Failed to open identity storage: ${result}`);
}
}
+111
View File
@@ -2,9 +2,17 @@ import { z } from 'zod';
import { os } from '@orpc/server';
import {
addGoogleAccount,
bindCloudIdentityProfile,
bindCloudIdentityProfileWithPayload,
deleteCloudIdentityProfileRevision,
listCloudAccounts,
deleteCloudAccount,
getCloudIdentityProfiles,
openCloudIdentityStorageFolder,
previewGenerateCloudIdentityProfile,
refreshAccountQuota,
restoreCloudIdentityProfileRevision,
restoreCloudBaselineProfile,
switchCloudAccount,
getAutoSwitchEnabled,
setAutoSwitchEnabled,
@@ -12,8 +20,53 @@ import {
startAuthFlow,
} from './handler';
import { CloudAccountSchema } from '../../types/cloudAccount';
import {
DeviceProfileSchema,
DeviceProfilesSnapshotSchema,
} from '../../types/account';
import { CloudAccountRepo } from '../database/cloudHandler';
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(),
}),
});
export const cloudRouter = os.router({
addGoogleAccount: os
@@ -77,4 +130,62 @@ export const cloudRouter = os.router({
throw 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();
}),
});
+405
View File
@@ -7,6 +7,7 @@ import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
import { getCloudAccountsDbPath, getAntigravityDbPaths } from '../../utils/paths';
import { logger } from '../../utils/logger';
import { CloudAccount } from '../../types/cloudAccount';
import { type DeviceProfile, type DeviceProfileVersion } from '../../types/account';
import { ItemTableValueRowSchema, TableInfoRowSchema } from '../../types/db';
import { decryptWithMigration, encrypt, type KeySource } from '../../utils/security';
import { ProtobufUtils } from '../../utils/protobuf';
@@ -21,6 +22,7 @@ const SQLITE_BUSY_CODES = new Set(['SQLITE_BUSY', 'SQLITE_LOCKED']);
const SQLITE_BUSY_TIMEOUT_MS = 3000;
const SQLITE_RETRY_DELAY_MS = 150;
const SQLITE_MAX_RETRIES = 3;
const DEVICE_PAYLOAD_SCHEMA_VERSION = 1;
type DrizzleExecutor = Pick<
BetterSQLite3Database<typeof drizzleSchema>,
@@ -73,6 +75,8 @@ function ensureDatabaseInitialized(dbPath: string): void {
avatar_url TEXT,
token_json TEXT NOT NULL,
quota_json TEXT,
device_profile_json TEXT,
device_history_json TEXT,
created_at INTEGER NOT NULL,
last_used INTEGER NOT NULL,
status TEXT DEFAULT 'active',
@@ -84,9 +88,17 @@ function ensureDatabaseInitialized(dbPath: string): void {
const tableInfoRaw = db.pragma('table_info(accounts)') as any[];
const tableInfo = parseRows(TableInfoRowSchema, tableInfoRaw, 'cloud.accounts.tableInfo');
const hasIsActive = tableInfo.some((col) => col.name === 'is_active');
const hasDeviceProfileJson = tableInfo.some((col) => col.name === 'device_profile_json');
const hasDeviceHistoryJson = tableInfo.some((col) => col.name === 'device_history_json');
if (!hasIsActive) {
db.exec('ALTER TABLE accounts ADD COLUMN is_active INTEGER DEFAULT 0');
}
if (!hasDeviceProfileJson) {
db.exec('ALTER TABLE accounts ADD COLUMN device_profile_json TEXT');
}
if (!hasDeviceHistoryJson) {
db.exec('ALTER TABLE accounts ADD COLUMN device_history_json TEXT');
}
// Create index on email for faster lookups
// Create index on email for faster lookups
@@ -142,6 +154,196 @@ interface MigrationStats {
failedFields: number;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function readStringCandidate(
source: Record<string, unknown>,
...keys: string[]
): string | undefined {
for (const key of keys) {
const candidate = source[key];
if (typeof candidate === 'string' && candidate.length > 0) {
return candidate;
}
}
return undefined;
}
function normalizeDeviceProfile(value: unknown): DeviceProfile | undefined {
if (!isRecord(value)) {
return undefined;
}
const machineId = readStringCandidate(value, 'machineId', 'machine_id');
const macMachineId = readStringCandidate(value, 'macMachineId', 'mac_machine_id');
const devDeviceId = readStringCandidate(value, 'devDeviceId', 'dev_device_id');
const sqmId = readStringCandidate(value, 'sqmId', 'sqm_id');
if (!machineId || !macMachineId || !devDeviceId || !sqmId) {
return undefined;
}
return {
machineId,
macMachineId,
devDeviceId,
sqmId,
};
}
function areDeviceProfilesEqual(left: DeviceProfile, right: DeviceProfile): boolean {
return (
left.machineId === right.machineId &&
left.macMachineId === right.macMachineId &&
left.devDeviceId === right.devDeviceId &&
left.sqmId === right.sqmId
);
}
function readVersionedProfilePayload(value: unknown): unknown {
if (!isRecord(value)) {
return value;
}
if (!('schemaVersion' in value)) {
return value;
}
const schemaVersion = value.schemaVersion;
if (typeof schemaVersion !== 'number' || !Number.isFinite(schemaVersion)) {
throw new Error('invalid_device_profile_schema_version');
}
if (schemaVersion !== DEVICE_PAYLOAD_SCHEMA_VERSION) {
throw new Error(`unsupported_device_profile_schema_version:${schemaVersion}`);
}
if (!('profile' in value)) {
throw new Error('invalid_device_profile_payload');
}
return value.profile;
}
function readVersionedHistoryPayload(value: unknown): unknown {
if (!isRecord(value)) {
return value;
}
if (!('schemaVersion' in value)) {
return value;
}
const schemaVersion = value.schemaVersion;
if (typeof schemaVersion !== 'number' || !Number.isFinite(schemaVersion)) {
throw new Error('invalid_device_history_schema_version');
}
if (schemaVersion !== DEVICE_PAYLOAD_SCHEMA_VERSION) {
throw new Error(`unsupported_device_history_schema_version:${schemaVersion}`);
}
if (!('history' in value)) {
throw new Error('invalid_device_history_payload');
}
return value.history;
}
function serializeDeviceProfile(profile: DeviceProfile | undefined): string | null {
if (!profile) {
return null;
}
return JSON.stringify({
schemaVersion: DEVICE_PAYLOAD_SCHEMA_VERSION,
profile,
});
}
function serializeDeviceHistory(history: DeviceProfileVersion[] | undefined): string | null {
if (!history || history.length === 0) {
return null;
}
return JSON.stringify({
schemaVersion: DEVICE_PAYLOAD_SCHEMA_VERSION,
history,
});
}
function normalizeDeviceHistory(value: unknown): DeviceProfileVersion[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
const normalized: DeviceProfileVersion[] = [];
for (const item of value) {
if (!isRecord(item)) {
continue;
}
const profile = normalizeDeviceProfile(item.profile);
if (!profile) {
continue;
}
const id = typeof item.id === 'string' && item.id.length > 0 ? item.id : uuidv4();
const createdAtCandidate = item.createdAt;
const createdAt =
typeof createdAtCandidate === 'number' && Number.isFinite(createdAtCandidate)
? Math.floor(createdAtCandidate)
: Math.floor(Date.now() / 1000);
const label = typeof item.label === 'string' && item.label.length > 0 ? item.label : 'legacy';
const isCurrent = item.isCurrent === true;
normalized.push({
id,
createdAt,
label,
profile,
isCurrent,
});
}
return normalized;
}
function parseDeviceProfileColumn(value: string | null | undefined): DeviceProfile | undefined {
if (!value) {
return undefined;
}
let parsed: unknown;
try {
parsed = JSON.parse(value);
} catch {
throw new Error('invalid_device_profile_json');
}
const normalized = normalizeDeviceProfile(readVersionedProfilePayload(parsed));
if (!normalized) {
throw new Error('invalid_device_profile_json');
}
return normalized;
}
function parseDeviceHistoryColumn(
value: string | null | undefined,
): DeviceProfileVersion[] | undefined {
if (!value) {
return undefined;
}
let parsed: unknown;
try {
parsed = JSON.parse(value);
} catch {
throw new Error('invalid_device_history_json');
}
const payload = readVersionedHistoryPayload(parsed);
if (!Array.isArray(payload)) {
throw new Error('invalid_device_history_json');
}
const normalized = normalizeDeviceHistory(payload);
if (!normalized) {
throw new Error('invalid_device_history_json');
}
if (normalized.length !== payload.length) {
throw new Error('invalid_device_history_entry');
}
return normalized;
}
function createMigrationStats(): MigrationStats {
return {
totalFields: 0,
@@ -260,6 +462,8 @@ export class CloudAccountRepo {
avatarUrl: account.avatar_url ?? null,
tokenJson: tokenEncrypted,
quotaJson: quotaEncrypted,
deviceProfileJson: serializeDeviceProfile(account.device_profile),
deviceHistoryJson: serializeDeviceHistory(account.device_history),
createdAt: account.created_at,
lastUsed: account.last_used,
status: account.status || 'active',
@@ -371,6 +575,8 @@ export class CloudAccountRepo {
avatar_url: normalizedRow.avatarUrl ?? undefined,
token: JSON.parse(tokenResult.value),
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,
@@ -437,6 +643,8 @@ export class CloudAccountRepo {
avatar_url: normalizedRow.avatarUrl ?? undefined,
token: JSON.parse(tokenResult.value),
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,
@@ -492,6 +700,203 @@ export class CloudAccountRepo {
}
}
static setDeviceBinding(id: string, profile: DeviceProfile, label: string): void {
const { raw, orm } = getCloudDb();
try {
const rows = orm
.select({
deviceProfileJson: accounts.deviceProfileJson,
deviceHistoryJson: accounts.deviceHistoryJson,
})
.from(accounts)
.where(eq(accounts.id, id))
.all();
const row = rows[0];
if (!row) {
throw new Error(`Account not found: ${id}`);
}
const boundProfile = parseDeviceProfileColumn(row.deviceProfileJson);
if (boundProfile && areDeviceProfilesEqual(boundProfile, profile)) {
logger.info(
`Skipping duplicate device profile binding for account ${id} (bound profile match)`,
);
return;
}
const historyRaw = parseDeviceHistoryColumn(row.deviceHistoryJson) || [];
const currentVersion = historyRaw.find((version) => version.isCurrent);
const latestVersion = historyRaw.length > 0 ? historyRaw[historyRaw.length - 1] : undefined;
if (currentVersion && areDeviceProfilesEqual(currentVersion.profile, profile)) {
logger.info(
`Skipping duplicate device profile binding for account ${id} (history current match)`,
);
return;
}
if (
!currentVersion &&
latestVersion &&
areDeviceProfilesEqual(latestVersion.profile, profile)
) {
logger.info(
`Skipping duplicate device profile binding for account ${id} (history latest match)`,
);
return;
}
const history = historyRaw.map((version) => ({
...version,
isCurrent: false,
}));
history.push({
id: uuidv4(),
createdAt: Math.floor(Date.now() / 1000),
label,
profile,
isCurrent: true,
});
orm
.update(accounts)
.set({
deviceProfileJson: serializeDeviceProfile(profile),
deviceHistoryJson: serializeDeviceHistory(history),
})
.where(eq(accounts.id, id))
.run();
} finally {
raw.close();
}
}
static getDeviceBinding(id: string): {
profile?: DeviceProfile;
history: DeviceProfileVersion[];
} {
const { raw, orm } = getCloudDb();
try {
const rows = orm
.select({
deviceProfileJson: accounts.deviceProfileJson,
deviceHistoryJson: accounts.deviceHistoryJson,
})
.from(accounts)
.where(eq(accounts.id, id))
.all();
const row = rows[0];
if (!row) {
throw new Error(`Account not found: ${id}`);
}
return {
profile: parseDeviceProfileColumn(row.deviceProfileJson),
history: parseDeviceHistoryColumn(row.deviceHistoryJson) || [],
};
} finally {
raw.close();
}
}
static restoreDeviceVersion(
id: string,
versionId: string,
baseline: DeviceProfile | null,
): DeviceProfile {
const { raw, orm } = getCloudDb();
try {
const rows = orm
.select({
deviceProfileJson: accounts.deviceProfileJson,
deviceHistoryJson: accounts.deviceHistoryJson,
})
.from(accounts)
.where(eq(accounts.id, id))
.all();
const row = rows[0];
if (!row) {
throw new Error(`Account not found: ${id}`);
}
const currentProfile = parseDeviceProfileColumn(row.deviceProfileJson);
const history = parseDeviceHistoryColumn(row.deviceHistoryJson) || [];
let targetProfile: DeviceProfile;
if (versionId === 'baseline') {
if (!baseline) {
throw new Error('Global original profile not found');
}
targetProfile = baseline;
} else if (versionId === 'current') {
if (!currentProfile) {
throw new Error('No currently bound profile');
}
targetProfile = currentProfile;
} else {
const targetVersion = history.find((version) => version.id === versionId);
if (!targetVersion) {
throw new Error('Device profile version not found');
}
targetProfile = targetVersion.profile;
}
const nextHistory = history.map((version) => ({
...version,
isCurrent: version.id === versionId,
}));
orm
.update(accounts)
.set({
deviceProfileJson: serializeDeviceProfile(targetProfile),
deviceHistoryJson: serializeDeviceHistory(nextHistory),
})
.where(eq(accounts.id, id))
.run();
return targetProfile;
} finally {
raw.close();
}
}
static deleteDeviceVersion(id: string, versionId: string): void {
if (versionId === 'baseline') {
throw new Error('Original profile cannot be deleted');
}
const { raw, orm } = getCloudDb();
try {
const rows = orm
.select({ deviceHistoryJson: accounts.deviceHistoryJson })
.from(accounts)
.where(eq(accounts.id, id))
.all();
const row = rows[0];
if (!row) {
throw new Error(`Account not found: ${id}`);
}
const history = parseDeviceHistoryColumn(row.deviceHistoryJson) || [];
if (history.some((version) => version.id === versionId && version.isCurrent)) {
throw new Error('Currently bound profile cannot be deleted');
}
const nextHistory = history.filter((version) => version.id !== versionId);
if (nextHistory.length === history.length) {
throw new Error('Historical device profile not found');
}
orm
.update(accounts)
.set({ deviceHistoryJson: serializeDeviceHistory(nextHistory) })
.where(eq(accounts.id, id))
.run();
} finally {
raw.close();
}
}
static setActive(id: string): void {
const { raw, orm } = getCloudDb();
+2
View File
@@ -8,6 +8,8 @@ export const accounts = sqliteTable('accounts', {
avatarUrl: text('avatar_url'),
tokenJson: text('token_json').notNull(),
quotaJson: text('quota_json'),
deviceProfileJson: text('device_profile_json'),
deviceHistoryJson: text('device_history_json'),
createdAt: integer('created_at').notNull(),
lastUsed: integer('last_used').notNull(),
status: text('status'),
+715
View File
@@ -0,0 +1,715 @@
import Database from 'better-sqlite3';
import fs from 'fs';
import path from 'path';
import { createHash, randomBytes } from 'crypto';
import { type DeviceProfile } from '../../types/account';
import { logger } from '../../utils/logger';
import { getAgentDir, getAntigravityDbPaths, getAntigravityStoragePaths } from '../../utils/paths';
const GLOBAL_BASELINE_FILE = 'device_original.json';
const SQLITE_RETRY_COUNT = 3;
const LAST_KNOWN_GOOD_DIR = 'device_last_known_good';
const LAST_KNOWN_GOOD_STORAGE_FILE = 'storage.json';
const LAST_KNOWN_GOOD_STATE_DB_FILE = 'state.vscdb';
const LAST_KNOWN_GOOD_MARKER_FILE = 'marker.json';
const DEVICE_HARDENING_SAFE_MODE_THRESHOLD = 3;
const DEVICE_HARDENING_SAFE_MODE_DURATION_MS = 5 * 60 * 1000;
export type DeviceApplyFailureReason =
| 'backup_failed'
| 'storage_write_failed'
| 'state_sync_failed'
| 'verify_storage_failed'
| 'verify_state_failed'
| 'snapshot_update_failed'
| 'rollback_failed'
| 'unknown';
export interface DeviceHardeningSnapshot {
consecutiveApplyFailures: number;
safeModeActive: boolean;
safeModeUntil: number | null;
lastFailureReason: DeviceApplyFailureReason | null;
lastFailureStage: string | null;
lastFailureAt: number | null;
}
const UUID_V4_REGEX =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const SQM_ID_REGEX =
/^\{[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}\}$/;
interface DeviceProfileTemplate {
machineIdPrefix: string;
machineIdHexLength: number;
}
const DEFAULT_PROFILE_TEMPLATE: DeviceProfileTemplate = {
machineIdPrefix: 'auth0|user_',
machineIdHexLength: 32,
};
const DEVICE_PROFILE_TEMPLATES: Record<string, DeviceProfileTemplate[]> = {
win32: [DEFAULT_PROFILE_TEMPLATE],
linux: [DEFAULT_PROFILE_TEMPLATE],
darwin: [DEFAULT_PROFILE_TEMPLATE],
};
interface LastKnownGoodMarker {
version: 1;
savedAt: number;
hasStateDb: boolean;
}
interface DeviceHardeningState {
consecutiveApplyFailures: number;
safeModeUntil: number | null;
lastFailureReason: DeviceApplyFailureReason | null;
lastFailureStage: string | null;
lastFailureAt: number | null;
}
const deviceHardeningState: DeviceHardeningState = {
consecutiveApplyFailures: 0,
safeModeUntil: null,
lastFailureReason: null,
lastFailureStage: null,
lastFailureAt: null,
};
function isSqliteBusyError(error: unknown): boolean {
if (!error || typeof error !== 'object') {
return false;
}
const candidate = error as { code?: string; message?: string };
if (candidate.code === 'SQLITE_BUSY' || candidate.code === 'SQLITE_LOCKED') {
return true;
}
if (typeof candidate.message === 'string') {
return candidate.message.includes('SQLITE_BUSY') || candidate.message.includes('SQLITE_LOCKED');
}
return false;
}
function resolveExistingPath(paths: string[]): string | null {
for (const targetPath of paths) {
if (fs.existsSync(targetPath)) {
return targetPath;
}
}
return null;
}
function randomHex(length: number): string {
return randomBytes(Math.ceil(length / 2))
.toString('hex')
.slice(0, length)
.toLowerCase();
}
function getLastKnownGoodSnapshotDir(): string {
return path.join(getAgentDir(), LAST_KNOWN_GOOD_DIR);
}
function getLastKnownGoodStoragePath(): string {
return path.join(getLastKnownGoodSnapshotDir(), LAST_KNOWN_GOOD_STORAGE_FILE);
}
function getLastKnownGoodStateDbPath(): string {
return path.join(getLastKnownGoodSnapshotDir(), LAST_KNOWN_GOOD_STATE_DB_FILE);
}
function getLastKnownGoodMarkerPath(): string {
return path.join(getLastKnownGoodSnapshotDir(), LAST_KNOWN_GOOD_MARKER_FILE);
}
function ensureDirectoryExists(dirPath: string): void {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
function readLastKnownGoodMarker(): LastKnownGoodMarker | null {
const markerPath = getLastKnownGoodMarkerPath();
if (!fs.existsSync(markerPath)) {
return null;
}
try {
const content = fs.readFileSync(markerPath, 'utf-8');
const parsed = JSON.parse(content) as Partial<LastKnownGoodMarker>;
if (
parsed &&
parsed.version === 1 &&
typeof parsed.savedAt === 'number' &&
typeof parsed.hasStateDb === 'boolean'
) {
return {
version: 1,
savedAt: parsed.savedAt,
hasStateDb: parsed.hasStateDb,
};
}
return null;
} catch {
return null;
}
}
function parseStorageJson(storagePath: string): Record<string, unknown> {
const content = fs.readFileSync(storagePath, 'utf-8');
const parsed = JSON.parse(content);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('storage.json top-level value must be an object');
}
return parsed as Record<string, unknown>;
}
function getTelemetryField(storage: Record<string, unknown>, key: string): string | null {
const telemetry = storage.telemetry;
if (telemetry && typeof telemetry === 'object' && !Array.isArray(telemetry)) {
const nestedValue = (telemetry as Record<string, unknown>)[key];
if (typeof nestedValue === 'string' && nestedValue.length > 0) {
return nestedValue;
}
}
const flatValue = storage[`telemetry.${key}`];
if (typeof flatValue === 'string' && flatValue.length > 0) {
return flatValue;
}
return null;
}
function ensureTelemetryObject(storage: Record<string, unknown>): Record<string, unknown> {
if (!storage.telemetry || typeof storage.telemetry !== 'object' || Array.isArray(storage.telemetry)) {
storage.telemetry = {};
}
return storage.telemetry as Record<string, unknown>;
}
function getStateDbPathFromStorage(storagePath: string): string {
return path.join(path.dirname(storagePath), 'state.vscdb');
}
function getGlobalBaselinePath(): string {
return path.join(getAgentDir(), GLOBAL_BASELINE_FILE);
}
function writeJsonAtomically(filePath: string, payload: unknown): void {
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
const content = `${JSON.stringify(payload, null, 2)}\n`;
fs.writeFileSync(tempPath, content, 'utf-8');
try {
fs.renameSync(tempPath, filePath);
} catch {
fs.copyFileSync(tempPath, filePath);
fs.unlinkSync(tempPath);
}
}
function readStateServiceMachineIdValue(dbPath: string): string | null {
if (!fs.existsSync(dbPath)) {
return null;
}
let db: Database.Database | null = null;
try {
db = new Database(dbPath, { readonly: true, fileMustExist: true });
db.pragma('busy_timeout = 3000');
const row = db
.prepare("SELECT value FROM ItemTable WHERE key = 'storage.serviceMachineId' LIMIT 1")
.get() as { value?: unknown } | undefined;
if (!row || typeof row.value !== 'string' || row.value.length === 0) {
return null;
}
return row.value;
} catch (error) {
logger.warn('Failed to read state.serviceMachineId from state.vscdb', error);
return null;
} finally {
if (db) {
db.close();
}
}
}
function ensureStorageProfileApplied(profile: DeviceProfile, storagePath: string): void {
const applied = readCurrentDeviceProfile(storagePath);
const isMatched =
applied.machineId === profile.machineId &&
applied.macMachineId === profile.macMachineId &&
applied.devDeviceId === profile.devDeviceId &&
applied.sqmId === profile.sqmId;
if (!isMatched) {
throw new Error('device_profile_integrity_check_failed_for_storage');
}
}
function ensureStateServiceMachineIdApplied(serviceMachineId: string, dbPath: string): void {
const value = readStateServiceMachineIdValue(dbPath);
if (value !== serviceMachineId) {
throw new Error('device_profile_integrity_check_failed_for_state_db');
}
}
function cleanupPathIfExists(targetPath: string): void {
if (fs.existsSync(targetPath)) {
fs.rmSync(targetPath, { force: true });
}
}
function cleanupStateDbJournalFiles(dbPath: string): void {
cleanupPathIfExists(`${dbPath}-wal`);
cleanupPathIfExists(`${dbPath}-shm`);
}
function pickDeviceProfileTemplate(): DeviceProfileTemplate {
const templates = DEVICE_PROFILE_TEMPLATES[process.platform] || [DEFAULT_PROFILE_TEMPLATE];
const randomIndex = Number.parseInt(randomHex(2), 16) % templates.length;
return templates[randomIndex];
}
function deterministicUuidFromSeed(seed: string, namespace: string): string {
const hash = createHash('sha256')
.update(`${namespace}:${seed}`)
.digest();
const bytes = Buffer.from(hash.subarray(0, 16));
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = bytes.toString('hex');
return [
hex.slice(0, 8),
hex.slice(8, 12),
hex.slice(12, 16),
hex.slice(16, 20),
hex.slice(20, 32),
].join('-');
}
function ensureGeneratedProfile(profile: DeviceProfile, template: DeviceProfileTemplate): void {
const machineIdPrefix = template.machineIdPrefix;
const machineIdSuffix = profile.machineId.slice(machineIdPrefix.length);
if (!profile.machineId.startsWith(machineIdPrefix)) {
throw new Error('generated_profile_machine_id_prefix_invalid');
}
if (machineIdSuffix.length !== template.machineIdHexLength) {
throw new Error('generated_profile_machine_id_length_invalid');
}
if (!/^[0-9a-f]+$/.test(machineIdSuffix)) {
throw new Error('generated_profile_machine_id_suffix_invalid');
}
if (!UUID_V4_REGEX.test(profile.macMachineId)) {
throw new Error('generated_profile_mac_machine_id_invalid');
}
if (!UUID_V4_REGEX.test(profile.devDeviceId)) {
throw new Error('generated_profile_dev_device_id_invalid');
}
if (!SQM_ID_REGEX.test(profile.sqmId)) {
throw new Error('generated_profile_sqm_id_invalid');
}
if (profile.macMachineId === profile.devDeviceId) {
throw new Error('generated_profile_uuid_collision');
}
}
function ensureStorageStateConsistency(
storagePath: string,
stateDbPath: string,
expectedStateDb: boolean,
requireStateMachineIdMatch: boolean = true,
): void {
const profile = readCurrentDeviceProfile(storagePath);
const hasStateDb = fs.existsSync(stateDbPath);
if (expectedStateDb && !hasStateDb) {
throw new Error('state_vscdb_missing_after_restore');
}
if (!hasStateDb) {
return;
}
const stateValue = readStateServiceMachineIdValue(stateDbPath);
if (!requireStateMachineIdMatch && !stateValue) {
return;
}
if (stateValue !== profile.devDeviceId) {
throw new Error('storage_state_service_machine_id_mismatch');
}
}
function saveLastKnownGoodSnapshot(storagePath: string, stateDbPath: string): void {
const snapshotDir = getLastKnownGoodSnapshotDir();
ensureDirectoryExists(snapshotDir);
const storageSnapshotPath = getLastKnownGoodStoragePath();
const stateDbSnapshotPath = getLastKnownGoodStateDbPath();
const markerPath = getLastKnownGoodMarkerPath();
const hasStateDb = fs.existsSync(stateDbPath);
fs.copyFileSync(storagePath, storageSnapshotPath);
if (hasStateDb) {
fs.copyFileSync(stateDbPath, stateDbSnapshotPath);
} else {
cleanupPathIfExists(stateDbSnapshotPath);
}
const marker: LastKnownGoodMarker = {
version: 1,
savedAt: Date.now(),
hasStateDb,
};
writeJsonAtomically(markerPath, marker);
}
function restoreLastKnownGoodSnapshot(storagePath: string, stateDbPath: string): boolean {
const marker = readLastKnownGoodMarker();
if (!marker) {
return false;
}
const storageSnapshotPath = getLastKnownGoodStoragePath();
const stateDbSnapshotPath = getLastKnownGoodStateDbPath();
if (!fs.existsSync(storageSnapshotPath)) {
return false;
}
try {
fs.copyFileSync(storageSnapshotPath, storagePath);
if (marker.hasStateDb && fs.existsSync(stateDbSnapshotPath)) {
fs.copyFileSync(stateDbSnapshotPath, stateDbPath);
} else {
cleanupPathIfExists(stateDbPath);
cleanupStateDbJournalFiles(stateDbPath);
}
ensureStorageStateConsistency(storagePath, stateDbPath, marker.hasStateDb);
return true;
} catch (error) {
logger.error('Failed to restore last known good device snapshot', error);
return false;
}
}
function classifyApplyFailure(stage: string, error: unknown): DeviceApplyFailureReason {
if (stage === 'prepare_backup') {
return 'backup_failed';
}
if (stage === 'prepare_write_storage') {
return 'storage_write_failed';
}
if (stage === 'prepare_sync_state') {
return 'state_sync_failed';
}
if (stage === 'verify_storage') {
return 'verify_storage_failed';
}
if (stage === 'verify_state') {
return 'verify_state_failed';
}
if (stage === 'commit_snapshot') {
return 'snapshot_update_failed';
}
if (stage === 'rollback') {
return 'rollback_failed';
}
if (error instanceof Error) {
if (error.message.includes('device_profile_integrity_check_failed_for_storage')) {
return 'verify_storage_failed';
}
if (error.message.includes('device_profile_integrity_check_failed_for_state_db')) {
return 'verify_state_failed';
}
}
return 'unknown';
}
function markApplySuccess(): void {
deviceHardeningState.consecutiveApplyFailures = 0;
deviceHardeningState.safeModeUntil = null;
}
function markApplyFailure(reason: DeviceApplyFailureReason, stage: string): void {
const now = Date.now();
deviceHardeningState.consecutiveApplyFailures += 1;
deviceHardeningState.lastFailureReason = reason;
deviceHardeningState.lastFailureStage = stage;
deviceHardeningState.lastFailureAt = now;
if (deviceHardeningState.consecutiveApplyFailures >= DEVICE_HARDENING_SAFE_MODE_THRESHOLD) {
deviceHardeningState.safeModeUntil = now + DEVICE_HARDENING_SAFE_MODE_DURATION_MS;
}
}
function isSafeModeActiveNow(): boolean {
if (!deviceHardeningState.safeModeUntil) {
return false;
}
const active = Date.now() < deviceHardeningState.safeModeUntil;
if (!active) {
deviceHardeningState.safeModeUntil = null;
}
return active;
}
function buildApplyError(
reason: DeviceApplyFailureReason,
stage: string,
original: unknown,
): Error {
const message = original instanceof Error ? original.message : String(original);
const wrapped = new Error(`device_apply_failed:${reason}:${stage}:${message}`);
(wrapped as Error & { deviceFailureReason?: DeviceApplyFailureReason; deviceFailureStage?: string })
.deviceFailureReason = reason;
(wrapped as Error & { deviceFailureReason?: DeviceApplyFailureReason; deviceFailureStage?: string })
.deviceFailureStage = stage;
return wrapped;
}
export function getDeviceHardeningSnapshot(): DeviceHardeningSnapshot {
const safeModeActive = isSafeModeActiveNow();
return {
consecutiveApplyFailures: deviceHardeningState.consecutiveApplyFailures,
safeModeActive,
safeModeUntil: safeModeActive ? deviceHardeningState.safeModeUntil : null,
lastFailureReason: deviceHardeningState.lastFailureReason,
lastFailureStage: deviceHardeningState.lastFailureStage,
lastFailureAt: deviceHardeningState.lastFailureAt,
};
}
export function isIdentityProfileApplyEnabled(): boolean {
const raw =
process.env.CRACK_IDENTITY_PROFILE_APPLY_ENABLED ??
process.env.CRACK_DEVICE_FINGERPRINT_ENABLED;
if (!raw) {
return !isSafeModeActiveNow();
}
const normalized = raw.trim().toLowerCase();
const envEnabled = (
normalized !== '0' &&
normalized !== 'false' &&
normalized !== 'off' &&
normalized !== 'no'
);
if (!envEnabled) {
return false;
}
return !isSafeModeActiveNow();
}
export function getStoragePath(): string {
const storagePath = resolveExistingPath(getAntigravityStoragePaths());
if (!storagePath) {
throw new Error('storage_json_not_found');
}
return storagePath;
}
export function getStorageDirectoryPath(): string {
return path.dirname(getStoragePath());
}
export function loadGlobalOriginalProfile(): DeviceProfile | null {
const baselinePath = getGlobalBaselinePath();
if (!fs.existsSync(baselinePath)) {
return null;
}
try {
const raw = fs.readFileSync(baselinePath, 'utf-8');
return JSON.parse(raw) as DeviceProfile;
} catch (error) {
logger.warn('Failed to load global original device profile', error);
return null;
}
}
export function saveGlobalOriginalProfile(profile: DeviceProfile): void {
const baselinePath = getGlobalBaselinePath();
const baselineDir = path.dirname(baselinePath);
if (!fs.existsSync(baselineDir)) {
fs.mkdirSync(baselineDir, { recursive: true });
}
if (fs.existsSync(baselinePath)) {
return;
}
fs.writeFileSync(baselinePath, JSON.stringify(profile, null, 2), 'utf-8');
}
export function readCurrentDeviceProfile(storagePath?: string): DeviceProfile {
const targetPath = storagePath || getStoragePath();
const storage = parseStorageJson(targetPath);
const machineId = getTelemetryField(storage, 'machineId');
const macMachineId = getTelemetryField(storage, 'macMachineId');
const devDeviceId = getTelemetryField(storage, 'devDeviceId');
const sqmId = getTelemetryField(storage, 'sqmId');
if (!machineId || !macMachineId || !devDeviceId || !sqmId) {
throw new Error('missing_device_fingerprint_fields');
}
return {
machineId,
macMachineId,
devDeviceId,
sqmId,
};
}
export function ensureGlobalOriginalFromCurrentStorage(): void {
if (loadGlobalOriginalProfile()) {
return;
}
try {
const profile = readCurrentDeviceProfile();
saveGlobalOriginalProfile(profile);
} catch (error) {
logger.warn('Failed to capture baseline device profile from storage.json', error);
}
}
export function generateDeviceProfile(): DeviceProfile {
const template = pickDeviceProfileTemplate();
const seed = randomHex(32);
const profile: DeviceProfile = {
machineId: `${template.machineIdPrefix}${seed.slice(0, template.machineIdHexLength)}`,
macMachineId: deterministicUuidFromSeed(seed, 'mac'),
devDeviceId: deterministicUuidFromSeed(seed, 'dev'),
sqmId: `{${deterministicUuidFromSeed(seed, 'sqm').toUpperCase()}}`,
};
ensureGeneratedProfile(profile, template);
return profile;
}
export function syncStateServiceMachineIdValue(serviceMachineId: string, dbPath?: string): void {
const targetDbPath = dbPath || resolveExistingPath(getAntigravityDbPaths());
if (!targetDbPath) {
throw new Error('state_vscdb_not_found');
}
const targetDbDir = path.dirname(targetDbPath);
if (!fs.existsSync(targetDbDir)) {
fs.mkdirSync(targetDbDir, { recursive: true });
}
for (let attempt = 1; attempt <= SQLITE_RETRY_COUNT; attempt += 1) {
let db: Database.Database | null = null;
try {
db = new Database(targetDbPath);
db.pragma('busy_timeout = 3000');
db.exec('CREATE TABLE IF NOT EXISTS ItemTable (key TEXT PRIMARY KEY, value TEXT);');
db.prepare("INSERT OR REPLACE INTO ItemTable (key, value) VALUES ('storage.serviceMachineId', ?)")
.run(serviceMachineId);
return;
} catch (error) {
if (isSqliteBusyError(error) && attempt < SQLITE_RETRY_COUNT) {
logger.warn(`state.vscdb busy, retrying (${attempt}/${SQLITE_RETRY_COUNT})`, error);
continue;
}
throw error;
} finally {
if (db) {
db.close();
}
}
}
throw new Error('sync_state_service_machine_id_failed');
}
export function applyDeviceProfile(profile: DeviceProfile): string {
const storagePath = getStoragePath();
const backupPath = `${storagePath}.backup`;
const stateDbPath = getStateDbPathFromStorage(storagePath);
const stateBackupPath = `${stateDbPath}.backup`;
const hadStateDb = fs.existsSync(stateDbPath);
let stage = 'prepare_backup';
try {
fs.copyFileSync(storagePath, backupPath);
if (hadStateDb) {
fs.copyFileSync(stateDbPath, stateBackupPath);
}
stage = 'prepare_write_storage';
const storage = parseStorageJson(storagePath);
const telemetry = ensureTelemetryObject(storage);
telemetry.machineId = profile.machineId;
telemetry.macMachineId = profile.macMachineId;
telemetry.devDeviceId = profile.devDeviceId;
telemetry.sqmId = profile.sqmId;
storage['telemetry.machineId'] = profile.machineId;
storage['telemetry.macMachineId'] = profile.macMachineId;
storage['telemetry.devDeviceId'] = profile.devDeviceId;
storage['telemetry.sqmId'] = profile.sqmId;
storage['storage.serviceMachineId'] = profile.devDeviceId;
writeJsonAtomically(storagePath, storage);
stage = 'prepare_sync_state';
syncStateServiceMachineIdValue(profile.devDeviceId, stateDbPath);
stage = 'verify_storage';
ensureStorageProfileApplied(profile, storagePath);
stage = 'verify_state';
ensureStateServiceMachineIdApplied(profile.devDeviceId, stateDbPath);
stage = 'commit_snapshot';
try {
saveLastKnownGoodSnapshot(storagePath, stateDbPath);
} catch (snapshotError) {
logger.warn('Failed to update last known good device snapshot', snapshotError);
}
cleanupPathIfExists(backupPath);
cleanupPathIfExists(stateBackupPath);
markApplySuccess();
return storagePath;
} catch (error) {
const classifiedReason = classifyApplyFailure(stage, error);
markApplyFailure(classifiedReason, stage);
let rollbackStage = 'rollback_immediate_backup';
let restoredFromImmediateBackup = false;
try {
if (fs.existsSync(backupPath)) {
fs.copyFileSync(backupPath, storagePath);
}
if (hadStateDb && fs.existsSync(stateBackupPath)) {
fs.copyFileSync(stateBackupPath, stateDbPath);
}
if (!hadStateDb) {
cleanupPathIfExists(stateDbPath);
cleanupStateDbJournalFiles(stateDbPath);
}
ensureStorageStateConsistency(storagePath, stateDbPath, hadStateDb, false);
restoredFromImmediateBackup = true;
} catch (restoreError) {
logger.error('Failed to restore device profile from immediate backup', restoreError);
rollbackStage = 'rollback_last_known_good';
}
if (!restoredFromImmediateBackup) {
const restoredFromLastKnownGood = restoreLastKnownGoodSnapshot(storagePath, stateDbPath);
if (!restoredFromLastKnownGood) {
logger.error('Failed to restore device profile from last known good snapshot');
markApplyFailure('rollback_failed', rollbackStage);
}
}
throw buildApplyError(classifiedReason, stage, error);
} finally {
cleanupPathIfExists(backupPath);
cleanupPathIfExists(stateBackupPath);
}
}
+91
View File
@@ -0,0 +1,91 @@
import { type DeviceProfile } from '../types/account';
import { logger } from '../utils/logger';
import { closeAntigravity, startAntigravity, _waitForProcessExit } from './process/handler';
import { applyDeviceProfile } from './device/handler';
import { type SwitchFailureReason, recordSwitchFailure, recordSwitchSuccess } from './switchMetrics';
export interface SwitchFlowOptions {
scope: 'local' | 'cloud';
targetProfile: DeviceProfile | null;
applyFingerprint: boolean;
processExitTimeoutMs: number;
performSwitch: () => Promise<void>;
}
function getErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
}
function toSwitchFailureReason(stage: string, error: unknown): SwitchFailureReason {
if (stage === 'close') {
return 'process_close_failed';
}
if (stage === 'missing_profile') {
return 'missing_bound_profile';
}
if (stage === 'apply') {
return 'apply_device_profile_failed';
}
if (stage === 'switch') {
return 'perform_switch_failed';
}
if (stage === 'start') {
return 'start_process_failed';
}
// Keep legacy compatibility with reason encoded in thrown errors.
if (error instanceof Error && error.message.includes('missing bound device profile')) {
return 'missing_bound_profile';
}
if (error instanceof Error && error.message.includes('device_apply_failed')) {
return 'apply_device_profile_failed';
}
return 'unknown';
}
export async function executeSwitchFlow(options: SwitchFlowOptions): Promise<void> {
const {
scope,
targetProfile,
applyFingerprint,
processExitTimeoutMs,
performSwitch,
} = options;
let stage = 'close';
try {
await closeAntigravity();
try {
await _waitForProcessExit(processExitTimeoutMs);
} catch (error) {
logger.warn('Process did not exit cleanly within timeout, but proceeding...', error);
}
stage = 'apply';
if (applyFingerprint) {
if (!targetProfile) {
stage = 'missing_profile';
throw new Error('Account has no bound identity profile');
}
applyDeviceProfile(targetProfile);
} else {
logger.warn(
'Identity profile apply is disabled by CRACK_IDENTITY_PROFILE_APPLY_ENABLED / CRACK_DEVICE_FINGERPRINT_ENABLED',
);
}
stage = 'switch';
await performSwitch();
stage = 'start';
await startAntigravity();
recordSwitchSuccess(scope);
} catch (error) {
const reason = toSwitchFailureReason(stage, error);
const message = getErrorMessage(error);
recordSwitchFailure(scope, reason, message);
throw error;
}
}
+80
View File
@@ -0,0 +1,80 @@
import { logger } from '../utils/logger';
type SwitchOwner = 'local-account-switch' | 'cloud-account-switch';
interface SwitchTask {
owner: SwitchOwner;
action: () => Promise<unknown>;
resolve: (value: unknown) => void;
reject: (reason?: unknown) => void;
}
let activeSwitchOwner: SwitchOwner | null = null;
const pendingQueue: SwitchTask[] = [];
let processing = false;
function processQueue(): void {
if (processing) {
return;
}
const nextTask = pendingQueue.shift();
if (!nextTask) {
return;
}
processing = true;
activeSwitchOwner = nextTask.owner;
logger.info(`Acquired switch guard: ${nextTask.owner}`);
Promise.resolve()
.then(nextTask.action)
.then((result) => {
nextTask.resolve(result);
})
.catch((error) => {
nextTask.reject(error);
})
.finally(() => {
logger.info(`Released switch guard: ${nextTask.owner}`);
activeSwitchOwner = null;
processing = false;
processQueue();
});
}
export async function runWithSwitchGuard<T>(
owner: SwitchOwner,
action: () => Promise<T>,
): Promise<T> {
return await new Promise<T>((resolve, reject) => {
pendingQueue.push({
owner,
action: async () => await action(),
resolve: (value) => {
resolve(value as T);
},
reject,
});
logger.info(
`Queued switch request: ${owner} (active=${activeSwitchOwner || 'none'}, pending=${pendingQueue.length})`,
);
processQueue();
});
}
export function getActiveSwitchOwner(): SwitchOwner | null {
return activeSwitchOwner;
}
export function getSwitchGuardSnapshot(): {
activeOwner: SwitchOwner | null;
pendingOwners: SwitchOwner[];
pendingCount: number;
} {
return {
activeOwner: activeSwitchOwner,
pendingOwners: pendingQueue.map((item) => item.owner),
pendingCount: pendingQueue.length,
};
}
+113
View File
@@ -0,0 +1,113 @@
import { logger } from '../utils/logger';
type SwitchScope = 'local' | 'cloud';
export type SwitchFailureReason =
| 'unknown'
| 'process_close_failed'
| 'missing_bound_profile'
| 'apply_device_profile_failed'
| 'perform_switch_failed'
| 'start_process_failed';
interface SwitchFailureMetadata {
reason: SwitchFailureReason;
message: string;
occurredAt: number;
}
interface SwitchMetricBucket {
switchSuccess: number;
switchFailure: number;
rollbackAttempt: number;
rollbackSuccess: number;
rollbackFailure: number;
failureReasons: Record<SwitchFailureReason, number>;
lastFailure: SwitchFailureMetadata | null;
}
type SwitchMetricMap = Record<SwitchScope, SwitchMetricBucket>;
const metrics: SwitchMetricMap = {
local: {
switchSuccess: 0,
switchFailure: 0,
rollbackAttempt: 0,
rollbackSuccess: 0,
rollbackFailure: 0,
failureReasons: {
unknown: 0,
process_close_failed: 0,
missing_bound_profile: 0,
apply_device_profile_failed: 0,
perform_switch_failed: 0,
start_process_failed: 0,
},
lastFailure: null,
},
cloud: {
switchSuccess: 0,
switchFailure: 0,
rollbackAttempt: 0,
rollbackSuccess: 0,
rollbackFailure: 0,
failureReasons: {
unknown: 0,
process_close_failed: 0,
missing_bound_profile: 0,
apply_device_profile_failed: 0,
perform_switch_failed: 0,
start_process_failed: 0,
},
lastFailure: null,
},
};
function logSnapshot(scope: SwitchScope, reason: string): void {
logger.info(`[switch-metrics] ${scope} ${reason}`, metrics[scope]);
}
export function recordSwitchSuccess(scope: SwitchScope): void {
metrics[scope].switchSuccess += 1;
logSnapshot(scope, 'switch_success');
}
export function recordSwitchFailure(
scope: SwitchScope,
reason: SwitchFailureReason = 'unknown',
message: string = '',
): void {
metrics[scope].switchFailure += 1;
metrics[scope].failureReasons[reason] += 1;
metrics[scope].lastFailure = {
reason,
message,
occurredAt: Date.now(),
};
logSnapshot(scope, 'switch_failure');
}
export function recordSwitchRollback(scope: SwitchScope, success: boolean): void {
metrics[scope].rollbackAttempt += 1;
if (success) {
metrics[scope].rollbackSuccess += 1;
} else {
metrics[scope].rollbackFailure += 1;
}
logSnapshot(scope, success ? 'rollback_success' : 'rollback_failure');
}
export function getSwitchMetricsSnapshot(): SwitchMetricMap {
return {
local: {
...metrics.local,
failureReasons: { ...metrics.local.failureReasons },
lastFailure: metrics.local.lastFailure ? { ...metrics.local.lastFailure } : null,
},
cloud: {
...metrics.cloud,
failureReasons: { ...metrics.cloud.failureReasons },
lastFailure: metrics.cloud.lastFailure ? { ...metrics.cloud.lastFailure } : null,
},
};
}