forked from NoeFabris/opencode-antigravity-auth
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathproject.ts
More file actions
320 lines (278 loc) · 8.99 KB
/
Copy pathproject.ts
File metadata and controls
320 lines (278 loc) · 8.99 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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
import {
getAntigravityHeaders,
ANTIGRAVITY_ENDPOINT_FALLBACKS,
ANTIGRAVITY_LOAD_ENDPOINTS,
ANTIGRAVITY_DEFAULT_PROJECT_ID,
} from "../constants";
import { formatRefreshParts, parseRefreshParts } from "./auth";
import { createLogger } from "./logger";
import type { OAuthAuthDetails, ProjectContextResult } from "./types";
const log = createLogger("project");
const projectContextResultCache = new Map<string, ProjectContextResult>();
const projectContextPendingCache = new Map<string, Promise<ProjectContextResult>>();
const CODE_ASSIST_METADATA = {
ideType: "ANTIGRAVITY",
platform: process.platform === "win32" ? "WINDOWS" : "MACOS",
pluginType: "GEMINI",
} as const;
interface AntigravityUserTier {
id?: string;
isDefault?: boolean;
userDefinedCloudaicompanionProject?: boolean;
}
interface LoadCodeAssistPayload {
cloudaicompanionProject?: string | { id?: string };
currentTier?: {
id?: string;
};
allowedTiers?: AntigravityUserTier[];
}
interface OnboardUserPayload {
done?: boolean;
response?: {
cloudaicompanionProject?: {
id?: string;
};
};
}
function buildMetadata(projectId?: string): Record<string, string> {
const metadata: Record<string, string> = {
ideType: CODE_ASSIST_METADATA.ideType,
platform: CODE_ASSIST_METADATA.platform,
pluginType: CODE_ASSIST_METADATA.pluginType,
};
if (projectId) {
metadata.duetProject = projectId;
}
return metadata;
}
/**
* Selects the default tier ID from the allowed tiers list.
*/
function getDefaultTierId(allowedTiers?: AntigravityUserTier[]): string | undefined {
if (!allowedTiers || allowedTiers.length === 0) {
return undefined;
}
for (const tier of allowedTiers) {
if (tier?.isDefault) {
return tier.id;
}
}
return allowedTiers[0]?.id;
}
/**
* Promise-based delay utility.
*/
function wait(ms: number): Promise<void> {
return new Promise(function (resolve) {
setTimeout(resolve, ms);
});
}
/**
* Extracts the cloudaicompanion project id from loadCodeAssist responses.
*/
function extractManagedProjectId(payload: LoadCodeAssistPayload | null): string | undefined {
if (!payload) {
return undefined;
}
if (typeof payload.cloudaicompanionProject === "string") {
return payload.cloudaicompanionProject;
}
if (payload.cloudaicompanionProject && typeof payload.cloudaicompanionProject.id === "string") {
return payload.cloudaicompanionProject.id;
}
return undefined;
}
/**
* Generates a cache key for project context based on refresh token.
*/
function getCacheKey(auth: OAuthAuthDetails): string | undefined {
const refresh = auth.refresh?.trim();
return refresh ? refresh : undefined;
}
/**
* Clears cached project context results and pending promises, globally or for a refresh key.
*/
export function invalidateProjectContextCache(refresh?: string): void {
if (!refresh) {
projectContextPendingCache.clear();
projectContextResultCache.clear();
return;
}
projectContextPendingCache.delete(refresh);
projectContextResultCache.delete(refresh);
}
/**
* Loads managed project information for the given access token and optional project.
*/
export async function loadManagedProject(
accessToken: string,
projectId?: string,
): Promise<LoadCodeAssistPayload | null> {
const metadata = buildMetadata(projectId);
const requestBody: Record<string, unknown> = { metadata };
const loadHeaders: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
"User-Agent": "google-api-nodejs-client/9.15.1",
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
"Client-Metadata": getAntigravityHeaders()["Client-Metadata"],
};
const loadEndpoints = Array.from(
new Set<string>([...ANTIGRAVITY_LOAD_ENDPOINTS, ...ANTIGRAVITY_ENDPOINT_FALLBACKS]),
);
for (const baseEndpoint of loadEndpoints) {
try {
const response = await fetch(
`${baseEndpoint}/v1internal:loadCodeAssist`,
{
method: "POST",
headers: loadHeaders,
body: JSON.stringify(requestBody),
},
);
if (!response.ok) {
continue;
}
return (await response.json()) as LoadCodeAssistPayload;
} catch (error) {
log.debug("Failed to load managed project", { endpoint: baseEndpoint, error: String(error) });
continue;
}
}
return null;
}
/**
* Onboards a managed project for the user, optionally retrying until completion.
*/
export async function onboardManagedProject(
accessToken: string,
tierId: string,
projectId?: string,
attempts = 10,
delayMs = 5000,
): Promise<string | undefined> {
const metadata = buildMetadata(projectId);
const requestBody: Record<string, unknown> = {
tierId,
metadata,
};
for (const baseEndpoint of ANTIGRAVITY_ENDPOINT_FALLBACKS) {
for (let attempt = 0; attempt < attempts; attempt += 1) {
try {
const response = await fetch(
`${baseEndpoint}/v1internal:onboardUser`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
...getAntigravityHeaders(),
},
body: JSON.stringify(requestBody),
},
);
if (!response.ok) {
break;
}
const payload = (await response.json()) as OnboardUserPayload;
const managedProjectId = payload.response?.cloudaicompanionProject?.id;
if (payload.done && managedProjectId) {
return managedProjectId;
}
if (payload.done && projectId) {
return projectId;
}
} catch (error) {
log.debug("Failed to onboard managed project", { endpoint: baseEndpoint, error: String(error) });
break;
}
await wait(delayMs);
}
}
return undefined;
}
/**
* Resolves an effective project ID for the current auth state, caching results per refresh token.
*/
export async function ensureProjectContext(auth: OAuthAuthDetails): Promise<ProjectContextResult> {
const accessToken = auth.access;
if (!accessToken) {
return { auth, effectiveProjectId: "" };
}
const cacheKey = getCacheKey(auth);
if (cacheKey) {
const cached = projectContextResultCache.get(cacheKey);
if (cached) {
return cached;
}
const pending = projectContextPendingCache.get(cacheKey);
if (pending) {
return pending;
}
}
const resolveContext = async (): Promise<ProjectContextResult> => {
const parts = parseRefreshParts(auth.refresh);
if (parts.managedProjectId) {
return { auth, effectiveProjectId: parts.managedProjectId };
}
const fallbackProjectId = ANTIGRAVITY_DEFAULT_PROJECT_ID;
const persistManagedProject = async (managedProjectId: string): Promise<ProjectContextResult> => {
const updatedAuth: OAuthAuthDetails = {
...auth,
refresh: formatRefreshParts({
refreshToken: parts.refreshToken,
projectId: parts.projectId,
managedProjectId,
}),
};
return { auth: updatedAuth, effectiveProjectId: managedProjectId };
};
// Try to resolve a managed project from Antigravity if possible.
const loadPayload = await loadManagedProject(accessToken, parts.projectId ?? fallbackProjectId);
const resolvedManagedProjectId = extractManagedProjectId(loadPayload);
if (resolvedManagedProjectId) {
return persistManagedProject(resolvedManagedProjectId);
}
// No managed project found - try to auto-provision one via onboarding.
// This handles accounts that were added before managed project provisioning was required.
const tierId = getDefaultTierId(loadPayload?.allowedTiers) ?? "FREE";
log.debug("Auto-provisioning managed project", { tierId, projectId: parts.projectId });
const provisionedProjectId = await onboardManagedProject(
accessToken,
tierId,
parts.projectId,
);
if (provisionedProjectId) {
log.debug("Successfully provisioned managed project", { provisionedProjectId });
return persistManagedProject(provisionedProjectId);
}
log.warn("Failed to provision managed project - account may not work correctly", {
hasProjectId: !!parts.projectId,
});
if (parts.projectId) {
return { auth, effectiveProjectId: parts.projectId };
}
// No project id present in auth; fall back to the hardcoded id for requests.
return { auth, effectiveProjectId: fallbackProjectId };
};
if (!cacheKey) {
return resolveContext();
}
const promise = resolveContext()
.then((result) => {
const nextKey = getCacheKey(result.auth) ?? cacheKey;
projectContextPendingCache.delete(cacheKey);
projectContextResultCache.set(nextKey, result);
if (nextKey !== cacheKey) {
projectContextResultCache.delete(cacheKey);
}
return result;
})
.catch((error) => {
projectContextPendingCache.delete(cacheKey);
throw error;
});
projectContextPendingCache.set(cacheKey, promise);
return promise;
}