import { Effect, FileSystem, Path, Random, Schema } from "effect"; import * as Crypto from "node:crypto"; import { homedir } from "node:os"; import { ServerConfig } from "../config.ts"; const CodexAuthJsonSchema = Schema.Struct({ tokens: Schema.Struct({ account_id: Schema.String, }), }); const ClaudeJsonSchema = Schema.Struct({ userID: Schema.String, }); class IdentifyUserError extends Schema.TaggedErrorClass()("IdentifyUserError", { message: Schema.String, cause: Schema.optional(Schema.Defect), }) {} const hash = (value: string) => Effect.try({ try: () => Crypto.createHash("sha256").update(value).digest("hex"), catch: (error) => new IdentifyUserError({ message: "Failed to hash identifier", cause: error, }), }); const getCodexAccountId = Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const authJsonPath = path.join(homedir(), ".codex", "auth.json"); const authJson = yield* Effect.flatMap( fileSystem.readFileString(authJsonPath), Schema.decodeEffect(Schema.fromJsonString(CodexAuthJsonSchema)), ); return authJson.tokens.account_id; }); const getClaudeUserId = Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const claudeJsonPath = path.join(homedir(), ".claude.json"); const claudeJson = yield* Effect.flatMap( fileSystem.readFileString(claudeJsonPath), Schema.decodeEffect(Schema.fromJsonString(ClaudeJsonSchema)), ); return claudeJson.userID; }); const upsertAnonymousId = Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const { anonymousIdPath } = yield* ServerConfig; const anonymousId = yield* fileSystem.readFileString(anonymousIdPath).pipe( Effect.catch(() => Effect.gen(function* () { const randomId = yield* Random.nextUUIDv4; yield* fileSystem.writeFileString(anonymousIdPath, randomId); return randomId; }), ), ); return anonymousId; }); /** * getTelemetryIdentifier - Users are "identified" by finding the first match of the following, then hashing the value. * 1. ~/.codex/auth.json tokens.account_id * 2. ~/.claude.json userID * 3. ~/.t3/telemetry/anonymous-id */ export const getTelemetryIdentifier = Effect.gen(function* () { const codexAccountId = yield* Effect.result(getCodexAccountId); if (codexAccountId._tag === "Success") { return yield* hash(codexAccountId.success); } const claudeUserId = yield* Effect.result(getClaudeUserId); if (claudeUserId._tag === "Success") { return yield* hash(claudeUserId.success); } const anonymousId = yield* Effect.result(upsertAnonymousId); if (anonymousId._tag === "Success") { return yield* hash(anonymousId.success); } return null; }).pipe( Effect.tapError((error) => Effect.logWarning("Failed to get identifier", { cause: error })), Effect.orElseSucceed(() => null), );