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 Option from "effect/Option";
import * as Path from "effect/Path";
import * as PlatformError from "effect/PlatformError";
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,
});
export const TelemetryIdentitySource = Schema.Literals(["codex", "claude", "anonymous"]);
export type TelemetryIdentitySource = typeof TelemetryIdentitySource.Type;
class TelemetryIdentityReadError extends Schema.TaggedError<TelemetryIdentityReadError>()(
"TelemetryIdentityReadError",
{
source: TelemetryIdentitySource,
filePath: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to read ${this.source} telemetry identity at '${this.filePath}'.`;
}
}
class TelemetryIdentityDecodeError extends Schema.TaggedError<TelemetryIdentityDecodeError>()(
"TelemetryIdentityDecodeError",
{
source: Schema.Literals(["codex", "claude"]),
filePath: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to decode ${this.source} telemetry identity at '${this.filePath}'.`;
}
}
export class TelemetryAnonymousIdGenerationError extends Schema.TaggedError<TelemetryAnonymousIdGenerationError>()(
"TelemetryAnonymousIdGenerationError",
{
source: Schema.Literal("anonymous"),
filePath: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to generate anonymous telemetry identity for '${this.filePath}'.`;
}
}
export class TelemetryAnonymousIdPersistenceError extends Schema.TaggedError<TelemetryAnonymousIdPersistenceError>()(
"TelemetryAnonymousIdPersistenceError",
{
source: Schema.Literal("anonymous"),
filePath: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to persist anonymous telemetry identity at '${this.filePath}'.`;
}
}
export class TelemetryIdentityHashError extends Schema.TaggedError<TelemetryIdentityHashError>()(
"TelemetryIdentityHashError",
{
source: TelemetryIdentitySource,
algorithm: Schema.Literal("SHA-256"),
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to hash ${this.source} telemetry identity with ${this.algorithm}.`;
}
}
type TelemetryIdentityError =
| TelemetryIdentityReadError
| TelemetryIdentityDecodeError
| TelemetryAnonymousIdGenerationError
| TelemetryAnonymousIdPersistenceError
| TelemetryIdentityHashError;
const decodeCodexAuthJson = Schema.decodeEffect(Schema.fromJsonString(CodexAuthJsonSchema));
const decodeClaudeJson = Schema.decodeEffect(Schema.fromJsonString(ClaudeJsonSchema));
function isNotFoundError(error: PlatformError.PlatformError): boolean {
return error.reason._tag === "NotFound";
}
const getTelemetryIdentityCauseAnnotations = (cause: unknown) => {
if (cause instanceof PlatformError.PlatformError) {
return {
causeKind: "platform",
platformReason: cause.reason._tag,
};
}
if (cause instanceof Schema.SchemaError) {
return { causeKind: "schema" };
}
return { causeKind: "other" };
};
const logTelemetryIdentityError = (error: TelemetryIdentityError) =>
Effect.logWarning(error.message).pipe(
Effect.annotateLogs({
errorTag: error._tag,
source: error.source,
...("filePath" in error ? { filePath: error.filePath } : {}),
...getTelemetryIdentityCauseAnnotations(error.cause),
...(error.stack === undefined ? {} : { errorStack: error.stack }),
}),
);
const readIdentityFile = (
fileSystem: FileSystem.FileSystem,
source: TelemetryIdentitySource,
filePath: string,
) =>
fileSystem.readFileString(filePath).pipe(
Effect.asSome,
Effect.catchTags({
PlatformError: (cause) =>
isNotFoundError(cause)
? Effect.succeed(Option.none<string>())
: Effect.fail(
new TelemetryIdentityReadError({
source,
filePath,
cause,
}),
),
}),
);
const hash = (source: TelemetryIdentitySource, value: string) =>
Crypto.Crypto.pipe(
Effect.flatMap((crypto) => crypto.digest("SHA-256", new TextEncoder().encode(value))),
Effect.map(Encoding.encodeHex),
Effect.mapError(
(cause) =>
new TelemetryIdentityHashError({
source,
algorithm: "SHA-256",
cause,
}),
),
);
const getCodexAccountId = Effect.fn("TelemetryIdentity.getCodexAccountId")(function* (
homeDirectory: string,
) {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const authJsonPath = path.join(homeDirectory, ".codex", "auth.json");
const encoded = yield* readIdentityFile(fileSystem, "codex", authJsonPath);
if (Option.isNone(encoded)) {
return Option.none<string>();
}
const authJson = yield* decodeCodexAuthJson(encoded.value).pipe(
Effect.mapError(
(cause) =>
new TelemetryIdentityDecodeError({
source: "codex",
filePath: authJsonPath,
cause,
}),
),
);
return Option.some(authJson.tokens.account_id);
});
const getClaudeUserId = Effect.fn("TelemetryIdentity.getClaudeUserId")(function* (
homeDirectory: string,
) {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const claudeJsonPath = path.join(homeDirectory, ".claude.json");
const encoded = yield* readIdentityFile(fileSystem, "claude", claudeJsonPath);
if (Option.isNone(encoded)) {
return Option.none<string>();
}
const claudeJson = yield* decodeClaudeJson(encoded.value).pipe(
Effect.mapError(
(cause) =>
new TelemetryIdentityDecodeError({
source: "claude",
filePath: claudeJsonPath,
cause,
}),
),
);
return Option.some(claudeJson.userID);
});
const upsertAnonymousId = Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const { anonymousIdPath } = yield* ServerConfig.ServerConfig;
const existing = yield* readIdentityFile(fileSystem, "anonymous", anonymousIdPath);
if (Option.isSome(existing)) {
return existing.value;
}
const anonymousId = yield* Crypto.Crypto.pipe(
Effect.flatMap((crypto) => crypto.randomUUIDv4),
Effect.mapError(
(cause) =>
new TelemetryAnonymousIdGenerationError({
source: "anonymous",
filePath: anonymousIdPath,
cause,
}),
),
);
yield* fileSystem.writeFileString(anonymousIdPath, anonymousId).pipe(
Effect.mapError(
(cause) =>
new TelemetryAnonymousIdPersistenceError({
source: "anonymous",
filePath: anonymousIdPath,
cause,
}),
),
);
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 getTelemetryIdentifierForHome = Effect.fn("getTelemetryIdentifierForHome")(
function* (homeDirectory: string) {
const codexAccountId = yield* getCodexAccountId(homeDirectory).pipe(
Effect.catchTags({
TelemetryIdentityReadError: (error) =>
logTelemetryIdentityError(error).pipe(Effect.as(Option.none<string>())),
TelemetryIdentityDecodeError: (error) =>
logTelemetryIdentityError(error).pipe(Effect.as(Option.none<string>())),
}),
);
if (Option.isSome(codexAccountId)) {
return yield* hash("codex", codexAccountId.value);
}
const claudeUserId = yield* getClaudeUserId(homeDirectory).pipe(
Effect.catchTags({
TelemetryIdentityReadError: (error) =>
logTelemetryIdentityError(error).pipe(Effect.as(Option.none<string>())),
TelemetryIdentityDecodeError: (error) =>
logTelemetryIdentityError(error).pipe(Effect.as(Option.none<string>())),
}),
);
if (Option.isSome(claudeUserId)) {
return yield* hash("claude", claudeUserId.value);
}
const anonymousId = yield* upsertAnonymousId.pipe(
Effect.asSome,
Effect.catchTags({
TelemetryIdentityReadError: (error) =>
logTelemetryIdentityError(error).pipe(Effect.as(Option.none<string>())),
TelemetryAnonymousIdGenerationError: (error) =>
logTelemetryIdentityError(error).pipe(Effect.as(Option.none<string>())),
TelemetryAnonymousIdPersistenceError: (error) =>
logTelemetryIdentityError(error).pipe(Effect.as(Option.none<string>())),
}),
);
if (Option.isSome(anonymousId)) {
return yield* hash("anonymous", anonymousId.value);
}
return null;
},
Effect.tapError(logTelemetryIdentityError),
Effect.orElseSucceed(() => null),
);
export const getTelemetryIdentifier = Effect.suspend(() =>
getTelemetryIdentifierForHome(NodeOS.homedir()),
);