| ... | @@ -30,6 +30,7 @@ const TokenResponse = Schema.Struct({ | ... | @@ -30,6 +30,7 @@ const TokenResponse = Schema.Struct({ |
| 30 | access_token: Schema.NonEmptyString, | 30 | access_token: Schema.NonEmptyString, |
| 31 | expires_in: Schema.Number.check(Schema.isGreaterThan(0)), | 31 | expires_in: Schema.Number.check(Schema.isGreaterThan(0)), |
| 32 | }); | 32 | }); |
| | 33 | const UserInfo = Schema.Struct({ id: Schema.NonEmptyString }); |
| 33 | const Quota = Schema.Struct({ | 34 | const Quota = Schema.Struct({ |
| 34 | remainingFraction: Schema.optional(Schema.Number), | 35 | remainingFraction: Schema.optional(Schema.Number), |
| 35 | resetTime: Schema.optional(Schema.String), | 36 | resetTime: Schema.optional(Schema.String), |
| ... | @@ -65,6 +66,7 @@ const Models = Schema.Struct({ | ... | @@ -65,6 +66,7 @@ const Models = Schema.Struct({ |
| 65 | | 66 | |
| 66 | const decodeTokenFile = Schema.decodeEffect(Schema.fromJsonString(TokenFile)); | 67 | const decodeTokenFile = Schema.decodeEffect(Schema.fromJsonString(TokenFile)); |
| 67 | const decodeTokenResponse = Schema.decodeUnknownEffect(TokenResponse); | 68 | const decodeTokenResponse = Schema.decodeUnknownEffect(TokenResponse); |
| | 69 | const decodeUserInfo = Schema.decodeUnknownEffect(UserInfo); |
| 68 | const decodeSummary = Schema.decodeUnknownEffect(Summary); | 70 | const decodeSummary = Schema.decodeUnknownEffect(Summary); |
| 69 | const decodeModels = Schema.decodeUnknownEffect(Models); | 71 | const decodeModels = Schema.decodeUnknownEffect(Models); |
| 70 | | 72 | |
| ... | @@ -168,9 +170,20 @@ export const makeAntigravityUsageProbe = Effect.fn("makeAntigravityUsageProbe")( | ... | @@ -168,9 +170,20 @@ export const makeAntigravityUsageProbe = Effect.fn("makeAntigravityUsageProbe")( |
| 168 | const platform = yield* HostProcess.Platform; | 170 | const platform = yield* HostProcess.Platform; |
| 169 | const architecture = yield* HostProcess.Architecture; | 171 | const architecture = yield* HostProcess.Architecture; |
| 170 | const tokenPath = path.join(input.profileDirectory, "antigravity-acp", "acp_token.json"); | 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 | let generation = 0; | 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 | const request = (request: HttpClientRequest.HttpClientRequest) => | 187 | const request = (request: HttpClientRequest.HttpClientRequest) => |
| 175 | client.execute(request).pipe( | 188 | client.execute(request).pipe( |
| 176 | Effect.provideService(FetchHttpClient.RequestInit, MANUAL_REDIRECT), | 189 | Effect.provideService(FetchHttpClient.RequestInit, MANUAL_REDIRECT), |
| ... | @@ -193,17 +206,12 @@ export const makeAntigravityUsageProbe = Effect.fn("makeAntigravityUsageProbe")( | ... | @@ -193,17 +206,12 @@ export const makeAntigravityUsageProbe = Effect.fn("makeAntigravityUsageProbe")( |
| 193 | }); | 206 | }); |
| 194 | } | 207 | } |
| 195 | const token = yield* fs.readFileString(tokenPath).pipe(Effect.flatMap(decodeTokenFile)); | 208 | const token = yield* fs.readFileString(tokenPath).pipe(Effect.flatMap(decodeTokenFile)); |
| 196 | const fingerprint = Array.from( | 209 | const fingerprint = yield* sha256( |
| 197 | yield* crypto.digest( | 210 | `${token.client_id}\0${token.refresh_token}\0${token.project_id ?? ""}`, |
| 198 | "SHA-256", | 211 | ); |
| 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(""); | | |
| 205 | const now = yield* Clock.currentTimeMillis; | 212 | const now = yield* Clock.currentTimeMillis; |
| 206 | let accessToken: string; | 213 | let accessToken: string; |
| | 214 | let account: string; |
| 207 | if (cached?.fingerprint !== fingerprint || cached.expiresAt <= now + 60_000) { | 215 | if (cached?.fingerprint !== fingerprint || cached.expiresAt <= now + 60_000) { |
| 208 | cached = undefined; | 216 | cached = undefined; |
| 209 | const refreshed = yield* request( | 217 | const refreshed = yield* request( |
| ... | @@ -217,11 +225,28 @@ export const makeAntigravityUsageProbe = Effect.fn("makeAntigravityUsageProbe")( | ... | @@ -217,11 +225,28 @@ export const makeAntigravityUsageProbe = Effect.fn("makeAntigravityUsageProbe")( |
| 217 | ), | 225 | ), |
| 218 | ).pipe(Effect.flatMap(decodeTokenResponse)); | 226 | ).pipe(Effect.flatMap(decodeTokenResponse)); |
| 219 | accessToken = refreshed.access_token; | 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 | if (generation === startedGeneration) { | 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 | } else { | 247 | } else { |
| 224 | accessToken = cached.accessToken; | 248 | accessToken = cached.accessToken; |
| | 249 | account = cached.account; |
| 225 | } | 250 | } |
| 226 | const fetchWindows = Effect.fn("AntigravityUsageProbe.fetchWindows")(function* <E>( | 251 | const fetchWindows = Effect.fn("AntigravityUsageProbe.fetchWindows")(function* <E>( |
| 227 | operation: string, | 252 | operation: string, |
| ... | @@ -259,7 +284,7 @@ export const makeAntigravityUsageProbe = Effect.fn("makeAntigravityUsageProbe")( | ... | @@ -259,7 +284,7 @@ export const makeAntigravityUsageProbe = Effect.fn("makeAntigravityUsageProbe")( |
| 259 | message: "Antigravity did not return usable quota information.", | 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 | const probe = read.pipe( | 289 | const probe = read.pipe( |
| 265 | Effect.timeout("30 seconds"), | 290 | Effect.timeout("30 seconds"), |