113 lines · 3.4 KB · TypeScript
RawDownload
import * as NodeOS from "node:os";import * as Crypto from "effect/Crypto";import * as Effect from "effect/Effect";import * as Encoding from "effect/Encoding";import * as FileSystem from "effect/FileSystem";import * as Path from "effect/Path";import * as Schema from "effect/Schema"; import * as 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>()("IdentifyUserError", {  operation: Schema.Literal("hash_identifier"),  algorithm: Schema.Literal("SHA-256"),  cause: Schema.Defect(),}) {  override get message(): string {    return `Failed to hash telemetry identifier with ${this.algorithm}.`;  }} const hash = (value: string) =>  Crypto.Crypto.pipe(    Effect.flatMap((crypto) => crypto.digest("SHA-256", new TextEncoder().encode(value))),    Effect.map(Encoding.encodeHex),    Effect.mapError(      (cause) =>        new IdentifyUserError({          operation: "hash_identifier",          algorithm: "SHA-256",          cause,        }),    ),  ); const getCodexAccountId = Effect.gen(function* () {  const fileSystem = yield* FileSystem.FileSystem;  const path = yield* Path.Path;   const authJsonPath = path.join(NodeOS.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(NodeOS.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.ServerConfig;   const anonymousId = yield* fileSystem.readFileString(anonymousIdPath).pipe(    Effect.catch(() =>      Crypto.Crypto.pipe(        Effect.flatMap((crypto) => crypto.randomUUIDv4),        Effect.tap((randomId) => fileSystem.writeFileString(anonymousIdPath, 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),);