Skip to content
Source
Sign inSign up

Identify.test.ts

271 lines10.4 KB

1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { assert, it } from "@effect/vitest";
4import * as Digest from "effect/crypto/Digest";
5import * as Effect from "effect/Effect";
6import * as Hex from "effect/encoding/Hex";
7import * as FileSystem from "effect/FileSystem";
8import * as Layer from "effect/Layer";
9import * as Logger from "effect/Logger";
10import * as Path from "effect/Path";
11import * as PlatformError from "effect/PlatformError";
12import * as References from "effect/References";
13​
14import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
15import { CLOUD_LINKED_USER_ID } from "../cloud/config.ts";
16import * as ServerConfig from "../config.ts";
17import * as Identify from "./Identify.ts";
18​
19interface CapturedLog {
20 readonly message: unknown;
21 readonly annotations: Readonly<Record<string, unknown>>;
23​
24const sha256 = Effect.fn("test.sha256")(function* (value: string) {
25 const digest = yield* Digest.Digest;
26 return Hex.encode(
27 yield* digest.digest(Digest.sha256, new TextEncoder().encode(value)).pipe(Effect.orDie),
28 );
29});
30​
31const makeCaptureLogger = (logs: CapturedLog[]) =>
32 Logger.make(({ fiber, message }) => {
33 logs.push({
34 message,
35 annotations: fiber.getRef(References.CurrentLogAnnotations),
36 });
37 });
38​
39const findIdentityLog = (
40 logs: ReadonlyArray<CapturedLog>,
41 source: Identify.TelemetryIdentitySource,
42 errorTag: string,
43) => logs.find((log) => log.annotations.source === source && log.annotations.errorTag === errorTag);
44​
45const unsignedJwt = (claims: Readonly<Record<string, unknown>>) =>
46 ["e30", Buffer.from(JSON.stringify(claims)).toString("base64url"), "signature"].join(".");
47​
48const layerIdentity = (prefix: string, logs?: CapturedLog[]) =>
49 Layer.provideMerge(
50 ServerSecretStore.layer,
51 ServerConfig.layerTest(process.cwd(), { prefix }),
52 ).pipe(
53 Layer.merge(
54 logs === undefined
55 ? Layer.empty
56 : Logger.layer([makeCaptureLogger(logs)], { mergeWithExisting: false }),
57 ),
58 );
59​
60const writeHomeFile = Effect.fn("test.writeHomeFile")(function* (
61 homeDirectory: string,
62 relativePath: string,
63 contents: string,
64) {
65 const fileSystem = yield* FileSystem.FileSystem;
66 const path = yield* Path.Path;
67 const filePath = path.join(homeDirectory, relativePath);
68 yield* fileSystem.makeDirectory(path.dirname(filePath), { recursive: true });
69 yield* fileSystem.writeFileString(filePath, contents);
70 return filePath;
71});
72​
73it.layer(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))("telemetry identity", (it) => {
74 it.effect("keeps the old distinct_id rule and a fixed install id as logins change", () =>
75 Effect.gen(function* () {
76 const config = yield* ServerConfig.ServerConfig;
77 const fileSystem = yield* FileSystem.FileSystem;
78 const path = yield* Path.Path;
79 const homeDirectory = path.join(config.baseDir, "home");
80 const anonymousId = "persisted-anonymous-id";
81 yield* fileSystem.writeFileString(config.anonymousIdPath, anonymousId);
82 const installId = yield* sha256(anonymousId);
83 const identity = () => Identify.getTelemetryIdentityForHome(homeDirectory);
84​
85 assert.deepEqual(yield* identity(), { distinctId: installId, installId });
86​
87 yield* writeHomeFile(homeDirectory, ".claude.json", `{"userID":"claude-machine"}`);
88 assert.deepEqual(yield* identity(), {
89 distinctId: yield* sha256("claude-machine"),
90 installId,
91 });
92​
93 yield* writeHomeFile(homeDirectory, ".codex/auth.json", `{"auth_mode":"apikey"}`);
94 assert.equal((yield* identity()).distinctId, yield* sha256("claude-machine"));
95​
96 yield* writeHomeFile(homeDirectory, ".codex/auth.json", `{"tokens":{"account_id":"ws-1"}}`);
97 assert.deepEqual(yield* identity(), { distinctId: yield* sha256("ws-1"), installId });
98 }).pipe(Effect.provide(layerIdentity("t3-telemetry-identify-install-"))),
99 );
100​
101 it.effect("leaves no torn anonymous id behind when its write fails midway", () =>
102 Effect.gen(function* () {
103 const config = yield* ServerConfig.ServerConfig;
104 const fileSystem = yield* FileSystem.FileSystem;
105 const path = yield* Path.Path;
106 const tornWrites = FileSystem.FileSystem.of({
107 ...fileSystem,
108 writeFileString: (filePath, data, options) =>
109 fileSystem
110 .writeFileString(filePath, data.slice(0, Math.floor(data.length / 2)), options)
111 .pipe(
112 Effect.andThen(
113 Effect.fail(
114 PlatformError.systemError({
115 _tag: "WriteZero",
116 module: "FileSystem",
117 method: "writeFileString",
118 pathOrDescriptor: filePath,
119 }),
120 ),
121 ),
122 ),
123 });
124​
125 const identity = yield* Identify.getTelemetryIdentityForHome(
126 path.join(config.baseDir, "home"),
127 ).pipe(Effect.provideService(FileSystem.FileSystem, tornWrites));
128​
129 assert.deepEqual(identity, { distinctId: null, installId: null });
130 assert.isFalse(yield* fileSystem.exists(config.anonymousIdPath));
131 }).pipe(Effect.provide(layerIdentity("t3-telemetry-identify-torn-"))),
132 );
133​
134 it.effect("does not overwrite the anonymous id path after a non-NotFound read failure", () => {
135 const logs: CapturedLog[] = [];
136​
137 return Effect.gen(function* () {
138 const config = yield* ServerConfig.ServerConfig;
139 const fileSystem = yield* FileSystem.FileSystem;
140 const path = yield* Path.Path;
141 yield* fileSystem.makeDirectory(config.anonymousIdPath);
142​
143 const identity = yield* Identify.getTelemetryIdentityForHome(
144 path.join(config.baseDir, "home"),
145 );
146 assert.deepEqual(identity, { distinctId: null, installId: null });
147 assert.deepEqual(yield* fileSystem.readDirectory(config.anonymousIdPath), []);
148​
149 const readLog = findIdentityLog(logs, "anonymous", "TelemetryIdentityReadError");
150 assert.equal(readLog?.annotations.filePath, config.anonymousIdPath);
151 assert.notEqual(readLog?.annotations.platformReason, "NotFound");
152 assert.notProperty(readLog?.annotations ?? {}, "cause");
153 assert.isUndefined(
154 findIdentityLog(logs, "anonymous", "TelemetryAnonymousIdPersistenceError"),
155 );
156 }).pipe(Effect.provide(layerIdentity("t3-telemetry-identify-read-", logs)));
157 });
158​
159 it.effect("reads the Connect account and each provider account as owners", () =>
160 Effect.gen(function* () {
161 const config = yield* ServerConfig.ServerConfig;
162 const secrets = yield* ServerSecretStore.ServerSecretStore;
163 const path = yield* Path.Path;
164 const homeDirectory = path.join(config.baseDir, "home");
165 yield* secrets.set(CLOUD_LINKED_USER_ID, new TextEncoder().encode("user_connect"));
166 yield* writeHomeFile(
167 homeDirectory,
168 ".codex/auth.json",
169 JSON.stringify({
170 tokens: {
171 account_id: "team-workspace",
172 id_token: unsignedJwt({
173 sub: "google-oauth2|login-method",
174 "https://api.openai.com/auth": { user_id: "user-openai-person" },
175 }),
176 },
177 }),
178 );
179 yield* writeHomeFile(
180 homeDirectory,
181 ".claude.json",
182 JSON.stringify({
183 userID: "claude-machine",
184 oauthAccount: { accountUuid: "claude-person" },
185 }),
186 );
187​
188 const owners = yield* Identify.getServerOwnersForHome(homeDirectory);
189​
190 assert.deepEqual(owners, [
191 { source: "connect", ownerId: yield* sha256("user_connect") },
192 { source: "openai", ownerId: yield* sha256("user-openai-person") },
193 { source: "claude", ownerId: yield* sha256("claude-person") },
194 ]);
195 }).pipe(Effect.provide(layerIdentity("t3-telemetry-identify-owners-"))),
196 );
197​
198 it.effect.each([
199 {
200 login: "an API key",
201 secret: "sk-private-openai-api-key",
202 authJson: (secret: string) => `{"auth_mode":"apikey","OPENAI_API_KEY":"${secret}"}`,
203 },
204 {
205 login: "an agent identity",
206 secret: "private-agent-identity-jwt",
207 authJson: (secret: string) =>
208 `{"auth_mode":"agentIdentity","OPENAI_API_KEY":null,"agent_identity":"${secret}"}`,
209 },
210 {
211 login: "a personal access token",
212 secret: "private-personal-access-token",
213 authJson: (secret: string) => `{"OPENAI_API_KEY":null,"personal_access_token":"${secret}"}`,
214 },
215 ])(
216 "adds no owner and logs nothing when Codex authenticates with $login",
217 ({ secret, authJson }) => {
218 const logs: CapturedLog[] = [];
219​
220 return Effect.gen(function* () {
221 const config = yield* ServerConfig.ServerConfig;
222 const path = yield* Path.Path;
223 const homeDirectory = path.join(config.baseDir, "home");
224 yield* writeHomeFile(homeDirectory, ".codex/auth.json", authJson(secret));
225​
226 assert.deepEqual(yield* Identify.getServerOwnersForHome(homeDirectory), []);
227 assert.deepEqual(logs, []);
228 }).pipe(Effect.provide(layerIdentity("t3-telemetry-identify-tokenless-", logs)));
229 },
230 );
231​
232 it.effect(
233 "warns about malformed Codex auth without its tokens and keeps the other owners",
234 () => {
235 const logs: CapturedLog[] = [];
236​
237 return Effect.gen(function* () {
238 const config = yield* ServerConfig.ServerConfig;
239 const path = yield* Path.Path;
240 const homeDirectory = path.join(config.baseDir, "home");
241 const privateAccessToken = "private-codex-access-token";
242 const codexAuthPath = yield* writeHomeFile(
243 homeDirectory,
244 ".codex/auth.json",
245 `{"tokens":{"access_token":"${privateAccessToken}"}}`,
246 );
247 yield* writeHomeFile(
248 homeDirectory,
249 ".claude.json",
250 `{"oauthAccount":{"accountUuid":"claude-person"}}`,
251 );
252​
253 assert.deepEqual(yield* Identify.getServerOwnersForHome(homeDirectory), [
254 { source: "claude", ownerId: yield* sha256("claude-person") },
255 ]);
256 const decodeLog = findIdentityLog(logs, "codex", "TelemetryIdentityDecodeError");
257 assert.equal(
258 decodeLog?.message,
259 `Failed to decode codex telemetry identity at '${codexAuthPath}'.`,
260 );
261 assert.equal(decodeLog?.annotations.causeKind, "schema");
262 assert.notInclude(
263 Object.values(decodeLog?.annotations ?? {})
264 .map(String)
265 .join("\n"),
266 privateAccessToken,
267 );
268 }).pipe(Effect.provide(layerIdentity("t3-telemetry-identify-decode-", logs)));
269 },
270 );
271});