返回源码地图

packages/identity/anonymous-user-id/src/index.ts

main snapshot · da00f7f5358f · 正文引用章节 17;完整原文可核对,不声称全文件人工逐行审计

完整原文供逐行核对;页面收录不代表每行都经过人工语义审核。MIT 许可见 许可证。

1/**
2 * Per-harness-home anonymous user id shared by telemetry and feedback.
3 *
4 * The id is a random UUID persisted as a bare line in `.anonymous-user-id` inside the
5 * harness home resolved by {@link resolveDshHome} (`$DSH_HOME` > `~/.dsh`),
6 * and never derived from the hostname, network address, git remote, or any
7 * other identifying source. It is scoped to the harness home, not the
8 * machine: every process sharing one `$DSH_HOME` reports the same id, and
9 * deleting the file mints a fresh identity on the next launch.
10 *
11 * Reads and writes are synchronous so boot-time and command consumers can
12 * use one API. The result is memoized per resolved file path: one process
13 * touches the disk once, and a file deleted mid-run keeps the process's id
14 * until the next launch.
15 *
16 * @module @deepseek-ai/dsh-anonymous-user-id
17 */
18
19import { randomUUID } from 'node:crypto'
20import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
21import { dirname, join } from 'node:path'
22import type { Branded } from '@deepseek-ai/dsh-brand'
23import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
24
25/** A harness-home-scoped anonymous user id (random UUID v4). */
26export type AnonymousUserId = Branded<'AnonymousUserId'>
27
28/** File inside the harness home storing the id: a bare UUID line, no wrapper format. */
29export const ANONYMOUS_USER_ID_FILE_NAME = '.anonymous-user-id'
30
31const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
32
33/** Ambient hooks for locating and generating the id; every field has a default. */
34export interface AnonymousUserIdOptions {
35 /** Environment consulted for `DSH_HOME`; defaults to `process.env`. */
36 env?: NodeJS.ProcessEnv
37 /** UUID generator; defaults to `crypto.randomUUID` (test hook). */
38 randomUUID?: () => string
39}
40
41/** Process-lifetime memo keyed by resolved file path, so distinct test homes never share an id. */
42const memo = new Map<string, AnonymousUserId>()
43
44/** Read a valid persisted id from the file, or `undefined` when absent/corrupt. */
45function readPersistedId(file: string): AnonymousUserId | undefined {
46 let text: string
47 try {
48 text = readFileSync(file, 'utf8')
49 } catch {
50 // Absent or unreadable: the caller mints and persists a fresh id.
51 return undefined
52 }
53 const value = text.trim()
54 return UUID_PATTERN.test(value) ? (value as AnonymousUserId) : undefined
55}
56
57/**
58 * Return the harness home's anonymous user id, creating and persisting one on
59 * first use. A concurrent first launch is settled by an exclusive-create
60 * write: the loser rereads the winner's id. (A reread landing in the winner's
61 * narrow create-to-write window can still yield two per-process ids for that
62 * run; the next launch converges on the persisted one.) Persistence is
63 * best-effort — a write failure (read-only home) still returns a usable id
64 * for the current run so feedback and telemetry are never blocked.
65 * @param options - home-location and UUID-generation seams.
66 * @returns the stable per-harness-home anonymous user id.
67 */
68export function getOrCreateAnonymousUserId(options: AnonymousUserIdOptions = {}): AnonymousUserId {
69 const file = join(resolveDshHome(undefined, options.env ?? process.env), ANONYMOUS_USER_ID_FILE_NAME)
70 const cached = memo.get(file)
71 if (cached !== undefined) return cached
72
73 let id = readPersistedId(file)
74 if (id === undefined) {
75 const generate = options.randomUUID ?? randomUUID
76 const created = generate() as AnonymousUserId
77 try {
78 mkdirSync(dirname(file), { recursive: true })
79 writeFileSync(file, `${created}\n`, { encoding: 'utf8', flag: 'wx' })
80 id = created
81 } catch {
82 // A wx refusal (EEXIST) covers both a concurrent winner and a
83 // pre-existing corrupt file: the reread adopts a valid winner, and an
84 // invalid reread falls through to the overwrite path. Non-EEXIST
85 // failures (read-only home) land there too, accepted best-effort below.
86 id = readPersistedId(file)
87 if (id === undefined) {
88 try {
89 writeFileSync(file, `${created}\n`, 'utf8')
90 } catch {
91 // Best-effort persistence: keep the fresh id in memory even when the
92 // home is unwritable, so this run still reports a consistent id.
93 }
94 id = created
95 }
96 }
97 }
98 memo.set(file, id)
99 return id
100}