| ... | ... | @@ -30,6 +30,7 @@ const TokenResponse = Schema.Struct({ |
| 30 | 30 | access_token: Schema.NonEmptyString, |
| 31 | 31 | expires_in: Schema.Number.check(Schema.isGreaterThan(0)), |
| 32 | 32 | }); |
| 33 | const UserInfo = Schema.Struct({ id: Schema.NonEmptyString }); |
| 33 | 34 | const Quota = Schema.Struct({ |
| 34 | 35 | remainingFraction: Schema.optional(Schema.Number), |
| 35 | 36 | resetTime: Schema.optional(Schema.String), |
| ... | ... | @@ -65,6 +66,7 @@ const Models = Schema.Struct({ |
| 65 | 66 | |
| 66 | 67 | const decodeTokenFile = Schema.decodeEffect(Schema.fromJsonString(TokenFile)); |
| 67 | 68 | const decodeTokenResponse = Schema.decodeUnknownEffect(TokenResponse); |
| 69 | const decodeUserInfo = Schema.decodeUnknownEffect(UserInfo); |
| 68 | 70 | const decodeSummary = Schema.decodeUnknownEffect(Summary); |
| 69 | 71 | const decodeModels = Schema.decodeUnknownEffect(Models); |
| 70 | 72 | |
| ... | ... | @@ -168,9 +170,20 @@ export const makeAntigravityUsageProbe = Effect.fn("makeAntigravityUsageProbe")( |
| 168 | 170 | const platform = yield* HostProcess.Platform; |
| 169 | 171 | const architecture = yield* HostProcess.Architecture; |
| 170 | 172 | const tokenPath = path.join(input.profileDirectory, "antigravity-acp", "acp_token.json"); |
| 171 | | let cached: { fingerprint: string; accessToken: string; expiresAt: number } | undefined; |
| 173 | let cached: |
| 174 | | { fingerprint: string; account: string; accessToken: string; expiresAt: number } |
| 175 | | undefined; |
| 172 | 176 | let generation = 0; |
| 173 | 177 | |
| 178 | const sha256 = (text: string) => |
| 179 | crypto |
| 180 | .digest("SHA-256", new TextEncoder().encode(text)) |
| 181 | .pipe( |
| 182 | Effect.map((digest) => |
| 183 | Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join(""), |
| 184 | ), |
| 185 | ); |
| 186 | |
| 174 | 187 | const request = (request: HttpClientRequest.HttpClientRequest) => |
| 175 | 188 | client.execute(request).pipe( |
| 176 | 189 | Effect.provideService(FetchHttpClient.RequestInit, MANUAL_REDIRECT), |
| ... | ... | @@ -193,17 +206,12 @@ export const makeAntigravityUsageProbe = Effect.fn("makeAntigravityUsageProbe")( |
| 193 | 206 | }); |
| 194 | 207 | } |
| 195 | 208 | const token = yield* fs.readFileString(tokenPath).pipe(Effect.flatMap(decodeTokenFile)); |
| 196 | | const fingerprint = Array.from( |
| 197 | | yield* crypto.digest( |
| 198 | | "SHA-256", |
| 199 | | new TextEncoder().encode( |
| 200 | | `${token.client_id}\0${token.refresh_token}\0${token.project_id ?? ""}`, |
| 201 | | ), |
| 202 | | ), |
| 203 | | (byte) => byte.toString(16).padStart(2, "0"), |
| 204 | | ).join(""); |
| 209 | const fingerprint = yield* sha256( |
| 210 | `${token.client_id}\0${token.refresh_token}\0${token.project_id ?? ""}`, |
| 211 | ); |
| 205 | 212 | const now = yield* Clock.currentTimeMillis; |
| 206 | 213 | let accessToken: string; |
| 214 | let account: string; |
| 207 | 215 | if (cached?.fingerprint !== fingerprint || cached.expiresAt <= now + 60_000) { |
| 208 | 216 | cached = undefined; |
| 209 | 217 | const refreshed = yield* request( |
| ... | ... | @@ -217,11 +225,28 @@ export const makeAntigravityUsageProbe = Effect.fn("makeAntigravityUsageProbe")( |
| 217 | 225 | ), |
| 218 | 226 | ).pipe(Effect.flatMap(decodeTokenResponse)); |
| 219 | 227 | accessToken = refreshed.access_token; |
| 228 | // Each machine's sign-in has its own refresh token, so the Google |
| 229 | // account id is what lets clients merge one account across environments. |
| 230 | account = yield* request( |
| 231 | HttpClientRequest.get("https://www.googleapis.com/oauth2/v2/userinfo").pipe( |
| 232 | HttpClientRequest.setHeaders({ Authorization: `Bearer ${accessToken}` }), |
| 233 | ), |
| 234 | ).pipe( |
| 235 | Effect.flatMap(decodeUserInfo), |
| 236 | Effect.flatMap((info) => sha256(`google-account\0${info.id}`)), |
| 237 | Effect.orElseSucceed(() => fingerprint), |
| 238 | ); |
| 220 | 239 | if (generation === startedGeneration) { |
| 221 | | cached = { fingerprint, accessToken, expiresAt: now + refreshed.expires_in * 1000 }; |
| 240 | cached = { |
| 241 | fingerprint, |
| 242 | account, |
| 243 | accessToken, |
| 244 | expiresAt: now + refreshed.expires_in * 1000, |
| 245 | }; |
| 222 | 246 | } |
| 223 | 247 | } else { |
| 224 | 248 | accessToken = cached.accessToken; |
| 249 | account = cached.account; |
| 225 | 250 | } |
| 226 | 251 | const fetchWindows = Effect.fn("AntigravityUsageProbe.fetchWindows")(function* <E>( |
| 227 | 252 | operation: string, |
| ... | ... | @@ -259,7 +284,7 @@ export const makeAntigravityUsageProbe = Effect.fn("makeAntigravityUsageProbe")( |
| 259 | 284 | message: "Antigravity did not return usable quota information.", |
| 260 | 285 | }); |
| 261 | 286 | } |
| 262 | | return { ...makeUsageLimits({ checkedAt, windows }), credentialFingerprint: fingerprint }; |
| 287 | return { ...makeUsageLimits({ checkedAt, windows }), credentialFingerprint: account }; |
| 263 | 288 | }); |
| 264 | 289 | const probe = read.pipe( |
| 265 | 290 | Effect.timeout("30 seconds"), |