| author | |
| committer | |
| log | bf7121d7a25fe84d76561c3142b9433615a703a1 |
| tree | 5c90bf94789cddb59f200fa4a7455880f6fb95d8 |
| parent | b4d3d51ac99d4306d754afb5c78c49845bfac3c1 |
| signature | Signed by PGP key B5690EEEBB952194 |
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>2 files changed, 59 insertions(+), 4 deletions(-)
apps/server/src/telemetry/Identify.test.ts+43| ... | @@ -57,6 +57,49 @@ it.layer(NodeServices.layer)("telemetry identity", (it) => { | ... | @@ -57,6 +57,49 @@ it.layer(NodeServices.layer)("telemetry identity", (it) => { |
| 57 | ), | 57 | ), |
| 58 | ); | 58 | ); |
| 59 | 59 | ||
| 60 | it.effect("falls back quietly when Codex authenticates with an API key", () => { | ||
| 61 | const logs: CapturedLog[] = []; | ||
| 62 | const logger = makeCaptureLogger(logs); | ||
| 63 | |||
| 64 | return Effect.gen(function* () { | ||
| 65 | const config = yield* ServerConfig.ServerConfig; | ||
| 66 | const fileSystem = yield* FileSystem.FileSystem; | ||
| 67 | const path = yield* Path.Path; | ||
| 68 | const homeDirectory = path.join(config.baseDir, "home"); | ||
| 69 | const codexAuthPath = path.join(homeDirectory, ".codex", "auth.json"); | ||
| 70 | const anonymousId = "api-key-fallback-anonymous-id"; | ||
| 71 | const privateApiKey = "sk-private-openai-api-key"; | ||
| 72 | |||
| 73 | yield* fileSystem.makeDirectory(path.dirname(codexAuthPath), { recursive: true }); | ||
| 74 | yield* fileSystem.writeFileString( | ||
| 75 | codexAuthPath, | ||
| 76 | `{"auth_mode":"apikey","OPENAI_API_KEY":"${privateApiKey}"}`, | ||
| 77 | ); | ||
| 78 | yield* fileSystem.writeFileString(config.anonymousIdPath, anonymousId); | ||
| 79 | |||
| 80 | const identifier = yield* Identify.getTelemetryIdentifierForHome(homeDirectory); | ||
| 81 | |||
| 82 | assert.equal(identifier, sha256(anonymousId)); | ||
| 83 | assert.isUndefined(findIdentityLog(logs, "codex", "TelemetryIdentityDecodeError")); | ||
| 84 | assert.isUndefined(findIdentityLog(logs, "codex", "TelemetryIdentityReadError")); | ||
| 85 | const allLogs = logs | ||
| 86 | .map((log) => | ||
| 87 | [String(log.message), ...Object.values(log.annotations).map(String)].join("\n"), | ||
| 88 | ) | ||
| 89 | .join("\n"); | ||
| 90 | assert.notInclude(allLogs, privateApiKey); | ||
| 91 | }).pipe( | ||
| 92 | Effect.provide( | ||
| 93 | Layer.merge( | ||
| 94 | ServerConfig.layerTest(process.cwd(), { | ||
| 95 | prefix: "t3-telemetry-identify-apikey-", | ||
| 96 | }), | ||
| 97 | Logger.layer([logger], { mergeWithExisting: false }), | ||
| 98 | ), | ||
| 99 | ), | ||
| 100 | ); | ||
| 101 | }); | ||
| 102 | |||
| 60 | it.effect("logs structured decode context and falls back from malformed Codex auth", () => { | 103 | it.effect("logs structured decode context and falls back from malformed Codex auth", () => { |
| 61 | const logs: CapturedLog[] = []; | 104 | const logs: CapturedLog[] = []; |
| 62 | const logger = makeCaptureLogger(logs); | 105 | const logger = makeCaptureLogger(logs); |
apps/server/src/telemetry/Identify.ts+16-4| ... | @@ -10,10 +10,17 @@ import * as Schema from "effect/Schema"; | ... | @@ -10,10 +10,17 @@ import * as Schema from "effect/Schema"; |
| 10 | 10 | ||
| 11 | import * as ServerConfig from "../config.ts"; | 11 | import * as ServerConfig from "../config.ts"; |
| 12 | 12 | ||
| 13 | /** | ||
| 14 | * Codex omits `tokens` entirely when the install authenticates with an API key | ||
| 15 | * rather than a ChatGPT account, so an absent `tokens` is a supported install | ||
| 16 | * and not a malformed file. | ||
| 17 | */ | ||
| 13 | const CodexAuthJsonSchema = Schema.Struct({ | 18 | const CodexAuthJsonSchema = Schema.Struct({ |
| 14 | tokens: Schema.Struct({ | 19 | tokens: Schema.optional( |
| 15 | account_id: Schema.String, | 20 | Schema.Struct({ |
| 16 | }), | 21 | account_id: Schema.String, |
| 22 | }), | ||
| 23 | ), | ||
| 17 | }); | 24 | }); |
| 18 | 25 | ||
| 19 | const ClaudeJsonSchema = Schema.Struct({ | 26 | const ClaudeJsonSchema = Schema.Struct({ |
| ... | @@ -183,7 +190,9 @@ const getCodexAccountId = Effect.fn("TelemetryIdentity.getCodexAccountId")(funct | ... | @@ -183,7 +190,9 @@ const getCodexAccountId = Effect.fn("TelemetryIdentity.getCodexAccountId")(funct |
| 183 | ), | 190 | ), |
| 184 | ); | 191 | ); |
| 185 | 192 | ||
| 186 | return Option.some(authJson.tokens.account_id); | 193 | return authJson.tokens === undefined |
| 194 | ? Option.none<string>() | ||
| 195 | : Option.some(authJson.tokens.account_id); | ||
| 187 | }); | 196 | }); |
| 188 | 197 | ||
| 189 | const getClaudeUserId = Effect.fn("TelemetryIdentity.getClaudeUserId")(function* ( | 198 | const getClaudeUserId = Effect.fn("TelemetryIdentity.getClaudeUserId")(function* ( |
| ... | @@ -250,6 +259,9 @@ const upsertAnonymousId = Effect.gen(function* () { | ... | @@ -250,6 +259,9 @@ const upsertAnonymousId = Effect.gen(function* () { |
| 250 | * 1. ~/.codex/auth.json tokens.account_id | 259 | * 1. ~/.codex/auth.json tokens.account_id |
| 251 | * 2. ~/.claude.json userID | 260 | * 2. ~/.claude.json userID |
| 252 | * 3. ~/.t3/telemetry/anonymous-id | 261 | * 3. ~/.t3/telemetry/anonymous-id |
| 262 | * | ||
| 263 | * A missing file or an API-key-only Codex auth.json falls through quietly. Only | ||
| 264 | * unreadable or malformed files warn. | ||
| 253 | */ | 265 | */ |
| 254 | export const getTelemetryIdentifierForHome = Effect.fn("getTelemetryIdentifierForHome")( | 266 | export const getTelemetryIdentifierForHome = Effect.fn("getTelemetryIdentifierForHome")( |
| 255 | function* (homeDirectory: string) { | 267 | function* (homeDirectory: string) { |