dsh-agy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/bin/dsh-agy.js +6 -0
- package/cordis.patch.yml +13 -0
- package/lib/blob-D1e7_uT1.mjs +63 -0
- package/lib/blob-D1e7_uT1.mjs.map +1 -0
- package/lib/cli/index.d.mts +10 -0
- package/lib/cli/index.d.mts.map +1 -0
- package/lib/cli/index.mjs +317 -0
- package/lib/cli/index.mjs.map +1 -0
- package/lib/exchange-MhFUiBAm.mjs +240 -0
- package/lib/exchange-MhFUiBAm.mjs.map +1 -0
- package/lib/import-DnKzgr-0.mjs +187 -0
- package/lib/import-DnKzgr-0.mjs.map +1 -0
- package/lib/index.d.mts +8 -0
- package/lib/index.d.mts.map +1 -0
- package/lib/index.mjs +15 -0
- package/lib/index.mjs.map +1 -0
- package/lib/models-Cu1g7AXV.mjs +222 -0
- package/lib/models-Cu1g7AXV.mjs.map +1 -0
- package/lib/parse-CiEzD9d_.mjs +174 -0
- package/lib/parse-CiEzD9d_.mjs.map +1 -0
- package/lib/plugin-common-I56kmGjr.mjs +270 -0
- package/lib/plugin-common-I56kmGjr.mjs.map +1 -0
- package/lib/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/lib/session-tawi2_uh.mjs +1221 -0
- package/lib/session-tawi2_uh.mjs.map +1 -0
- package/lib/translate-h2bjH7ub.mjs +148 -0
- package/lib/translate-h2bjH7ub.mjs.map +1 -0
- package/lib/web/plugin.d.mts +8 -0
- package/lib/web/plugin.d.mts.map +1 -0
- package/lib/web/plugin.mjs +1618 -0
- package/lib/web/plugin.mjs.map +1 -0
- package/package.json +81 -0
|
@@ -0,0 +1,1221 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import lockfile from "proper-lockfile";
|
|
6
|
+
//#region src/oauth/constants.ts
|
|
7
|
+
var constants_exports = /* @__PURE__ */ __exportAll({
|
|
8
|
+
AGY_CLIENT_ID: () => AGY_CLIENT_ID,
|
|
9
|
+
AGY_CLIENT_SECRET: () => AGY_CLIENT_SECRET,
|
|
10
|
+
AGY_DEFAULT_REDIRECT_URI: () => AGY_DEFAULT_REDIRECT_URI,
|
|
11
|
+
AGY_ENDPOINT_AUTOPUSH: () => AGY_ENDPOINT_AUTOPUSH,
|
|
12
|
+
AGY_ENDPOINT_DAILY: () => AGY_ENDPOINT_DAILY,
|
|
13
|
+
AGY_ENDPOINT_DAILY_SANDBOX: () => AGY_ENDPOINT_DAILY_SANDBOX,
|
|
14
|
+
AGY_ENDPOINT_FALLBACKS: () => AGY_ENDPOINT_FALLBACKS,
|
|
15
|
+
AGY_ENDPOINT_PROD: () => AGY_ENDPOINT_PROD,
|
|
16
|
+
AGY_ENDPOINT_SKIP_STATUSES: () => AGY_ENDPOINT_SKIP_STATUSES,
|
|
17
|
+
AGY_SCOPES: () => AGY_SCOPES,
|
|
18
|
+
AGY_VERSION_FALLBACK: () => AGY_VERSION_FALLBACK,
|
|
19
|
+
OAUTH_AUTHORIZE_URL: () => OAUTH_AUTHORIZE_URL,
|
|
20
|
+
OAUTH_TOKEN_URL: () => OAUTH_TOKEN_URL,
|
|
21
|
+
OAUTH_USERINFO_URL: () => OAUTH_USERINFO_URL,
|
|
22
|
+
fetchAgyFirstOk: () => fetchAgyFirstOk,
|
|
23
|
+
getAgyBootstrapClientMetadata: () => getAgyBootstrapClientMetadata,
|
|
24
|
+
getAgyBootstrapUserAgent: () => getAgyBootstrapUserAgent
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* Antigravity (agy) OAuth and API constants.
|
|
28
|
+
*
|
|
29
|
+
* The client id/secret below are the public Google consumer-OAuth credentials
|
|
30
|
+
* shipped inside the Antigravity desktop product and its `agy` CLI; they are
|
|
31
|
+
* embedded in many public tools (see NOTICE.md). They are not secrets owned by
|
|
32
|
+
* this project.
|
|
33
|
+
*/
|
|
34
|
+
const AGY_CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
|
|
35
|
+
const AGY_CLIENT_SECRET = "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";
|
|
36
|
+
/** Required scopes. `openid` must NOT be added: it routes Google into the hanging
|
|
37
|
+
* `firstparty/nativeapp` consent for this client (verified by OmniRoute). */
|
|
38
|
+
const AGY_SCOPES = [
|
|
39
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
40
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
41
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
42
|
+
"https://www.googleapis.com/auth/cclog",
|
|
43
|
+
"https://www.googleapis.com/auth/experimentsandconfigs"
|
|
44
|
+
];
|
|
45
|
+
const OAUTH_AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
46
|
+
const OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
47
|
+
const OAUTH_USERINFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo";
|
|
48
|
+
/** Default loopback callback used by the standalone CLI listener (fixed port, like opencode). */
|
|
49
|
+
const AGY_DEFAULT_REDIRECT_URI = "http://localhost:51121/oauth-callback";
|
|
50
|
+
/**
|
|
51
|
+
* Antigravity API endpoints. The daily runtime host (no .sandbox suffix) is the
|
|
52
|
+
* live endpoint for consumer OAuth accounts — cloudcode-pa.googleapis.com
|
|
53
|
+
* answers RESOURCE_EXHAUSTED for them (verified by live probe), while the
|
|
54
|
+
* daily host answers 200. Order matters: first reachable non-429/403 wins.
|
|
55
|
+
*/
|
|
56
|
+
const AGY_ENDPOINT_DAILY = "https://daily-cloudcode-pa.googleapis.com";
|
|
57
|
+
const AGY_ENDPOINT_PROD = "https://cloudcode-pa.googleapis.com";
|
|
58
|
+
const AGY_ENDPOINT_DAILY_SANDBOX = "https://daily-cloudcode-pa.sandbox.googleapis.com";
|
|
59
|
+
const AGY_ENDPOINT_AUTOPUSH = "https://autopush-cloudcode-pa.sandbox.googleapis.com";
|
|
60
|
+
/** Runtime/bootstrap endpoint fallback order (daily first, mirroring OmniRoute). */
|
|
61
|
+
const AGY_ENDPOINT_FALLBACKS = [
|
|
62
|
+
AGY_ENDPOINT_DAILY,
|
|
63
|
+
AGY_ENDPOINT_PROD,
|
|
64
|
+
AGY_ENDPOINT_DAILY_SANDBOX,
|
|
65
|
+
AGY_ENDPOINT_AUTOPUSH
|
|
66
|
+
];
|
|
67
|
+
/** Statuses that mean "this endpoint is not usable for this account"; skip to the next. */
|
|
68
|
+
const AGY_ENDPOINT_SKIP_STATUSES = /* @__PURE__ */ new Set([429, 403]);
|
|
69
|
+
/**
|
|
70
|
+
* Try each runtime endpoint in order, skipping unusable ones (429/403/network).
|
|
71
|
+
* Returns the first other response (2xx or a real error like 400/401); when
|
|
72
|
+
* every endpoint is unusable, returns the last skipped response so the caller's
|
|
73
|
+
* classifier can still produce a meaningful error.
|
|
74
|
+
*/
|
|
75
|
+
async function fetchAgyFirstOk(urlPath, init, fetchImpl = fetch) {
|
|
76
|
+
let lastSkipped = null;
|
|
77
|
+
for (const baseEndpoint of AGY_ENDPOINT_FALLBACKS) try {
|
|
78
|
+
const response = await fetchImpl(`${baseEndpoint}${urlPath}`, init);
|
|
79
|
+
if (!AGY_ENDPOINT_SKIP_STATUSES.has(response.status)) return response;
|
|
80
|
+
lastSkipped = response;
|
|
81
|
+
} catch {}
|
|
82
|
+
if (lastSkipped) return lastSkipped;
|
|
83
|
+
throw new Error("all agy endpoints failed");
|
|
84
|
+
}
|
|
85
|
+
/** Default Antigravity client version used in User-Agent strings; overridden by the
|
|
86
|
+
* runtime version fetcher (see runtime/fingerprint.ts). */
|
|
87
|
+
const AGY_VERSION_FALLBACK = "1.18.3";
|
|
88
|
+
/** Electron-style UA used for bootstrap calls (loadCodeAssist/onboardUser). */
|
|
89
|
+
function getAgyBootstrapUserAgent(version = AGY_VERSION_FALLBACK) {
|
|
90
|
+
return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Antigravity/${version} Chrome/138.0.7204.235 Electron/37.3.1 Safari/537.36`;
|
|
91
|
+
}
|
|
92
|
+
/** Client-Metadata payload for bootstrap calls. */
|
|
93
|
+
function getAgyBootstrapClientMetadata(platform = process.platform) {
|
|
94
|
+
return `{"ideType":"ANTIGRAVITY","platform":"${platform === "win32" ? "WINDOWS" : "MACOS"}","pluginType":"GEMINI"}`;
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/runtime/identity.ts
|
|
98
|
+
/**
|
|
99
|
+
* Antigravity request/session identity generation (mirrors the active
|
|
100
|
+
* OmniRoute implementation; the archived reference stopped maintaining these).
|
|
101
|
+
*/
|
|
102
|
+
/** One request id per upstream call: `agent/<epoch>/<8 hex>`. */
|
|
103
|
+
function generateAntigravityRequestId() {
|
|
104
|
+
return `agent/${Date.now()}/${randomBytes(4).toString("hex")}`;
|
|
105
|
+
}
|
|
106
|
+
const FNV_OFFSET_I64 = -3750763044362895579n;
|
|
107
|
+
const FNV_PRIME_I64 = 1099511628211n;
|
|
108
|
+
/** 64-bit FNV-1a hash of a string (stable across processes). */
|
|
109
|
+
function fnv1a64(input) {
|
|
110
|
+
let hash = FNV_OFFSET_I64;
|
|
111
|
+
for (let i = 0; i < input.length; i++) {
|
|
112
|
+
hash ^= BigInt(input.charCodeAt(i));
|
|
113
|
+
hash = BigInt.asIntN(64, hash * FNV_PRIME_I64);
|
|
114
|
+
}
|
|
115
|
+
return hash;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Stable per-account session id: same account always derives the same id, so
|
|
119
|
+
* multi-turn context caching keys consistently; unknown accounts get a fresh
|
|
120
|
+
* random id.
|
|
121
|
+
*/
|
|
122
|
+
function deriveAntigravitySessionId(accountKey) {
|
|
123
|
+
if (!accountKey || accountKey.trim().length === 0) return null;
|
|
124
|
+
const hash = fnv1a64(accountKey.trim());
|
|
125
|
+
return `-${((hash < 0n ? -hash : hash) % 9000000000000000000n).toString()}`;
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/runtime/version.ts
|
|
129
|
+
/**
|
|
130
|
+
* Fresh Antigravity client version resolution: fetch from the product's own
|
|
131
|
+
* release feeds (6h TTL, single in-flight dedupe), falling back to the pinned
|
|
132
|
+
* pool. Keeps fingerprint User-Agent version strings current — a stale version
|
|
133
|
+
* is the most detectable fingerprint anomaly.
|
|
134
|
+
*/
|
|
135
|
+
const VERSION_CACHE_TTL_MS = 216e5;
|
|
136
|
+
const VERSION_FETCH_TIMEOUT_MS = 5e3;
|
|
137
|
+
/** Source 1: Antigravity IDE auto-update release feed (array of {version}). */
|
|
138
|
+
const IDE_RELEASE_FEED_URL = "https://antigravity-auto-updater-974169037036.us-central1.run.app/releases";
|
|
139
|
+
/** Source 2: agy CLI GitHub releases (object with tag_name). */
|
|
140
|
+
const CLI_RELEASE_URL = "https://api.github.com/repos/google-antigravity/antigravity-cli/releases/latest";
|
|
141
|
+
function compareSemver(a, b) {
|
|
142
|
+
const pa = a.split(".").map((n) => Number(n) || 0);
|
|
143
|
+
const pb = b.split(".").map((n) => Number(n) || 0);
|
|
144
|
+
for (let i = 0; i < 3; i++) {
|
|
145
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
146
|
+
if (diff !== 0) return diff;
|
|
147
|
+
}
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
function pickNewestVersion(...versions) {
|
|
151
|
+
const valid = versions.filter((v) => typeof v === "string" && /^\d+\.\d+\.\d+$/.test(v));
|
|
152
|
+
if (valid.length === 0) return null;
|
|
153
|
+
return valid.reduce((best, v) => !best || compareSemver(v, best) > 0 ? v : best, null);
|
|
154
|
+
}
|
|
155
|
+
function parseIdeReleaseFeed(payload) {
|
|
156
|
+
if (!Array.isArray(payload)) return null;
|
|
157
|
+
return pickNewestVersion(...payload.map((entry) => entry?.version));
|
|
158
|
+
}
|
|
159
|
+
function parseCliRelease(payload) {
|
|
160
|
+
if (!payload || typeof payload !== "object") return null;
|
|
161
|
+
const release = payload;
|
|
162
|
+
return pickNewestVersion(release.tag_name ?? release.name);
|
|
163
|
+
}
|
|
164
|
+
async function fetchJsonWithTimeout(fetchImpl, url) {
|
|
165
|
+
const controller = new AbortController();
|
|
166
|
+
const timeoutId = setTimeout(() => controller.abort(), VERSION_FETCH_TIMEOUT_MS);
|
|
167
|
+
try {
|
|
168
|
+
const response = await fetchImpl(url, {
|
|
169
|
+
headers: {
|
|
170
|
+
Accept: "application/json",
|
|
171
|
+
"User-Agent": "dsh-agy/0.1"
|
|
172
|
+
},
|
|
173
|
+
signal: controller.signal
|
|
174
|
+
});
|
|
175
|
+
if (!response.ok) throw new Error(`Version source ${url} returned ${response.status}`);
|
|
176
|
+
return response.json();
|
|
177
|
+
} finally {
|
|
178
|
+
clearTimeout(timeoutId);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function resolveProductVersion(state, fallback, sourceUrl, parse, fetchImpl) {
|
|
182
|
+
const now = Date.now();
|
|
183
|
+
if (state.cache && now - state.cache.fetchedAt < VERSION_CACHE_TTL_MS) return pickNewestVersion(state.cache.version, fallback) ?? fallback;
|
|
184
|
+
if (state.inFlight) return state.inFlight;
|
|
185
|
+
state.inFlight = (async () => {
|
|
186
|
+
let resolved = null;
|
|
187
|
+
try {
|
|
188
|
+
resolved = parse(await fetchJsonWithTimeout(fetchImpl, sourceUrl));
|
|
189
|
+
} catch {
|
|
190
|
+
resolved = null;
|
|
191
|
+
}
|
|
192
|
+
const version = pickNewestVersion(resolved, state.cache?.version, fallback) ?? fallback;
|
|
193
|
+
if (resolved) state.cache = {
|
|
194
|
+
version,
|
|
195
|
+
fetchedAt: Date.now()
|
|
196
|
+
};
|
|
197
|
+
return version;
|
|
198
|
+
})();
|
|
199
|
+
try {
|
|
200
|
+
return await state.inFlight;
|
|
201
|
+
} finally {
|
|
202
|
+
state.inFlight = null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** Resolve the newest known Antigravity version from the IDE feed. */
|
|
206
|
+
function resolveAntigravityIdeVersion(fetchImpl = fetch) {
|
|
207
|
+
return resolveProductVersion(ideState, AGY_VERSION_FALLBACK, IDE_RELEASE_FEED_URL, parseIdeReleaseFeed, fetchImpl);
|
|
208
|
+
}
|
|
209
|
+
/** Resolve the newest known Antigravity version from the CLI releases. */
|
|
210
|
+
function resolveAntigravityCliVersion(fetchImpl = fetch) {
|
|
211
|
+
return resolveProductVersion(cliState, AGY_VERSION_FALLBACK, CLI_RELEASE_URL, parseCliRelease, fetchImpl);
|
|
212
|
+
}
|
|
213
|
+
/** Best available version: newest of both sources, cached 6h. */
|
|
214
|
+
async function resolveAntigravityVersion(fetchImpl = fetch) {
|
|
215
|
+
const [ide, cli] = await Promise.all([resolveAntigravityIdeVersion(fetchImpl), resolveAntigravityCliVersion(fetchImpl)]);
|
|
216
|
+
return pickNewestVersion(ide, cli) ?? "1.18.3";
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Bounded resolve for fingerprint generation: never block the failure path on
|
|
220
|
+
* cold version feeds. Resolves undefined on timeout/error so callers fall
|
|
221
|
+
* back to the pinned version pool; the abandoned fetch keeps running and
|
|
222
|
+
* populates the 6h cache for later calls.
|
|
223
|
+
*/
|
|
224
|
+
async function resolveAntigravityVersionBounded(timeoutMs = 750) {
|
|
225
|
+
let timer;
|
|
226
|
+
try {
|
|
227
|
+
return await Promise.race([resolveAntigravityVersion().catch(() => void 0), new Promise((resolve) => {
|
|
228
|
+
timer = setTimeout(() => resolve(void 0), timeoutMs);
|
|
229
|
+
})]);
|
|
230
|
+
} finally {
|
|
231
|
+
clearTimeout(timer);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/** Synchronously peek at a fresh cached version (no network). */
|
|
235
|
+
function peekCachedAntigravityVersion() {
|
|
236
|
+
const now = Date.now();
|
|
237
|
+
for (const state of [ideState, cliState]) if (state.cache && now - state.cache.fetchedAt < VERSION_CACHE_TTL_MS) return state.cache.version;
|
|
238
|
+
}
|
|
239
|
+
const ideState = { inFlight: null };
|
|
240
|
+
const cliState = { inFlight: null };
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/oauth/auth.ts
|
|
243
|
+
const ACCESS_TOKEN_EXPIRY_BUFFER_MS = 6e4;
|
|
244
|
+
/** Split the packed refresh string into its constituent refresh token and project ids. */
|
|
245
|
+
function parseRefreshParts(refresh) {
|
|
246
|
+
const [refreshToken = "", projectId = "", managedProjectId = ""] = (refresh ?? "").split("|");
|
|
247
|
+
return {
|
|
248
|
+
refreshToken: refreshToken || void 0,
|
|
249
|
+
projectId: projectId || void 0,
|
|
250
|
+
managedProjectId: managedProjectId || void 0
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/** Serialize refresh token parts into the stored string format. */
|
|
254
|
+
function formatRefreshParts(parts) {
|
|
255
|
+
const projectSegment = parts.projectId ?? "";
|
|
256
|
+
const base = `${parts.refreshToken ?? ""}|${projectSegment}`;
|
|
257
|
+
return parts.managedProjectId ? `${base}|${parts.managedProjectId}` : base;
|
|
258
|
+
}
|
|
259
|
+
/** Whether the access token is expired or missing, with a buffer for clock skew. */
|
|
260
|
+
function accessTokenExpired(auth) {
|
|
261
|
+
if (!auth.access || typeof auth.expires !== "number") return true;
|
|
262
|
+
return auth.expires <= Date.now() + ACCESS_TOKEN_EXPIRY_BUFFER_MS;
|
|
263
|
+
}
|
|
264
|
+
/** Absolute expiry timestamp from a duration; malformed durations expire immediately. */
|
|
265
|
+
function calculateTokenExpiry(requestTimeMs, expiresInSeconds) {
|
|
266
|
+
const seconds = typeof expiresInSeconds === "number" ? expiresInSeconds : 3600;
|
|
267
|
+
if (Number.isNaN(seconds) || seconds <= 0) return requestTimeMs;
|
|
268
|
+
return requestTimeMs + seconds * 1e3;
|
|
269
|
+
}
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region src/oauth/refresh.ts
|
|
272
|
+
var AgyTokenRefreshError = class extends Error {
|
|
273
|
+
code;
|
|
274
|
+
description;
|
|
275
|
+
status;
|
|
276
|
+
statusText;
|
|
277
|
+
constructor(options) {
|
|
278
|
+
super(options.message);
|
|
279
|
+
this.name = "AgyTokenRefreshError";
|
|
280
|
+
this.code = options.code;
|
|
281
|
+
this.description = options.description;
|
|
282
|
+
this.status = options.status;
|
|
283
|
+
this.statusText = options.statusText;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
/** Parse Google token-endpoint error payloads, tolerating varied shapes. */
|
|
287
|
+
function parseOAuthErrorPayload(text) {
|
|
288
|
+
if (!text) return {};
|
|
289
|
+
try {
|
|
290
|
+
const payload = JSON.parse(text);
|
|
291
|
+
if (!payload || typeof payload !== "object") return { description: text };
|
|
292
|
+
let code;
|
|
293
|
+
if (typeof payload.error === "string") code = payload.error;
|
|
294
|
+
else if (payload.error && typeof payload.error === "object") {
|
|
295
|
+
code = payload.error.status ?? payload.error.code;
|
|
296
|
+
if (!payload.error_description && payload.error.message) return {
|
|
297
|
+
code,
|
|
298
|
+
description: payload.error.message
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
const description = payload.error_description;
|
|
302
|
+
if (description) return {
|
|
303
|
+
code,
|
|
304
|
+
description
|
|
305
|
+
};
|
|
306
|
+
if (payload.error && typeof payload.error === "object" && payload.error.message) return {
|
|
307
|
+
code,
|
|
308
|
+
description: payload.error.message
|
|
309
|
+
};
|
|
310
|
+
return { code };
|
|
311
|
+
} catch {
|
|
312
|
+
return { description: text };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Refresh the access token for an account. `revoked` means Google rejected the
|
|
317
|
+
* refresh token (`invalid_grant`) — the account must be re-authenticated.
|
|
318
|
+
*/
|
|
319
|
+
async function refreshAccessToken(auth) {
|
|
320
|
+
const parts = parseRefreshParts(auth.refresh);
|
|
321
|
+
if (!parts.refreshToken) return {
|
|
322
|
+
type: "failed",
|
|
323
|
+
error: new AgyTokenRefreshError({
|
|
324
|
+
message: "Missing refresh token",
|
|
325
|
+
status: 400,
|
|
326
|
+
statusText: "Bad Request"
|
|
327
|
+
})
|
|
328
|
+
};
|
|
329
|
+
try {
|
|
330
|
+
const startTime = Date.now();
|
|
331
|
+
const response = await fetch(OAUTH_TOKEN_URL, {
|
|
332
|
+
method: "POST",
|
|
333
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
334
|
+
body: new URLSearchParams({
|
|
335
|
+
grant_type: "refresh_token",
|
|
336
|
+
refresh_token: parts.refreshToken,
|
|
337
|
+
client_id: AGY_CLIENT_ID,
|
|
338
|
+
client_secret: AGY_CLIENT_SECRET
|
|
339
|
+
})
|
|
340
|
+
});
|
|
341
|
+
if (!response.ok) {
|
|
342
|
+
const errorText = await response.text().catch(() => void 0);
|
|
343
|
+
const { code, description } = parseOAuthErrorPayload(errorText);
|
|
344
|
+
const details = [code, description ?? errorText].filter(Boolean).join(": ");
|
|
345
|
+
const baseMessage = `Agy token refresh failed (${response.status} ${response.statusText})`;
|
|
346
|
+
const message = details ? `${baseMessage} - ${details}` : baseMessage;
|
|
347
|
+
if (code === "invalid_grant") return { type: "revoked" };
|
|
348
|
+
return {
|
|
349
|
+
type: "failed",
|
|
350
|
+
error: new AgyTokenRefreshError({
|
|
351
|
+
message,
|
|
352
|
+
code,
|
|
353
|
+
description: description ?? errorText,
|
|
354
|
+
status: response.status,
|
|
355
|
+
statusText: response.statusText
|
|
356
|
+
})
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
const payload = await response.json();
|
|
360
|
+
const refreshedParts = {
|
|
361
|
+
refreshToken: payload.refresh_token ?? parts.refreshToken,
|
|
362
|
+
projectId: parts.projectId,
|
|
363
|
+
managedProjectId: parts.managedProjectId
|
|
364
|
+
};
|
|
365
|
+
return {
|
|
366
|
+
type: "success",
|
|
367
|
+
auth: {
|
|
368
|
+
access: payload.access_token,
|
|
369
|
+
expires: calculateTokenExpiry(startTime, payload.expires_in),
|
|
370
|
+
refresh: formatRefreshParts(refreshedParts)
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (error instanceof AgyTokenRefreshError) throw error;
|
|
375
|
+
return {
|
|
376
|
+
type: "failed",
|
|
377
|
+
error: new AgyTokenRefreshError({
|
|
378
|
+
message: error instanceof Error ? error.message : "Unknown refresh error",
|
|
379
|
+
status: 0,
|
|
380
|
+
statusText: ""
|
|
381
|
+
})
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
//#endregion
|
|
386
|
+
//#region src/store/keyring.ts
|
|
387
|
+
/**
|
|
388
|
+
* Master-key management and AES-256-GCM secret codec for the account store.
|
|
389
|
+
*
|
|
390
|
+
* The master key lives in the DSH credentials document (`~/.dsh/.credentials.yaml`,
|
|
391
|
+
* 0600) under `AGY_MASTER_KEY` so both the in-harness plugin (via
|
|
392
|
+
* `ctx.credentials`) and the standalone `dsh-agy` CLI (direct file read) can
|
|
393
|
+
* encrypt and decrypt the same account store.
|
|
394
|
+
*/
|
|
395
|
+
const MASTER_KEY_REF = "AGY_MASTER_KEY";
|
|
396
|
+
const ENC_PREFIX$1 = "enc:v1:";
|
|
397
|
+
/** AES-256-GCM codec; ciphertext format `enc:v1:<iv-b64>:<tag-b64>:<data-b64>`. */
|
|
398
|
+
function createAesGcmCodec(key) {
|
|
399
|
+
if (key.length !== 32) throw new Error(`createAesGcmCodec: master key must be 32 bytes, got ${key.length}`);
|
|
400
|
+
return {
|
|
401
|
+
encrypt(plaintext) {
|
|
402
|
+
const iv = randomBytes(12);
|
|
403
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
404
|
+
const data = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
405
|
+
const tag = cipher.getAuthTag();
|
|
406
|
+
return `${ENC_PREFIX$1}${iv.toString("base64url")}:${tag.toString("base64url")}:${data.toString("base64url")}`;
|
|
407
|
+
},
|
|
408
|
+
decrypt(payload) {
|
|
409
|
+
if (!payload.startsWith(ENC_PREFIX$1)) throw new Error("decrypt: payload is not in encrypted format");
|
|
410
|
+
const [, , ivB64 = "", tagB64 = "", dataB64 = ""] = payload.split(":");
|
|
411
|
+
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(ivB64, "base64url"));
|
|
412
|
+
decipher.setAuthTag(Buffer.from(tagB64, "base64url"));
|
|
413
|
+
return Buffer.concat([decipher.update(Buffer.from(dataB64, "base64url")), decipher.final()]).toString("utf8");
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
/** Derive a 32-byte key from an arbitrary master-key string (SHA-256). */
|
|
418
|
+
function deriveKey(masterKey) {
|
|
419
|
+
return createHash("sha256").update(masterKey, "utf8").digest();
|
|
420
|
+
}
|
|
421
|
+
/** Default DSH home (`~/.dsh`), honoring `$DSH_HOME`. */
|
|
422
|
+
function resolveDshHome() {
|
|
423
|
+
return process.env.DSH_HOME ? resolve(process.env.DSH_HOME) : join(homedir(), ".dsh");
|
|
424
|
+
}
|
|
425
|
+
function homedir() {
|
|
426
|
+
return process.env.HOME ?? process.env.USERPROFILE ?? ".";
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Minimal reader for the DSH credentials document: a flat YAML mapping of
|
|
430
|
+
* `KEY: "value"` / `KEY: value` lines. Values are unquoted with JSON-style
|
|
431
|
+
* escaping for double-quoted scalars; anything more complex fails loudly.
|
|
432
|
+
*/
|
|
433
|
+
function readCredentialsDocument(file) {
|
|
434
|
+
if (!existsSync(file)) return /* @__PURE__ */ new Map();
|
|
435
|
+
const entries = /* @__PURE__ */ new Map();
|
|
436
|
+
const text = readFileSync(file, "utf8");
|
|
437
|
+
for (const line of text.split(/\r?\n/)) {
|
|
438
|
+
const trimmed = line.trim();
|
|
439
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("---")) continue;
|
|
440
|
+
const match = /^([A-Za-z_][A-Za-z0-9_]*):\s*(.*)$/.exec(trimmed);
|
|
441
|
+
if (!match) continue;
|
|
442
|
+
const [, key = "", rawValue = ""] = match;
|
|
443
|
+
let value;
|
|
444
|
+
if (rawValue.startsWith("\"")) try {
|
|
445
|
+
value = JSON.parse(rawValue);
|
|
446
|
+
} catch {
|
|
447
|
+
throw new Error(`readCredentialsDocument: invalid quoted value for "${key}" in ${file}`);
|
|
448
|
+
}
|
|
449
|
+
else if (rawValue.startsWith("'")) {
|
|
450
|
+
if (!rawValue.endsWith("'") || rawValue.length < 2) throw new Error(`readCredentialsDocument: unterminated single-quoted value for "${key}" in ${file}`);
|
|
451
|
+
value = rawValue.slice(1, -1).replace(/''/g, "'");
|
|
452
|
+
} else value = rawValue;
|
|
453
|
+
if (value.length === 0) continue;
|
|
454
|
+
entries.set(key, value);
|
|
455
|
+
}
|
|
456
|
+
return entries;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* POSIX owner-only enforcement. Windows mode bits never report 0600 (and
|
|
460
|
+
* chmod is a no-op there), so the check is skipped on win32; the encrypted
|
|
461
|
+
* account file and credentials document remain the only defense-in-depth
|
|
462
|
+
* layer on that platform.
|
|
463
|
+
*/
|
|
464
|
+
function assertOwnerOnly(file) {
|
|
465
|
+
if (process.platform === "win32") return;
|
|
466
|
+
const mode = statSync(file).mode & 511;
|
|
467
|
+
if (mode !== 384) throw new Error(`dsh-agy: ${file} is readable beyond its owner (mode ${mode.toString(8)}); run "chmod 600" before starting again`);
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Load the master key from the DSH credentials document. Returns undefined when
|
|
471
|
+
* the document or the reference is absent.
|
|
472
|
+
*/
|
|
473
|
+
function loadMasterKey(dshHome) {
|
|
474
|
+
const file = join(dshHome, ".credentials.yaml");
|
|
475
|
+
if (!existsSync(file)) return void 0;
|
|
476
|
+
assertOwnerOnly(file);
|
|
477
|
+
return readCredentialsDocument(file).get(MASTER_KEY_REF);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Generate and persist a fresh master key (0600) in the DSH credentials document.
|
|
481
|
+
*
|
|
482
|
+
* Append-only + atomic rename: the DSH credentials service owns this file and
|
|
483
|
+
* preserves comments and the formatting of untouched entries (and may hold
|
|
484
|
+
* YAML constructs this minimal reader cannot parse). Rewriting the whole file
|
|
485
|
+
* from our parsed view would silently drop those; appending keeps them.
|
|
486
|
+
*/
|
|
487
|
+
function persistMasterKey(dshHome, masterKey) {
|
|
488
|
+
const file = join(dshHome, ".credentials.yaml");
|
|
489
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
490
|
+
const existingText = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
491
|
+
if (existingText.length > 0 && readCredentialsDocument(file).has("AGY_MASTER_KEY")) throw new Error(`persistMasterKey: ${MASTER_KEY_REF} already exists in ${file}`);
|
|
492
|
+
const entry = `${MASTER_KEY_REF}: ${JSON.stringify(masterKey)}`;
|
|
493
|
+
const next = existingText.length > 0 && !existingText.endsWith("\n") ? `${existingText}\n${entry}\n` : `${existingText}${entry}\n`;
|
|
494
|
+
const tmp = `${file}.tmp-masterkey`;
|
|
495
|
+
writeFileSync(tmp, next, { mode: 384 });
|
|
496
|
+
renameSync(tmp, file);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Resolve (load or create) the master key for a dsh home, then build the codec.
|
|
500
|
+
* Creating a key writes the credentials document; read-only setups should call
|
|
501
|
+
* {@link loadMasterKey} first and surface a friendly error instead.
|
|
502
|
+
*/
|
|
503
|
+
function resolveMasterKeyCodec(dshHome) {
|
|
504
|
+
let masterKey = loadMasterKey(dshHome);
|
|
505
|
+
let created = false;
|
|
506
|
+
if (!masterKey) {
|
|
507
|
+
masterKey = randomBytes(32).toString("hex");
|
|
508
|
+
persistMasterKey(dshHome, masterKey);
|
|
509
|
+
created = true;
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
codec: createAesGcmCodec(deriveKey(masterKey)),
|
|
513
|
+
created
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
const ENC_PREFIX = "enc:v1:";
|
|
517
|
+
const properFileLock = { async withLock(file, fn) {
|
|
518
|
+
const release = await lockfile.lock(file, {
|
|
519
|
+
stale: 3e4,
|
|
520
|
+
update: 1e4,
|
|
521
|
+
retries: {
|
|
522
|
+
retries: 10,
|
|
523
|
+
factor: 1.5,
|
|
524
|
+
minTimeout: 50,
|
|
525
|
+
maxTimeout: 2e3
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
try {
|
|
529
|
+
return await fn();
|
|
530
|
+
} finally {
|
|
531
|
+
await release();
|
|
532
|
+
}
|
|
533
|
+
} };
|
|
534
|
+
function isEncrypted(value) {
|
|
535
|
+
return value.startsWith(ENC_PREFIX);
|
|
536
|
+
}
|
|
537
|
+
/** Decrypt one account's refresh field when encrypted; plaintext passes through (legacy). */
|
|
538
|
+
function decryptAccount(account, codec) {
|
|
539
|
+
if (isEncrypted(account.refresh)) return {
|
|
540
|
+
...account,
|
|
541
|
+
refresh: codec.decrypt(account.refresh)
|
|
542
|
+
};
|
|
543
|
+
return account;
|
|
544
|
+
}
|
|
545
|
+
/** Encrypt one account's refresh field (plaintext stays plaintext if no codec change). */
|
|
546
|
+
function encryptAccount(account, codec) {
|
|
547
|
+
if (!isEncrypted(account.refresh)) return {
|
|
548
|
+
...account,
|
|
549
|
+
refresh: codec.encrypt(account.refresh)
|
|
550
|
+
};
|
|
551
|
+
return account;
|
|
552
|
+
}
|
|
553
|
+
function decryptStorage(storage, codec) {
|
|
554
|
+
return {
|
|
555
|
+
...storage,
|
|
556
|
+
accounts: storage.accounts.map((a) => decryptAccount(a, codec))
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
function encryptStorage(storage, codec) {
|
|
560
|
+
return {
|
|
561
|
+
...storage,
|
|
562
|
+
accounts: storage.accounts.map((a) => encryptAccount(a, codec))
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
function migrateV1ToV2(v1) {
|
|
566
|
+
return {
|
|
567
|
+
version: 2,
|
|
568
|
+
accounts: v1.accounts.map((acc) => ({
|
|
569
|
+
email: acc.email,
|
|
570
|
+
refreshToken: acc.refreshToken,
|
|
571
|
+
projectId: acc.projectId,
|
|
572
|
+
managedProjectId: acc.managedProjectId,
|
|
573
|
+
addedAt: acc.addedAt,
|
|
574
|
+
lastUsed: acc.lastUsed,
|
|
575
|
+
lastSwitchReason: acc.lastSwitchReason,
|
|
576
|
+
rateLimitResetTimes: acc.isRateLimited && acc.rateLimitResetTime ? { default: acc.rateLimitResetTime } : void 0
|
|
577
|
+
})),
|
|
578
|
+
activeIndex: v1.activeIndex
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
function migrateV2ToV3(v2) {
|
|
582
|
+
return {
|
|
583
|
+
version: 3,
|
|
584
|
+
accounts: v2.accounts.map((acc) => ({
|
|
585
|
+
email: acc.email,
|
|
586
|
+
refresh: `${acc.refreshToken}|${acc.projectId ?? ""}|${acc.managedProjectId ?? ""}`,
|
|
587
|
+
projectId: acc.projectId,
|
|
588
|
+
managedProjectId: acc.managedProjectId,
|
|
589
|
+
addedAt: acc.addedAt,
|
|
590
|
+
lastUsed: acc.lastUsed,
|
|
591
|
+
enabled: true,
|
|
592
|
+
lastSwitchReason: acc.lastSwitchReason,
|
|
593
|
+
rateLimitResetTimes: acc.rateLimitResetTimes
|
|
594
|
+
})),
|
|
595
|
+
activeIndex: v2.activeIndex
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
function migrateV3ToV4(v3) {
|
|
599
|
+
return {
|
|
600
|
+
version: 4,
|
|
601
|
+
accounts: v3.accounts,
|
|
602
|
+
activeIndex: v3.activeIndex
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function migrateStorage(raw) {
|
|
606
|
+
switch (raw.version) {
|
|
607
|
+
case 1: return migrateStorage(migrateV1ToV2(raw));
|
|
608
|
+
case 2: return migrateStorage(migrateV2ToV3(raw));
|
|
609
|
+
case 3: return migrateStorage(migrateV3ToV4(raw));
|
|
610
|
+
case 4: return raw;
|
|
611
|
+
default: throw new Error(`migrateStorage: unsupported storage version ${raw.version}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
var JsonAccountStore = class {
|
|
615
|
+
file;
|
|
616
|
+
codec;
|
|
617
|
+
lock;
|
|
618
|
+
constructor(options) {
|
|
619
|
+
this.file = options.file;
|
|
620
|
+
this.codec = options.codec;
|
|
621
|
+
this.lock = options.lock ?? properFileLock;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Ensure the store document exists before locking: proper-lockfile resolves
|
|
625
|
+
* the target's realpath and fails with ENOENT when it is absent.
|
|
626
|
+
*/
|
|
627
|
+
ensureFile() {
|
|
628
|
+
if (existsSync(this.file)) return;
|
|
629
|
+
mkdirSync(dirname(this.file), { recursive: true });
|
|
630
|
+
const tmp = `${this.file}.tmp-init`;
|
|
631
|
+
writeFileSync(tmp, JSON.stringify({
|
|
632
|
+
version: 4,
|
|
633
|
+
accounts: [],
|
|
634
|
+
activeIndex: 0
|
|
635
|
+
}) + "\n", { mode: 384 });
|
|
636
|
+
renameSync(tmp, this.file);
|
|
637
|
+
}
|
|
638
|
+
async load() {
|
|
639
|
+
let text;
|
|
640
|
+
try {
|
|
641
|
+
text = readFileSync(this.file, "utf8");
|
|
642
|
+
} catch (error) {
|
|
643
|
+
if (error.code === "ENOENT") return {
|
|
644
|
+
version: 4,
|
|
645
|
+
accounts: [],
|
|
646
|
+
activeIndex: 0
|
|
647
|
+
};
|
|
648
|
+
throw error;
|
|
649
|
+
}
|
|
650
|
+
assertOwnerOnly(this.file);
|
|
651
|
+
return decryptStorage(migrateStorage(JSON.parse(text)), this.codec);
|
|
652
|
+
}
|
|
653
|
+
async save(storage) {
|
|
654
|
+
const encrypted = encryptStorage(storage, this.codec);
|
|
655
|
+
mkdirSync(dirname(this.file), { recursive: true });
|
|
656
|
+
const tmp = `${this.file}.tmp`;
|
|
657
|
+
writeFileSync(tmp, JSON.stringify(encrypted, null, 2) + "\n", { mode: 384 });
|
|
658
|
+
renameSync(tmp, this.file);
|
|
659
|
+
}
|
|
660
|
+
async mutate(fn) {
|
|
661
|
+
this.ensureFile();
|
|
662
|
+
return this.lock.withLock(this.file, async () => {
|
|
663
|
+
const storage = await this.load();
|
|
664
|
+
const result = await fn(storage);
|
|
665
|
+
await this.save(storage);
|
|
666
|
+
return result;
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
/** Pick the active account, falling back to the first enabled one. */
|
|
671
|
+
function resolveActiveAccount(storage) {
|
|
672
|
+
const accounts = storage.accounts;
|
|
673
|
+
if (accounts.length === 0) return void 0;
|
|
674
|
+
const active = accounts[storage.activeIndex];
|
|
675
|
+
if (active && active.enabled !== false) return {
|
|
676
|
+
account: active,
|
|
677
|
+
index: storage.activeIndex
|
|
678
|
+
};
|
|
679
|
+
const fallbackIndex = accounts.findIndex((a) => a.enabled !== false);
|
|
680
|
+
if (fallbackIndex === -1) return void 0;
|
|
681
|
+
return {
|
|
682
|
+
account: accounts[fallbackIndex],
|
|
683
|
+
index: fallbackIndex
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
//#endregion
|
|
687
|
+
//#region src/runtime/rotation.ts
|
|
688
|
+
const BACKOFF_TIERS_MS = [
|
|
689
|
+
5e3,
|
|
690
|
+
1e4,
|
|
691
|
+
2e4,
|
|
692
|
+
3e4,
|
|
693
|
+
6e4
|
|
694
|
+
];
|
|
695
|
+
function backoffFor(consecutiveFailures, maxJitterMs = 1e3) {
|
|
696
|
+
const index = Math.min(Math.max(consecutiveFailures, 0), BACKOFF_TIERS_MS.length - 1);
|
|
697
|
+
return (BACKOFF_TIERS_MS[index] ?? BACKOFF_TIERS_MS[BACKOFF_TIERS_MS.length - 1]) + Math.floor(Math.random() * maxJitterMs);
|
|
698
|
+
}
|
|
699
|
+
/** Whether the account is currently in a cooldown window. */
|
|
700
|
+
function isCoolingDown(account, now = Date.now()) {
|
|
701
|
+
return (account.coolingDownUntil ?? 0) > now;
|
|
702
|
+
}
|
|
703
|
+
/** Clear expired rate limits and cooldowns in place. */
|
|
704
|
+
function clearExpiredState(account, now = Date.now()) {
|
|
705
|
+
if (account.rateLimitResetTimes) {
|
|
706
|
+
const fresh = Object.fromEntries(Object.entries(account.rateLimitResetTimes).filter(([, reset]) => reset > now));
|
|
707
|
+
account.rateLimitResetTimes = Object.keys(fresh).length > 0 ? fresh : void 0;
|
|
708
|
+
}
|
|
709
|
+
if (account.coolingDownUntil && account.coolingDownUntil <= now) {
|
|
710
|
+
account.coolingDownUntil = void 0;
|
|
711
|
+
account.cooldownReason = void 0;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
/** 24h cooldown for a fully exhausted daily quota (single-account: stop hitting the wall). */
|
|
715
|
+
const FULL_QUOTA_COOLDOWN_MS = 864e5;
|
|
716
|
+
/**
|
|
717
|
+
* Decide what to do after one failed attempt.
|
|
718
|
+
* @param kind - classified failure kind.
|
|
719
|
+
* @param category - 429 sub-category when kind is rate-limit.
|
|
720
|
+
* @param account - the account that failed (mutated with cooldown/rate-limit state).
|
|
721
|
+
* @param consecutiveFailures - consecutive failures on this account.
|
|
722
|
+
* @param retryAfterMs - server-provided retry delay when present.
|
|
723
|
+
*/
|
|
724
|
+
function decideRotation(kind, account, consecutiveFailures, retryAfterMs, category = "unknown") {
|
|
725
|
+
const now = Date.now();
|
|
726
|
+
const backoffMs = backoffFor(consecutiveFailures);
|
|
727
|
+
switch (kind) {
|
|
728
|
+
case "rate-limit":
|
|
729
|
+
if (category === "soft_rate_limit") return {
|
|
730
|
+
action: "retry",
|
|
731
|
+
backoffMs: Math.min(retryAfterMs ?? backoffMs, 3e3)
|
|
732
|
+
};
|
|
733
|
+
if (category === "quota_exhausted") {
|
|
734
|
+
account.coolingDownUntil = now + FULL_QUOTA_COOLDOWN_MS;
|
|
735
|
+
account.cooldownReason = void 0;
|
|
736
|
+
return {
|
|
737
|
+
action: "cool",
|
|
738
|
+
backoffMs: FULL_QUOTA_COOLDOWN_MS
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
account.coolingDownUntil = now + (retryAfterMs ?? 3e5);
|
|
742
|
+
account.cooldownReason = void 0;
|
|
743
|
+
return {
|
|
744
|
+
action: "rotate",
|
|
745
|
+
backoffMs
|
|
746
|
+
};
|
|
747
|
+
case "auth-failure":
|
|
748
|
+
account.verificationRequired = true;
|
|
749
|
+
account.verificationRequiredAt = now;
|
|
750
|
+
account.verificationRequiredReason = "auth-failure";
|
|
751
|
+
account.enabled = false;
|
|
752
|
+
return { action: "revoke" };
|
|
753
|
+
case "network-error":
|
|
754
|
+
account.coolingDownUntil = now + backoffMs;
|
|
755
|
+
account.cooldownReason = "network-error";
|
|
756
|
+
return {
|
|
757
|
+
action: "rotate",
|
|
758
|
+
backoffMs
|
|
759
|
+
};
|
|
760
|
+
case "project-error":
|
|
761
|
+
account.coolingDownUntil = now + backoffMs;
|
|
762
|
+
account.cooldownReason = "project-error";
|
|
763
|
+
return {
|
|
764
|
+
action: "cool",
|
|
765
|
+
backoffMs
|
|
766
|
+
};
|
|
767
|
+
case "transient": return {
|
|
768
|
+
action: "retry",
|
|
769
|
+
backoffMs
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Pick the next account index for rotation (round-robin across enabled,
|
|
775
|
+
* non-cooling accounts; falls back to the active one when all are cooling).
|
|
776
|
+
*/
|
|
777
|
+
function pickNextAccountIndex(accounts, currentIndex, now = Date.now()) {
|
|
778
|
+
if (accounts.length <= 1) return currentIndex;
|
|
779
|
+
const enabled = accounts.map((a, i) => ({
|
|
780
|
+
account: a,
|
|
781
|
+
index: i
|
|
782
|
+
})).filter(({ account, index }) => index !== currentIndex && account.enabled !== false && !isCoolingDown(account, now));
|
|
783
|
+
if (enabled.length === 0) return currentIndex;
|
|
784
|
+
return (enabled.find((e) => e.index > currentIndex) ?? enabled[0]).index;
|
|
785
|
+
}
|
|
786
|
+
//#endregion
|
|
787
|
+
//#region src/runtime/fingerprint.ts
|
|
788
|
+
/**
|
|
789
|
+
* Device fingerprint generation for rate-limit mitigation (two layers):
|
|
790
|
+
*
|
|
791
|
+
* 1. Per-request randomized headers — platform/arch/SDK-client pools.
|
|
792
|
+
* 2. Per-account persistent fingerprint — deviceId/sessionToken/UA snapshot
|
|
793
|
+
* with bounded history (≤5 versions, restorable), regenerated when an
|
|
794
|
+
* account's capacity looks exhausted.
|
|
795
|
+
*
|
|
796
|
+
* All tunable pools live in fingerprint-data.json so they can be updated
|
|
797
|
+
* without a code release (the reference implementation stopped at an old
|
|
798
|
+
* version string once archived — that staleness is the detectable signal).
|
|
799
|
+
*/
|
|
800
|
+
const DEFAULT_FINGERPRINT_DATA = {
|
|
801
|
+
comment: "Hot-updatable fingerprint data. Version pools and client strings must track the real Antigravity product; update this file (or ship an override) when a release ships. Kept out of code so stale versions never get compiled in.",
|
|
802
|
+
versionPool: [
|
|
803
|
+
"1.18.3",
|
|
804
|
+
"1.17.0",
|
|
805
|
+
"1.16.0"
|
|
806
|
+
],
|
|
807
|
+
platforms: [
|
|
808
|
+
"windows/amd64",
|
|
809
|
+
"darwin/arm64",
|
|
810
|
+
"darwin/amd64"
|
|
811
|
+
],
|
|
812
|
+
architectures: ["x64", "arm64"],
|
|
813
|
+
osVersions: {
|
|
814
|
+
"darwin": [
|
|
815
|
+
"10.15.7",
|
|
816
|
+
"11.6.8",
|
|
817
|
+
"12.6.3",
|
|
818
|
+
"13.5.2",
|
|
819
|
+
"14.2.1",
|
|
820
|
+
"14.5"
|
|
821
|
+
],
|
|
822
|
+
"win32": [
|
|
823
|
+
"10.0.19041",
|
|
824
|
+
"10.0.19042",
|
|
825
|
+
"10.0.19043",
|
|
826
|
+
"10.0.22000",
|
|
827
|
+
"10.0.22621",
|
|
828
|
+
"10.0.22631"
|
|
829
|
+
],
|
|
830
|
+
"linux": [
|
|
831
|
+
"5.15.0",
|
|
832
|
+
"5.19.0",
|
|
833
|
+
"6.1.0",
|
|
834
|
+
"6.2.0",
|
|
835
|
+
"6.5.0",
|
|
836
|
+
"6.6.0"
|
|
837
|
+
]
|
|
838
|
+
},
|
|
839
|
+
sdkClients: [
|
|
840
|
+
"google-cloud-sdk vscode_cloudshelleditor/0.1",
|
|
841
|
+
"google-cloud-sdk vscode/1.86.0",
|
|
842
|
+
"google-cloud-sdk vscode/1.87.0",
|
|
843
|
+
"google-cloud-sdk vscode/1.96.0"
|
|
844
|
+
],
|
|
845
|
+
ideTypes: ["ANTIGRAVITY"],
|
|
846
|
+
pluginTypes: ["GEMINI"]
|
|
847
|
+
};
|
|
848
|
+
const USER_OVERRIDE_FILE = "agy-fingerprint-data.json";
|
|
849
|
+
/**
|
|
850
|
+
* Effective fingerprint data: a user override at `$DSH_HOME/agy-fingerprint-data.json`
|
|
851
|
+
* wins when present and parseable (hot-updatable without a code release — the
|
|
852
|
+
* bundled copy is compiled in), otherwise the bundled defaults.
|
|
853
|
+
*/
|
|
854
|
+
let cachedData = null;
|
|
855
|
+
function getFingerprintData() {
|
|
856
|
+
if (cachedData) return cachedData;
|
|
857
|
+
try {
|
|
858
|
+
const dshHome = process.env.DSH_HOME ? process.env.DSH_HOME : join(process.env.HOME ?? ".", ".dsh");
|
|
859
|
+
const overrideFile = join(dshHome, USER_OVERRIDE_FILE);
|
|
860
|
+
if (existsSync(overrideFile)) {
|
|
861
|
+
const parsed = JSON.parse(readFileSync(overrideFile, "utf8"));
|
|
862
|
+
if (parsed && Array.isArray(parsed.versionPool) && parsed.versionPool.length > 0) {
|
|
863
|
+
cachedData = parsed;
|
|
864
|
+
return cachedData;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
} catch {}
|
|
868
|
+
cachedData = DEFAULT_FINGERPRINT_DATA;
|
|
869
|
+
return cachedData;
|
|
870
|
+
}
|
|
871
|
+
function randomFrom(arr) {
|
|
872
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
873
|
+
}
|
|
874
|
+
function platformToDisplayName(platform) {
|
|
875
|
+
return platform.startsWith("windows") ? "WINDOWS" : "MACOS";
|
|
876
|
+
}
|
|
877
|
+
/** Generate a randomized device fingerprint representing one apparent device. */
|
|
878
|
+
function generateFingerprint(data = getFingerprintData(), version = randomFrom(data.versionPool)) {
|
|
879
|
+
const platform = randomFrom(data.platforms);
|
|
880
|
+
return {
|
|
881
|
+
deviceId: randomUUID(),
|
|
882
|
+
sessionToken: randomBytes(16).toString("hex"),
|
|
883
|
+
userAgent: `antigravity/${version} ${platform}`,
|
|
884
|
+
apiClient: randomFrom(data.sdkClients),
|
|
885
|
+
clientMetadata: {
|
|
886
|
+
ideType: randomFrom(data.ideTypes),
|
|
887
|
+
platform: platformToDisplayName(platform),
|
|
888
|
+
pluginType: randomFrom(data.pluginTypes)
|
|
889
|
+
},
|
|
890
|
+
createdAt: Date.now()
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
/** Per-request randomized headers (platform + SDK client pools). */
|
|
894
|
+
function getRandomizedHeaders(data = getFingerprintData(), version = randomFrom(data.versionPool)) {
|
|
895
|
+
const platform = randomFrom(data.platforms);
|
|
896
|
+
return {
|
|
897
|
+
"User-Agent": `antigravity/${version} ${platform}`,
|
|
898
|
+
"X-Goog-Api-Client": randomFrom(data.sdkClients),
|
|
899
|
+
"Client-Metadata": JSON.stringify({
|
|
900
|
+
ideType: randomFrom(data.ideTypes),
|
|
901
|
+
platform: platformToDisplayName(platform),
|
|
902
|
+
pluginType: randomFrom(data.pluginTypes)
|
|
903
|
+
})
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
/** Rewrite the version inside a fingerprint UA; reports whether it changed. */
|
|
907
|
+
function updateFingerprintVersion(fingerprint, version) {
|
|
908
|
+
const pattern = /^(antigravity\/)([\d.]+)/;
|
|
909
|
+
const match = fingerprint.userAgent.match(pattern);
|
|
910
|
+
if (!match || match[2] === version) return false;
|
|
911
|
+
fingerprint.userAgent = fingerprint.userAgent.replace(pattern, `$1${version}`);
|
|
912
|
+
return true;
|
|
913
|
+
}
|
|
914
|
+
/** Append a fingerprint to the account history (bounded), then use it as current. */
|
|
915
|
+
function recordFingerprintVersion(history, fingerprint, reason) {
|
|
916
|
+
return [...history ?? [], {
|
|
917
|
+
fingerprint,
|
|
918
|
+
timestamp: Date.now(),
|
|
919
|
+
reason
|
|
920
|
+
}].slice(-5);
|
|
921
|
+
}
|
|
922
|
+
//#endregion
|
|
923
|
+
//#region src/session.ts
|
|
924
|
+
/**
|
|
925
|
+
* Resolve the impersonation headers for one request from the account's
|
|
926
|
+
* persistent fingerprint (stable identity), falling back to full per-request
|
|
927
|
+
* randomization when the account has no fingerprint yet.
|
|
928
|
+
*/
|
|
929
|
+
function impersonationHeadersFor(account) {
|
|
930
|
+
const fingerprint = account.fingerprint;
|
|
931
|
+
if (fingerprint) return {
|
|
932
|
+
"User-Agent": fingerprint.userAgent,
|
|
933
|
+
"X-Goog-Api-Client": fingerprint.apiClient,
|
|
934
|
+
"Client-Metadata": JSON.stringify(fingerprint.clientMetadata)
|
|
935
|
+
};
|
|
936
|
+
const randomized = getRandomizedHeaders(DEFAULT_FINGERPRINT_DATA);
|
|
937
|
+
return {
|
|
938
|
+
"User-Agent": randomized["User-Agent"],
|
|
939
|
+
"X-Goog-Api-Client": randomized["X-Goog-Api-Client"],
|
|
940
|
+
"Client-Metadata": randomized["Client-Metadata"]
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
var AgySessionManager = class {
|
|
944
|
+
store;
|
|
945
|
+
onRotate;
|
|
946
|
+
tokenCache = /* @__PURE__ */ new Map();
|
|
947
|
+
/** In-flight refresh promises keyed by account: concurrent requests share one refresh. */
|
|
948
|
+
refreshInFlight = /* @__PURE__ */ new Map();
|
|
949
|
+
failureCounts = /* @__PURE__ */ new Map();
|
|
950
|
+
/** Accounts whose request-time project discovery already failed (no retry per request). */
|
|
951
|
+
projectRetryFailed = /* @__PURE__ */ new Set();
|
|
952
|
+
constructor(options) {
|
|
953
|
+
this.store = options.store;
|
|
954
|
+
this.onRotate = options.onRotate;
|
|
955
|
+
}
|
|
956
|
+
accountKey(account) {
|
|
957
|
+
return account.email ?? `idx-${account.refresh}`;
|
|
958
|
+
}
|
|
959
|
+
/** Resolve a usable access token for the account, refreshing when expired. */
|
|
960
|
+
async accessTokenFor(account) {
|
|
961
|
+
const key = this.accountKey(account);
|
|
962
|
+
const cached = this.tokenCache.get(key);
|
|
963
|
+
if (cached && !accessTokenExpired({
|
|
964
|
+
access: cached.access,
|
|
965
|
+
expires: cached.expires,
|
|
966
|
+
refresh: account.refresh
|
|
967
|
+
})) return {
|
|
968
|
+
access: cached.access,
|
|
969
|
+
expires: cached.expires,
|
|
970
|
+
refresh: account.refresh
|
|
971
|
+
};
|
|
972
|
+
const inFlight = this.refreshInFlight.get(key);
|
|
973
|
+
if (inFlight) return inFlight;
|
|
974
|
+
const refreshing = (async () => {
|
|
975
|
+
const result = await refreshAccessToken({
|
|
976
|
+
access: cached?.access ?? "",
|
|
977
|
+
expires: cached?.expires ?? 0,
|
|
978
|
+
refresh: account.refresh
|
|
979
|
+
});
|
|
980
|
+
if (result.type === "success") {
|
|
981
|
+
this.tokenCache.set(key, {
|
|
982
|
+
access: result.auth.access,
|
|
983
|
+
expires: result.auth.expires
|
|
984
|
+
});
|
|
985
|
+
return result.auth;
|
|
986
|
+
}
|
|
987
|
+
if (result.type === "revoked") {
|
|
988
|
+
this.tokenCache.delete(key);
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
})();
|
|
992
|
+
this.refreshInFlight.set(key, refreshing);
|
|
993
|
+
try {
|
|
994
|
+
return await refreshing;
|
|
995
|
+
} finally {
|
|
996
|
+
this.refreshInFlight.delete(key);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
async pickAccount(storage) {
|
|
1000
|
+
const now = Date.now();
|
|
1001
|
+
for (const account of storage.accounts) clearExpiredState(account, now);
|
|
1002
|
+
let resolved = resolveActiveAccount(storage);
|
|
1003
|
+
if (!resolved) return void 0;
|
|
1004
|
+
if (isCoolingDown(resolved.account, now)) {
|
|
1005
|
+
const nextIndex = pickNextAccountIndex(storage.accounts, resolved.index, now);
|
|
1006
|
+
if (nextIndex !== resolved.index) resolved = {
|
|
1007
|
+
account: storage.accounts[nextIndex],
|
|
1008
|
+
index: nextIndex
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
return resolved;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Adapter hook: resolve the active session (refresh if needed), healing a
|
|
1015
|
+
* missing projectId at request time — the OAuth-time loadCodeAssist may have
|
|
1016
|
+
* transiently failed even when the Google account owns a Cloud Code project
|
|
1017
|
+
* (mirrors OmniRoute's ensureAntigravityProjectAssigned + persistence).
|
|
1018
|
+
*/
|
|
1019
|
+
async getSession() {
|
|
1020
|
+
const storage = await this.store.load();
|
|
1021
|
+
const picked = await this.pickAccount(storage);
|
|
1022
|
+
if (!picked) return void 0;
|
|
1023
|
+
const auth = await this.accessTokenFor(picked.account);
|
|
1024
|
+
if (!auth) return void 0;
|
|
1025
|
+
const key = this.accountKey(picked.account);
|
|
1026
|
+
if (!picked.account.projectId && !this.projectRetryFailed.has(key)) try {
|
|
1027
|
+
const { loadCodeAssist } = await import("./exchange-MhFUiBAm.mjs").then((n) => n.n);
|
|
1028
|
+
const { projectId } = await loadCodeAssist(auth.access);
|
|
1029
|
+
if (projectId) {
|
|
1030
|
+
await this.store.mutate((s) => {
|
|
1031
|
+
const account = s.accounts[picked.index];
|
|
1032
|
+
if (account) {
|
|
1033
|
+
account.projectId = projectId;
|
|
1034
|
+
const parts = parseRefreshParts(account.refresh);
|
|
1035
|
+
account.refresh = formatRefreshParts({
|
|
1036
|
+
refreshToken: parts.refreshToken,
|
|
1037
|
+
projectId,
|
|
1038
|
+
managedProjectId: parts.managedProjectId
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
picked.account.projectId = projectId;
|
|
1043
|
+
} else this.projectRetryFailed.add(key);
|
|
1044
|
+
} catch {
|
|
1045
|
+
this.projectRetryFailed.add(key);
|
|
1046
|
+
}
|
|
1047
|
+
return {
|
|
1048
|
+
auth,
|
|
1049
|
+
account: picked.account,
|
|
1050
|
+
index: picked.index,
|
|
1051
|
+
impersonation: impersonationHeadersFor(picked.account)
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
/** Adapter hook: apply rotation decisions and fingerprint regeneration. */
|
|
1055
|
+
async reportFailure(kind, session, info) {
|
|
1056
|
+
const storage = await this.store.load();
|
|
1057
|
+
const account = storage.accounts[session.index];
|
|
1058
|
+
if (!account) return;
|
|
1059
|
+
const key = this.accountKey(account);
|
|
1060
|
+
const consecutive = (this.failureCounts.get(key) ?? 0) + 1;
|
|
1061
|
+
this.failureCounts.set(key, consecutive);
|
|
1062
|
+
const decision = decideRotation(kind, account, consecutive, info?.retryAfterMs, info?.rateLimitCategory);
|
|
1063
|
+
if (decision.action === "revoke") {
|
|
1064
|
+
this.tokenCache.delete(key);
|
|
1065
|
+
this.failureCounts.delete(key);
|
|
1066
|
+
await this.store.save(storage);
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
if (kind === "rate-limit") {
|
|
1070
|
+
if (!account.fingerprint) {
|
|
1071
|
+
account.fingerprint = generateFingerprint(void 0, await resolveAntigravityVersionBounded());
|
|
1072
|
+
account.fingerprintHistory = recordFingerprintVersion(account.fingerprintHistory, account.fingerprint, "initial");
|
|
1073
|
+
} else {
|
|
1074
|
+
const cached = peekCachedAntigravityVersion();
|
|
1075
|
+
if (cached) updateFingerprintVersion(account.fingerprint, cached);
|
|
1076
|
+
if (consecutive >= 2) {
|
|
1077
|
+
const fresh = generateFingerprint(void 0, await resolveAntigravityVersionBounded());
|
|
1078
|
+
account.fingerprintHistory = recordFingerprintVersion(account.fingerprintHistory, fresh, "regenerated");
|
|
1079
|
+
account.fingerprint = fresh;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (decision.action === "rotate") {
|
|
1084
|
+
const nextIndex = pickNextAccountIndex(storage.accounts, session.index);
|
|
1085
|
+
if (nextIndex !== session.index) {
|
|
1086
|
+
storage.activeIndex = nextIndex;
|
|
1087
|
+
this.onRotate?.(session.index, nextIndex, kind);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
await this.store.save(storage);
|
|
1091
|
+
}
|
|
1092
|
+
/** Adapter hook: reset the failure counter after a clean completion. */
|
|
1093
|
+
async markSuccess(session) {
|
|
1094
|
+
const account = session.account;
|
|
1095
|
+
const key = this.accountKey(account);
|
|
1096
|
+
this.failureCounts.delete(key);
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Test call: one short streaming request against the live backend.
|
|
1100
|
+
* Returns the collected text or a structured error message.
|
|
1101
|
+
*/
|
|
1102
|
+
async testCall(model, prompt = "Reply with exactly: OK", maxTokens = 1024) {
|
|
1103
|
+
const session = await this.getSession();
|
|
1104
|
+
if (!session) return {
|
|
1105
|
+
ok: false,
|
|
1106
|
+
error: "No agy account configured — run `dsh-agy login` first."
|
|
1107
|
+
};
|
|
1108
|
+
try {
|
|
1109
|
+
const { toAgyRequestBody } = await import("./translate-h2bjH7ub.mjs").then((n) => n.n);
|
|
1110
|
+
const { fetchAgyFirstOk } = await Promise.resolve().then(() => constants_exports);
|
|
1111
|
+
const { parseAgySse } = await import("./parse-CiEzD9d_.mjs").then((n) => n.n);
|
|
1112
|
+
const body = toAgyRequestBody({
|
|
1113
|
+
provider: "agy",
|
|
1114
|
+
model,
|
|
1115
|
+
messages: [{
|
|
1116
|
+
id: "test-1",
|
|
1117
|
+
role: "user",
|
|
1118
|
+
content: [{
|
|
1119
|
+
type: "text",
|
|
1120
|
+
text: prompt
|
|
1121
|
+
}]
|
|
1122
|
+
}],
|
|
1123
|
+
maxTokens
|
|
1124
|
+
}, {
|
|
1125
|
+
projectId: session.account.projectId,
|
|
1126
|
+
sessionId: deriveAntigravitySessionId(session.account.email) ?? void 0
|
|
1127
|
+
});
|
|
1128
|
+
const response = await fetchAgyFirstOk("/v1internal:streamGenerateContent?alt=sse", {
|
|
1129
|
+
method: "POST",
|
|
1130
|
+
headers: {
|
|
1131
|
+
authorization: `Bearer ${session.auth.access}`,
|
|
1132
|
+
"content-type": "application/json",
|
|
1133
|
+
accept: "text/event-stream",
|
|
1134
|
+
...session.impersonation
|
|
1135
|
+
},
|
|
1136
|
+
body: JSON.stringify(body)
|
|
1137
|
+
});
|
|
1138
|
+
if (!response.ok) {
|
|
1139
|
+
const text = await response.text().catch(() => "");
|
|
1140
|
+
return {
|
|
1141
|
+
ok: false,
|
|
1142
|
+
error: `HTTP ${response.status}: ${text.slice(0, 300)}`
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
if (!response.body) return {
|
|
1146
|
+
ok: false,
|
|
1147
|
+
error: "no response body"
|
|
1148
|
+
};
|
|
1149
|
+
const text = [];
|
|
1150
|
+
for await (const chunk of parseAgySse(response.body)) if (chunk.type === "text-delta") text.push(chunk.text);
|
|
1151
|
+
return {
|
|
1152
|
+
ok: text.length > 0,
|
|
1153
|
+
text: text.join(""),
|
|
1154
|
+
error: text.length > 0 ? void 0 : "empty response"
|
|
1155
|
+
};
|
|
1156
|
+
} catch (error) {
|
|
1157
|
+
return {
|
|
1158
|
+
ok: false,
|
|
1159
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
/** Export one account as a paste-credential blob (for migration to another host). */
|
|
1164
|
+
async exportBlob(index) {
|
|
1165
|
+
const account = (await this.store.load()).accounts[index];
|
|
1166
|
+
if (!account) return { error: "account not found" };
|
|
1167
|
+
const auth = await this.accessTokenFor(account);
|
|
1168
|
+
if (!auth) return { error: "refresh failed (revoked?)" };
|
|
1169
|
+
const { encodeCredentialBlob } = await import("./blob-D1e7_uT1.mjs").then((n) => n.t);
|
|
1170
|
+
const parts = parseRefreshParts(account.refresh);
|
|
1171
|
+
return { blob: encodeCredentialBlob("agy", {
|
|
1172
|
+
access_token: auth.access,
|
|
1173
|
+
refresh_token: parts.refreshToken,
|
|
1174
|
+
expires_in: Math.max(0, Math.round((auth.expires - Date.now()) / 1e3))
|
|
1175
|
+
}) };
|
|
1176
|
+
}
|
|
1177
|
+
/** CLI/web helper: verify an account's credentials (refresh + userinfo). */
|
|
1178
|
+
async verifyAccount(index) {
|
|
1179
|
+
const account = (await this.store.load()).accounts[index];
|
|
1180
|
+
if (!account) return {
|
|
1181
|
+
ok: false,
|
|
1182
|
+
error: "account not found"
|
|
1183
|
+
};
|
|
1184
|
+
const auth = await this.accessTokenFor(account);
|
|
1185
|
+
if (!auth) return {
|
|
1186
|
+
ok: false,
|
|
1187
|
+
error: "refresh failed (revoked?)"
|
|
1188
|
+
};
|
|
1189
|
+
try {
|
|
1190
|
+
const response = await fetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", { headers: { Authorization: `Bearer ${auth.access}` } });
|
|
1191
|
+
if (!response.ok) return {
|
|
1192
|
+
ok: false,
|
|
1193
|
+
error: `userinfo ${response.status}`
|
|
1194
|
+
};
|
|
1195
|
+
const info = await response.json();
|
|
1196
|
+
await this.store.mutate((s) => {
|
|
1197
|
+
const target = s.accounts[index];
|
|
1198
|
+
if (target) {
|
|
1199
|
+
target.enabled = true;
|
|
1200
|
+
target.verificationRequired = false;
|
|
1201
|
+
target.verificationRequiredAt = void 0;
|
|
1202
|
+
target.verificationRequiredReason = void 0;
|
|
1203
|
+
target.verificationUrl = void 0;
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
return {
|
|
1207
|
+
ok: true,
|
|
1208
|
+
email: info.email
|
|
1209
|
+
};
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
return {
|
|
1212
|
+
ok: false,
|
|
1213
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
//#endregion
|
|
1219
|
+
export { OAUTH_TOKEN_URL as C, getAgyBootstrapUserAgent as D, getAgyBootstrapClientMetadata as E, OAUTH_AUTHORIZE_URL as S, fetchAgyFirstOk as T, AGY_CLIENT_ID as _, MASTER_KEY_REF as a, AGY_ENDPOINT_FALLBACKS as b, loadMasterKey as c, resolveMasterKeyCodec as d, calculateTokenExpiry as f, generateAntigravityRequestId as g, deriveAntigravitySessionId as h, JsonAccountStore as i, persistMasterKey as l, resolveAntigravityVersionBounded as m, generateFingerprint as n, createAesGcmCodec as o, resolveAntigravityVersion as p, recordFingerprintVersion as r, deriveKey as s, AgySessionManager as t, resolveDshHome as u, AGY_CLIENT_SECRET as v, OAUTH_USERINFO_URL as w, AGY_SCOPES as x, AGY_DEFAULT_REDIRECT_URI as y };
|
|
1220
|
+
|
|
1221
|
+
//# sourceMappingURL=session-tawi2_uh.mjs.map
|