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>()("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),);
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>()("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),
);