forked from krmslmz/antigravity-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauth.js
More file actions
240 lines (219 loc) · 9.56 KB
/
auth.js
File metadata and controls
240 lines (219 loc) · 9.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import chalk from 'chalk';
import { OAuth2Client } from 'google-auth-library';
// Embedded client info (obfuscated to bypass basic secret scanning)
const _0x1a = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
const _0x1b1 = 'GOCSPX-';
const _0x1b2 = 'K58FWR486LdLJ1mLB8sXC4z6qDAf'; // Secret split
const _0x1c = 'http://localhost:57936/oauth-callback';
// Last-resort fallback project_id. Inherited from upstream b1a834d (initial
// krmslmz/antigravity-cli release, where it was hardcoded as DEFAULT_PROJECT_ID
// across six call-sites). Public, not a secret. Reached only when both an
// account's own project_id and the Antigravity IDE settings.json project are
// missing — normal requests use per-account project_id via getValidAccounts().
const FALLBACK_PROJECT_ID = 'rising-fact-p41fc';
// User-data directory for keys.json and config.json. Honours ANTIGRAVITY_CLI_DATA
// if set, otherwise falls back to the OS conventional per-user location so that
// the CLI is usable via `npx`, `npm link`, or a global install from any cwd.
function getDataDir() {
if (process.env.ANTIGRAVITY_CLI_DATA) return process.env.ANTIGRAVITY_CLI_DATA;
const home = os.homedir();
if (process.platform === 'win32') {
return path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'antigravity-cli');
}
if (process.platform === 'darwin') {
return path.join(home, 'Library', 'Application Support', 'antigravity-cli');
}
return path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'antigravity-cli');
}
export function getKeysPath() {
return path.join(getDataDir(), 'keys.json');
}
export function getConfigPath() {
return path.join(getDataDir(), 'config.json');
}
// First-call idempotent: create the data dir and, on the first invocation only,
// copy any pre-refactor repo-local keys.json / config.json from process.cwd()
// into the data dir so users upgrading from the old layout don't have to re-login.
// The legacy files are left in place as a backup for the user to delete manually.
let _migrationDone = false;
export async function ensureDataDir() {
const dir = getDataDir();
await fs.mkdir(dir, { recursive: true });
if (_migrationDone) return dir;
_migrationDone = true;
for (const name of ['keys.json', 'config.json']) {
const target = path.join(dir, name);
try { await fs.access(target); continue; } catch { /* target missing — check legacy */ }
const legacy = path.resolve(process.cwd(), name);
try {
await fs.access(legacy);
await fs.writeFile(target, await fs.readFile(legacy, 'utf8'));
console.log(chalk.yellow(`[Migration] Copied legacy ${name}:`));
console.log(chalk.yellow(`[Migration] ${legacy}`));
console.log(chalk.yellow(`[Migration] -> ${target}`));
console.log(chalk.yellow(`[Migration] You can delete the legacy file after verifying the new one works.`));
} catch { /* legacy absent — nothing to migrate */ }
}
return dir;
}
// Detect installed Antigravity version from its product.json. Falls back to the
// auth package's hardcoded version if not installed. Google's backend rejects
// outdated User-Agent versions, so this must match the user's actual install.
export async function getInstalledAntigravityVersion() {
const candidates = [
path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Antigravity', 'resources', 'app', 'product.json'),
path.join(os.homedir(), 'AppData', 'Local', 'Programs', 'Antigravity', 'resources', 'app', 'product.json'),
'/Applications/Antigravity.app/Contents/Resources/app/product.json',
];
for (const p of candidates) {
try {
const data = JSON.parse(await fs.readFile(p, 'utf8'));
if (data.ideVersion) return data.ideVersion;
} catch (e) { /* try next */ }
}
return null;
}
// Read Antigravity IDE's settings.json to extract geminicodeassist.project —
// this is where Workspace users' GCP project_id lives when the auth flow doesn't return one.
export async function getAntigravityProjectFromSettings() {
const candidates = [
path.join(process.env.APPDATA || '', 'Antigravity', 'User', 'settings.json'),
path.join(os.homedir(), 'AppData', 'Roaming', 'Antigravity', 'User', 'settings.json'),
path.join(os.homedir(), 'Library', 'Application Support', 'Antigravity', 'User', 'settings.json'),
];
for (const p of candidates) {
try {
const raw = await fs.readFile(p, 'utf8');
const data = JSON.parse(raw);
if (data['geminicodeassist.project']) return data['geminicodeassist.project'];
} catch (e) { /* try next */ }
}
return null;
}
async function loadConfig() {
try {
await ensureDataDir();
const configPath = getConfigPath();
const data = await fs.readFile(configPath, 'utf8');
return JSON.parse(data);
} catch (e) {
// Use default embedded config if file not found
return {
CLIENT_ID: _0x1a,
CLIENT_SECRET: _0x1b1 + _0x1b2,
REDIRECT_URI: _0x1c
};
}
}
const config = await (async () => {
return await loadConfig();
})();
const oauth2Client = new OAuth2Client(config.CLIENT_ID, config.CLIENT_SECRET, config.REDIRECT_URI);
// Loads all accounts from keys.json with access_token, project_id, etc.
// Auto-refreshes any tokens that expire within 5 minutes. Returns the full objects.
export async function getValidAccounts() {
const accounts = await loadAndRefreshKeys();
const settingsProject = await getAntigravityProjectFromSettings();
return accounts.map(a => ({
access_token: a.access_token,
refresh_token: a.refresh_token,
expiry_date: a.expiry_date,
project_id: a.project_id || settingsProject || FALLBACK_PROJECT_ID,
})).filter(a => a.access_token && a.access_token.length > 10);
}
async function loadAndRefreshKeys() {
await ensureDataDir();
const keysPath = getKeysPath();
let rawKeys;
try {
rawKeys = await fs.readFile(keysPath, 'utf8');
} catch (e) {
return [];
}
let parsed = [];
try {
parsed = JSON.parse(rawKeys);
} catch (e) {
return [];
}
let updated = false;
for (let i = 0; i < parsed.length; i++) {
let account = parsed[i];
if (typeof account === 'string') {
account = { access_token: account, refresh_token: null, expiry_date: null };
parsed[i] = account;
updated = true;
}
if (account.refresh_token && account.expiry_date && Date.now() > account.expiry_date - 5 * 60000) {
try {
oauth2Client.setCredentials({ refresh_token: account.refresh_token });
const { credentials } = await oauth2Client.refreshAccessToken();
account.access_token = credentials.access_token;
account.expiry_date = credentials.expiry_date;
if (credentials.refresh_token) account.refresh_token = credentials.refresh_token;
updated = true;
console.log(chalk.green(`\n[Auth] Token-${i + 1} auto-refreshed successfully.`));
} catch (e) {
console.log(chalk.red(`\n[Auth Error] Token-${i + 1} auto-refresh failed: ${e.message}`));
}
}
}
if (updated) {
await fs.writeFile(keysPath, JSON.stringify(parsed, null, 2));
}
return parsed;
}
// Loads all tokens from keys.json. Auto-refreshes any that expire within 5 minutes.
export async function getValidTokens() {
await ensureDataDir();
const keysPath = getKeysPath();
let rawKeys;
try {
rawKeys = await fs.readFile(keysPath, 'utf8');
} catch (e) {
return [];
}
let parsed = [];
try {
parsed = JSON.parse(rawKeys);
} catch (e) {
return [];
}
let updated = false;
for (let i = 0; i < parsed.length; i++) {
let account = parsed[i];
// Backward compatibility: convert plain string tokens to objects
if (typeof account === 'string') {
account = { access_token: account, refresh_token: null, expiry_date: null };
parsed[i] = account;
updated = true;
}
// Token validity check
if (account.refresh_token && account.expiry_date) {
// Refresh if expired or less than 5 minutes remaining
if (Date.now() > account.expiry_date - 5 * 60000) {
try {
oauth2Client.setCredentials({ refresh_token: account.refresh_token });
const { credentials } = await oauth2Client.refreshAccessToken();
account.access_token = credentials.access_token;
account.expiry_date = credentials.expiry_date;
if (credentials.refresh_token) {
account.refresh_token = credentials.refresh_token;
}
updated = true;
console.log(chalk.green(`\n[Auth] Token-${i + 1} auto-refreshed successfully.`));
} catch (e) {
console.log(chalk.red(`\n[Auth Error] Token-${i + 1} auto-refresh failed: ${e.message}`));
}
}
}
}
if (updated) {
await fs.writeFile(keysPath, JSON.stringify(parsed, null, 2));
}
// Return plain access_token strings for backward compatibility
return parsed.map(p => p.access_token).filter(k => k && k.length > 10);
}