-
Notifications
You must be signed in to change notification settings - Fork 0
Stamp anonymous X-Gusto-CLI-Install-Id header on outbound requests #153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,8 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { randomUUID } from "node:crypto"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { homedir } from "node:os"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import path from "node:path"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { parse, stringify } from "smol-toml"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { isTelemetryEnabled } from "./env.ts"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { Environment } from "./global-flags.ts"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { OutputMode } from "./output.ts"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -14,12 +16,18 @@ export interface UserConfig { | |||||||||||||||||||||||||||||||||||||||||||||||||
| environment?: Environment; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| format?: OutputMode; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| skills_auto_install?: SkillsAutoInstall; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Anonymous per-install UUID managed by getOrCreateInstallId; not user-configurable. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| install_id?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const ENV_VALUES: readonly Environment[] = ["sandbox", "production"] as const; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const FORMAT_VALUES: readonly OutputMode[] = ["agent", "human"] as const; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const SKILLS_AUTO_INSTALL_VALUES: readonly SkillsAutoInstall[] = ["ask", "always", "never"] as const; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Permissive UUID shape check — variant intentionally not pinned; we only care that on-disk | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // values look like real UUIDs so corruption is rejected. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const INSTALL_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // `json` is the advertised alias for `agent` (see the `--json` / `--agent` global flags). | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Accept it as a `format` value and persist it as `agent` so the config mirrors the flags. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const FORMAT_ALIASES: Readonly<Record<string, OutputMode>> = { json: "agent" } as const; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -55,17 +63,67 @@ export async function readConfig(paths: ConfigPaths = configPaths()): Promise<Us | |||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function writeConfig(cfg: UserConfig, paths: ConfigPaths = configPaths()): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const { mkdir, chmod } = await import("node:fs/promises"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const { mkdir, chmod, rename, rm } = await import("node:fs/promises"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| await mkdir(paths.dir, { recursive: true, mode: 0o700 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| await Bun.write(paths.file, stringify(stripUndefined(cfg))); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| await chmod(paths.file, 0o600); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Write to a uniquely-named temp file and rename into place: POSIX rename on the same | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // filesystem is atomic, so a concurrent reader can never observe a half-written file. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Suffix includes pid + a UUID so two concurrent writes in the same process (or across | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // processes with recycled PIDs) don't step on each other's temp file. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const tmp = `${paths.file}.${process.pid}.${randomUUID()}.tmp`; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| await Bun.write(tmp, stringify(stripUndefined(cfg))); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| await chmod(tmp, 0o600); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| await rename(tmp, paths.file); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+66
to
+76
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The commit message says this mirrors
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Best-effort tmp cleanup; don't shadow the real error. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| await rm(tmp, { force: true }).catch(() => {}); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| throw err; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function resetConfig(paths: ConfigPaths = configPaths()): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const { rm } = await import("node:fs/promises"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| await rm(paths.file, { force: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Return the persisted anonymous install_id, generating and persisting a UUIDv4 on first use. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * On a genuine first-run race two callers may each generate + write their own UUID: last-writer | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * wins on disk and every future caller converges. Not full first-writer-wins semantics (that | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * would need a lock file), but adequate — divergence is bounded to one command per racing caller | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * and self-heals on the next call. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function getOrCreateInstallId(paths: ConfigPaths = configPaths()): Promise<string> { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const cfg = await readConfig(paths); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (cfg.install_id) return cfg.install_id; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const install_id = randomUUID(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| await writeConfig({ ...cfg, install_id }, paths); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const settled = await readConfig(paths); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return settled.install_id ?? install_id; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** The install_id value to stamp on an outbound request, honoring GUSTO_TELEMETRY opt-out. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Returns undefined (suppresses the header) when telemetry is disabled or when the on-disk | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * config can't be read/written — telemetry is best-effort and must never fail the user's command. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Memoized per process when called with the default paths so the file is read at most once | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * per invocation regardless of how many outbound requests the command makes. Callers that pass | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * an explicit `paths` (tests) bypass the cache and resolve fresh each call. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let cachedDefaultInstallId: Promise<string | undefined> | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function resolveInstallIdHeader(paths?: ConfigPaths): Promise<string | undefined> { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (paths !== undefined) return resolveInstallIdOnce(paths); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (cachedDefaultInstallId === undefined) cachedDefaultInstallId = resolveInstallIdOnce(configPaths()); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return cachedDefaultInstallId; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| async function resolveInstallIdOnce(paths: ConfigPaths): Promise<string | undefined> { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isTelemetryEnabled()) return undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return await getOrCreateInstallId(paths); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export function validateKey(key: string): ConfigKey | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return (CONFIG_KEYS as readonly string[]).includes(key) ? (key as ConfigKey) : null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -113,6 +171,10 @@ function pickValid(raw: Record<string, unknown>): UserConfig { | |||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| out.skills_auto_install = raw.skills_auto_install as SkillsAutoInstall; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Drop corrupted values so getOrCreateInstallId regenerates on next call. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof raw.install_id === "string" && INSTALL_ID_PATTERN.test(raw.install_id)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| out.install_id = raw.install_id; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return out; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
/ionINSTALL_ID_PATTERNis the only thing letting an uppercase-hex UUID on disk survivepickValid, but nothing exercises it — every test here uses lowercase-valid or outright-garbage values. Drop the flag in a future refactor and CI stays green while hand-edited or migrated configs silently regenerate their id on every run.