feat: append cli_context param to altimate auth URL for PostHog session correlation - #1068
feat: append cli_context param to altimate auth URL for PostHog session correlation#1068altimate-harness-bot[bot] wants to merge 4 commits into
Conversation
|
This PR doesn't fully meet our contributing guidelines and PR template. What needs to be fixed:
Please edit this PR description to address the above within 2 hours, or it will be automatically closed. If you believe this was flagged incorrectly, please let a maintainer know. |
…rrelation
- Append base64url-encoded cli_context param to the register URL opened
by AltimateAuthPlugin. Context blob: { v, machine_id, cli_version }.
- machine_id is the existing stable UUID from ~/.altimate/machine-id
(already in every App Insights event). If the file is missing, log a
debug message instead of silently omitting.
- Export buildCliContext() and add 3 unit tests covering: valid context,
missing machine-id file, and whitespace trimming.
33de593 to
b8c72c5
Compare
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.
Tip: disable this comment in your organization's Code Review settings.
sahrizvi
left a comment
There was a problem hiding this comment.
Review summary
Verdict: request changes — 0 critical, 4 major, 6 minor, 3 nits.
The mechanics here are right: base64url is the correct codec (unpadded, no +//, so no escaping surprises), the payload is version-tagged with v: 1 before anyone needs it, and a failed read never blocks sign-in. Exporting buildCliContext with an injectable path also makes it testable against the real filesystem instead of a mocked fs.
Two things block merge. First, the change transmits a persistent device identifier in a way the product's own published privacy documentation says it will not. Second, the feature can silently fail to do the one thing it exists to do, because it reads a file that only the telemetry module knows how to create.
Detailed findings are inline. Below is what has no single line to attach to.
Documentation changes needed (files not in this diff)
docs/docs/reference/telemetry.md:150anddocs/docs/reference/security-faq.md:133state, without qualification, that "Both identifiers are only sent when telemetry is enabled." This PR sends the machine id on the auth URL regardless of the telemetry setting. Either gate the parameter on the opt-out or correct the promise — the current combination is a written commitment the code does not keep.docs/docs/reference/telemetry.md:147anddocs/docs/reference/security-faq.md:132describe the machine id as serving "only to distinguish one machine from another in aggregate analytics" and "NOT tied to ... identity". Linking the CLI device to an authenticated user is worth disclosing there.
Cross-repo contract to confirm on the companion frontend PR
- Decode as base64url, not standard base64.
- Require
v === 1; reject unknown versions rather than best-effort parsing. - Cap decoded length and catch base64/JSON parse failures — this param is attacker-suppliable.
- Treat
cli_version: "local"as a legitimate development value, not a release version. - Decide explicitly what an empty or absent
machine_idmeans. If it means "do not alias", enforce that — aliasing on an empty value would merge unrelated anonymous sessions into a single identity.
Nits (non-blocking)
- Sync I/O on the interactive auth path —
readFileSyncinside an asyncauthorize(). The file is tiny andtelemetry/index.tsalready reads it synchronously, so this matches existing habit; worth changing only if the shared machine-id helper ends up async. - Manual URL concatenation — the browser-OAuth plugins in this repo build authorize URLs with
URLSearchParams(src/plugin/xai.ts,codex.ts,digitalocean.ts,snowflake-cortex.ts). The manual style predates this PR, which adds a second hand-escaped param to it. Follow-up cleanup, not a blocker. - Weak version assertion —
expect(typeof ctx["cli_version"]).toBe("string")does catch the key being dropped or renamed, so it is not vacuous, but it never checks the value.expect(ctx["cli_version"]).toBe(InstallationVersion)is strictly stronger. (InstallationVersionistypeof-guarded atpackages/core/src/installation/version.ts:7and can never be a non-string.)
Considered and not raised
- The optional
machineIdPath?parameter is a legitimate testability seam, not a smell — a test-only env var would be worse, since it makes test behaviour reachable in production builds. - The oversized-file concern is not a denial-of-service vector; the file sits in the user's own home directory. The real consequence is a broken auth URL, which is covered inline.
- The first-run window where telemetry has not yet written the machine id is real but narrow:
src/index.ts:126startsTelemetry.init()at CLI startup, long before a human clicks through sign-in. It is fire-and-forget and the write sits behind twoawaits, so the race exists — but the deterministic failure is the opt-out path, not this.
Verified locally: bun test test/altimate/altimate-plugin.test.ts -> 3 pass, 7 expect calls.
| export function buildCliContext(machineIdPath?: string): string { | ||
| const idPath = machineIdPath ?? path.join(os.homedir(), ".altimate", "machine-id") | ||
| let machineId = "" | ||
| try { | ||
| machineId = fs.readFileSync(idPath, "utf8").trim() | ||
| } catch { | ||
| log.debug("machine-id file not found — cli_context will omit machine_id") | ||
| } | ||
| const ctx = { v: 1, machine_id: machineId, cli_version: InstallationVersion } | ||
| return Buffer.from(JSON.stringify(ctx)).toString("base64url") | ||
| } |
There was a problem hiding this comment.
MAJOR — Logic Error / Design: machine-id lifecycle is split; this reads, only telemetry creates
buildCliContext() reads ~/.altimate/machine-id but never creates it. The only writer is Telemetry.doInit() (telemetry/index.ts:1702-1722), which carries race-safe exclusive-create (flag: "wx") logic this code does not share.
When the file is absent, this yields machine_id: "" while telemetry goes on to mint and use a real UUID — so the browser session and the CLI device carry different identities, and the correlation this PR exists to provide silently fails.
The path path.join(os.homedir(), ".altimate", "machine-id") is now constructed independently in three places:
telemetry/index.ts:1702— read + createplugin/altimate.ts:61— read only (new here)cli/welcome.ts:46— existence check
Fix: extract one shared helper with the existing read-or-create wx semantics and use it at all three call sites.
Note this alone does not resolve the opt-out issue or the empty-value issue flagged separately — the helper also needs to carry the consent decision and return an optional, validated id.
| `&redirect=${encodeURIComponent(redirect)}` + | ||
| `&state=${state}` | ||
| `&state=${state}` + | ||
| `&cli_context=${encodeURIComponent(buildCliContext())}` |
There was a problem hiding this comment.
MAJOR — Security (privacy): the machine ID is sent even when telemetry is disabled, contradicting the published docs
Telemetry.doInit() returns early — before the machine-id block — when ALTIMATE_TELEMETRY_DISABLED=true or config.telemetry.disabled is set (telemetry/index.ts:1667-1679). buildCliContext() checks neither, so this parameter is appended unconditionally.
A user who opted out but has a machine-id file from an earlier run still transmits that stable device identifier on every sign-in, specifically for product analytics.
The shipped documentation promises the opposite without qualification:
docs/docs/reference/telemetry.md:150— "Both identifiers are only sent when telemetry is enabled."docs/docs/reference/security-faq.md:133— "Both identifiers are only sent when telemetry is enabled."
Fix: resolve the opt-out through the same config/env path telemetry uses, and omit machine_id entirely when the user has opted out.
(The mirror case — opted out with no pre-existing file, so machine_id is permanently "" — is reasonable behaviour, but it should be a deliberate documented decision rather than a side effect.)
| `&state=${state}` + | ||
| `&cli_context=${encodeURIComponent(buildCliContext())}` |
There was a problem hiding this comment.
MAJOR — Security: a persistent device identifier lives in a URL query parameter
Query params land in browser history, app.myaltimate.com access logs, any CDN/WAF in front of it, the clipboard when a user copies this URL for an SSH/tmux sign-in, and potentially a Referer header if /register loads third-party resources.
That is a durable copy of a device identifier scattered across systems that have no retention policy for it — unlike the telemetry pipeline, which does.
Fix (any of):
- Confirm the param is scrubbed from access logs and that
/registersets a restrictiveReferrer-Policy. - Move the payload to a URL fragment (
#cli_context=...) — never transmitted to the server, still readable by the page, which fits this use case exactly. - Stronger long-term: send a short-lived correlation token that maps to the device server-side, rather than the durable identifier itself.
| const idPath = machineIdPath ?? path.join(os.homedir(), ".altimate", "machine-id") | ||
| let machineId = "" | ||
| try { | ||
| machineId = fs.readFileSync(idPath, "utf8").trim() |
There was a problem hiding this comment.
MAJOR — Bug / Security: no size or format validation on contents copied into the URL
readFileSync(idPath, "utf8").trim() accepts whatever is on disk — a multi-megabyte file, a symlink to another file, binary data (silently producing U+FFFD), embedded newlines — and all of it is base64-encoded into the authorize URL.
- A corrupt or oversized file produces a URL past browser/proxy length limits (~2 KB on older stacks, 8 KB on many servers), turning a working sign-in into an opaque browser error. The read is fail-open for missing files but not for malformed ones.
- A symlink planted at
~/.altimate/machine-idcopies another file's contents into a URL sent to and logged by Altimate's servers. Not a privilege-boundary break — anyone who can write that path already controls the account — but a real exfiltration primitive that validation removes for free.
Fix: use lstat (not stat, which follows symlinks and would still accept a symlink to a regular file), reject anything oversized or not a regular file, then validate shape:
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
machineId = UUID_RE.test(raw) ? raw : ""The canonical writer at telemetry/index.ts:1713 uses randomUUID(), so a UUID check rejects nothing legitimate.
| // Build a base64url-encoded context blob so the frontend can correlate this | ||
| // browser auth session with CLI telemetry. Fields are minimal and non-PII: | ||
| // machine_id is a random UUID stored locally, never an email or real identity. |
There was a problem hiding this comment.
MINOR — Documentation: the comment does not disclose the CLI-to-account linkage
The raw value is indeed still a random UUID, so "never an email or real identity" is literally true. But the stated purpose of this change is to link that device to an authenticated user in analytics, and the user-facing docs describe the identifier as "purely random and serves only to distinguish one machine from another in aggregate analytics" (docs/docs/reference/telemetry.md:147) and "NOT tied to your hardware, OS, or identity" (docs/docs/reference/security-faq.md:132).
Fix: soften this comment and add the disclosure to both docs — that signing in associates the anonymous machine id with the account in analytics. This is an additive disclosure gap, distinct from the flat contradiction flagged on the URL line.
| } catch { | ||
| log.debug("machine-id file not found — cli_context will omit machine_id") | ||
| } | ||
| const ctx = { v: 1, machine_id: machineId, cli_version: InstallationVersion } |
There was a problem hiding this comment.
MINOR — Design: an empty machine_id is transmitted rather than omitted
Every failure path converges on machine_id: "", and the empty value is still sent. The telemetry module already handles this correctly — ...(machineId && { machine_id: machineId }) at telemetry/index.ts:1570 omits the key — so this diverges from the pattern it imitates.
The test comment at altimate-plugin.test.ts:34 claims the empty string lets the frontend distinguish "error reading" from "no key", but the key is never omitted in any path, so that distinction does not exist.
If the companion frontend aliases on the empty value, unrelated anonymous sessions merge into one identity. That consequence lives in the other repository and is unverified here — worth confirming on that side.
Fix:
const ctx: Record<string, unknown> = { v: 1, cli_version: InstallationVersion }
if (machineId) ctx["machine_id"] = machineIdand make "absent means do not alias" explicit in the frontend contract.
| } catch { | ||
| log.debug("machine-id file not found — cli_context will omit machine_id") | ||
| } |
There was a problem hiding this comment.
MINOR — Code Quality: the catch and its log describe a narrower failure than they handle
The bare catch swallows EACCES, EISDIR, ELOOP, ENOTDIR and I/O errors, but logs "machine-id file not found". It also says it "will omit machine_id" when it in fact sends "". Someone debugging a missing correlation caused by a permissions problem is actively misled.
Fix:
} catch (err) {
const code = (err as NodeJS.ErrnoException)?.code
if (code === "ENOENT") log.debug("machine-id not present for cli_context")
else log.warn("machine-id read failed", { code, path: idPath })
}Non-ENOENT codes indicate a real local problem and deserve more than debug.
| log.debug("machine-id file not found — cli_context will omit machine_id") | ||
| } | ||
| const ctx = { v: 1, machine_id: machineId, cli_version: InstallationVersion } | ||
| return Buffer.from(JSON.stringify(ctx)).toString("base64url") |
There was a problem hiding this comment.
MINOR — Documentation: the frontend contract is unspecified, and this payload is untrusted input on that side
Nothing records what the consumer must do: decode base64url (not standard base64), require v === 1, cap decoded size, catch base64/JSON parse failures, validate field types, and treat cli_version: "local" as a legitimate dev value rather than a release.
From the frontend's perspective a URL query param is attacker-suppliable — anyone can hand-craft one and load /register.
Fix: add JSDoc here stating the contract, and make sure the companion PR validates rather than trusts.
| import * as path from "path" | ||
| import { buildCliContext } from "../../src/altimate/plugin/altimate" | ||
|
|
||
| describe("buildCliContext", () => { |
There was a problem hiding this comment.
MINOR — Testing: the integration point is untested
All three tests exercise buildCliContext() in isolation. Nothing asserts that the authorize URL actually carries cli_context, that it decodes back, or that client / redirect / state survive alongside it.
Delete the &cli_context=... line in altimate.ts and this entire suite still passes — which is the definition of an untested feature.
Fix: extract a buildAuthorizeUrl() and assert on it via new URL() / URLSearchParams, then decode cli_context independently.
|
|
||
| describe("buildCliContext", () => { | ||
| test("returns a valid base64url-encoded JSON blob with machine_id", () => { | ||
| const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "altimate-ctx-")) |
There was a problem hiding this comment.
MINOR — Testing: no failure-mode coverage, and the repo's temp-dir fixture is bypassed
Untested paths, several of which carry the bugs flagged elsewhere in this review: telemetry opted out, permission-denied on an existing file, empty file (0 bytes), non-UUID or binary contents, oversized file, path-is-a-directory, and symlink.
The fixture value "test-uuid-1234" is also not a UUID, which quietly blesses arbitrary contents as acceptable input.
Separately, this hand-rolls fs.mkdtempSync + manual fs.rmSync cleanup while the repo ships a tmpdir() fixture with await using auto-cleanup at test/fixture/fixture.ts:147. The manual version leaks temp directories whenever a test throws mid-run.
- MAJOR 1: extract getOrCreateMachineId() helper that mints a UUID when absent (wx exclusive-create to handle races); buildCliContext now always resolves the same machine_id that telemetry would use, including creating the file on demand. - MAJOR 2: honour ALTIMATE_TELEMETRY_DISABLED=true — skip machine_id read/create entirely when opt-out env var is set, matching the guard in telemetry/index.ts. - MINOR 3: distinguish ENOENT from other errors in catch block (EACCES, EISDIR, etc.); log.warn with error code for non-ENOENT failures instead of a misleading "file not found" message. - MINOR 4: omit machine_id key entirely when empty (use Record<string,unknown> with conditional assignment) instead of sending machine_id:"", matching the telemetry module pattern. - MINOR 5: export buildAuthorizeUrl() helper; add URL-integration tests asserting cli_context is present in the authorize URL and decodes to a valid JSON blob. Deleting the cli_context line now causes test failures. - MINOR 6: update comment above buildCliContext() to accurately describe its purpose — PostHog session correlation via posthog.alias() — rather than the inaccurate "never an email or real identity" framing.
Review round 2 — fix commit
|
| # | Item | Status |
|---|---|---|
| 1 | Machine-id lifecycle split / path duplication | Partial — helper added, but telemetry and welcome still carry their own copies |
| 2 | Sent despite telemetry opt-out | Partial — env var honoured, config opt-out ignored; now also creates the id |
| 3 | No size/format validation | Not addressed |
| 4 | Device ID in URL query param | Not addressed |
| 5 | Privacy docs stale | Not addressed |
| 6 | Empty machine_id transmitted |
Resolved |
| 7 | Misleading catch/log | Partial — fixed in buildCliContext, reintroduced in the new helper (see minor 1) |
| 8 | Authorize URL untested | Resolved — though the harness has its own problem (see major 3) |
| 9 | Failure-mode coverage / tmpdir() fixture |
Partial |
| 10 | Frontend decode contract | Not addressed — a comment names posthog.alias, but no contract is specified |
Major
1. The config opt-out is ignored, and the fix now mints an identifier for users who used it
altimate.ts:93, :62-80; telemetry/index.ts:1667, :1676
buildCliContext guards on process.env.ALTIMATE_TELEMETRY_DISABLED !== "true" and nothing else. Telemetry.doInit() honours two independent opt-outs — the env var at telemetry/index.ts:1667 and userConfig.telemetry?.disabled at :1676 — and the docs present them as equally valid: "Disable telemetry entirely with ALTIMATE_TELEMETRY_DISABLED=true or the config option above" (docs/docs/reference/telemetry.md:150, config documented at :111).
The new part is the creation side effect. Previously this code only read the file, so a config-opted-out user with no prior machine id transmitted nothing. Now getOrCreateMachineId mints and persists one during sign-in, then sends it — the CLI creates a permanent tracking identifier for a user who used a documented opt-out.
Compounding it, the comment at :91 says this "matches the guard in telemetry/index.ts::doInit". It matches half of it. A partial opt-out that advertises itself as complete is worse than none, because it stops the next reader from checking.
Fix: resolve the full opt-out policy in one place and consult it here. Config.get() is async while buildCliContext is sync, so either make context construction async or pass a resolved telemetryEnabled flag in from the caller. Failing that, this function should go back to read-only and never create — leaving creation to telemetry, where consent is already resolved.
2. The "shared" helper is not shared, and a new comment claims it is
altimate.ts:61; telemetry/index.ts:1702-1722; cli/welcome.ts:46
Line 61 states the helper is "Used by both buildCliContext and the telemetry module's doInit()." The commit changed only altimate.ts and the test file. telemetry/index.ts:1702-1722 still holds its own inline read-or-create implementation, and welcome.ts:46 still builds the path independently.
The result is worse than the original finding: there are now two independent read-or-create implementations that must stay in sync, where before there was one creator and one reader — plus a comment asserting they are unified. Line 85's "written by the telemetry module" is stale for the same reason.
Fix: move the path and lifecycle into a neutral module (e.g. altimate/telemetry/machine-id.ts) imported by telemetry, auth, and welcome — a plugin importing from telemetry, or the reverse, is the wrong dependency direction. Keep "read existing" separate from "create" so callers do not inherit an unexpected persistence side effect. Until the migration happens, the comment should not claim it has.
3. The test suite writes a persistent machine-id into the runner's home directory
altimate-plugin.test.ts:156, :184; altimate.ts:115-122
Both buildAuthorizeUrl tests call the function with no path override. buildAuthorizeUrl calls buildCliContext() with no argument at :120, which reaches getOrCreateMachineId(undefined), defaults to os.homedir()/.altimate/machine-id at :63, and writes at :72-75.
Reproduced against a clean isolated HOME: the run created a 36-byte UUID file. So bun test on a developer laptop or a CI runner now mints an analytics identity that will later be reported as a real device. It also corrupts an existing signal — cli/welcome.ts:46-47 uses existsSync on that file as the "upgrade vs fresh install" proxy, so a machine that has only ever run the test suite is thereafter classified as an upgrade.
The tests look isolated but are not: both create a temp machine-id containing "url-test-uuid" at :149-151, and that file is never read. The comment at :153-155 admits the path is not plumbed through. Dead setup that disguises the problem.
There is already a pattern for this in the repo — test/altimate/telemetry/onboarding.test.ts:367-372 redirects HOME to a temp dir with a comment explaining this exact hazard.
Fix: give buildAuthorizeUrl an optional machineIdPath forwarded to buildCliContext, and/or redirect HOME the way onboarding.test.ts does.
4. No size, format, or symlink validation — unchanged
altimate.ts:65
fs.readFileSync(idPath, "utf8").trim() still accepts arbitrary contents: no lstat, no size cap, no UUID check. An oversized, multi-line, or symlinked file still ends up base64-encoded in the auth URL — breaking sign-in past URL length limits, and still usable to copy another file's text into a URL that gets logged server-side. (Invalid byte sequences are replaced by the utf8 decode rather than passed through verbatim, but the content is still unbounded and unvalidated.)
The case for validating is stronger now, not weaker: line 76 mints ids with randomUUID(), so writer and validator would agree by construction. The tests themselves feed "test-uuid-1234" and "expected-uuid", showing non-UUID content sails through.
Fix: lstat and reject non-regular files, cap size before reading, require the canonical UUID shape.
5. Persistent device identifier still travels in a URL query parameter
altimate.ts:115-120, :419
Unchanged. Base64url is an encoding, not confidentiality. The identifier still reaches browser history, access logs, CDN/WAF logs, the clipboard on SSH/tmux sign-in, and potentially a Referer header.
Fix: a short-lived opaque correlation nonce registered server-side, or delivery over the authenticated back-channel. A fragment would remove server-log exposure but not history or clipboard.
6. Privacy documentation is untouched and now inaccurate in a second way
docs/docs/reference/telemetry.md:150,154; security-faq.md:133; also telemetry.md:68,147, security-faq.md:132
Neither commit changed docs/. telemetry.md:150 and security-faq.md:133 still promise both identifiers "are only sent when telemetry is enabled", which major 1 shows is still false. Beyond the original finding, the docs name Azure Application Insights as the destination and state that no separate data store is maintained (:154), while the new comment at altimate.ts:86-87 describes the frontend aliasing the id into PostHog. Both the opt-out promise and the destination need correcting before this ships.
Minor
1. The new helper treats every write failure as a lost race
altimate.ts:76-79
try { fs.writeFileSync(idPath, candidate, { encoding: "utf8", flag: "wx" }); return candidate }
catch { return fs.readFileSync(idPath, "utf8").trim() }The bare catch assumes EEXIST. On EACCES, EROFS or ENOSPC the re-read targets a file that was never created, throws ENOENT, and reaches buildCliContext's handler — which sees code === "ENOENT" and logs the benign "machine-id not present" at debug. So a real permissions or disk failure gets downgraded to the quietest log level. (Errors like EISDIR surface on the initial read at :65 and are correctly rethrown at :67, so the misreporting is limited to write-stage failures.) Auth still degrades gracefully by omitting the id.
Fix:
} catch (writeErr) {
if ((writeErr as NodeJS.ErrnoException)?.code !== "EEXIST") throw writeErr
return fs.readFileSync(idPath, "utf8").trim()
}2. The concurrency test does not test concurrency
altimate-plugin.test.ts:131-144
getOrCreateMachineId is synchronous, so both calls run to completion before Promise.all receives anything. The first creates the file; the second takes the plain read path. The wx branch at :77-79 is never entered — this test passes unchanged if flag: "wx" becomes a plain write, which is precisely the property it claims to guard.
Fix: force EEXIST with a stubbed writeFileSync, or spawn two real processes. If neither is worthwhile, drop the test rather than keep a guard that guards nothing.
3. Conditional assertion cannot prove what the test is for
altimate-plugin.test.ts:172-175
if (hasOwnProperty(ctx, "machine_id")) { ... } passes when the key is absent, so it cannot establish that machine_id was emitted. The sibling tests already save and restore ALTIMATE_TELEMETRY_DISABLED, so the expected state is controllable — assert it directly.
4. Remaining untested paths, and the repo fixture is still bypassed
altimate-plugin.test.ts
Still uncovered: config-based opt-out, non-UUID contents, oversized file, symlink, permission-denied on an existing file, path-is-a-directory, and the real wx race. Every test still hand-rolls mkdtempSync + rmSync, leaking temp directories on failure, instead of the tmpdir() fixture at test/fixture/fixture.ts:147.
5. Frontend decode contract still unspecified
altimate.ts:83-87
The comment naming posthog.alias(email, machine_id) is welcome honesty, but there is still no stated contract: base64url rather than standard base64, require v === 1, cap decoded size, reject malformed input, treat cli_version: "local" as a dev value, and treat an absent machine_id as "do not alias". From the frontend's side this parameter is attacker-suppliable.
Nits
buildAuthorizeUrlnow mutates the filesystem two calls deep. Abuild…name that implies no side effects is what makes major 3 easy to miss — resolving the id before URL construction and passing it in would fix both.altimate.ts:85— "written by the telemetry module" is stale now that this file writes it too.
Considered and not raised
stateis not URL-encoded — not an issue. It israndomBytes(16).toString("hex")ataltimate.ts:387; hex is URL-safe by construction, and this predates the PR.
What the fix got right
getOrCreateMachineIdfaithfully reproduces thewxexclusive-create pattern including the lost-race re-read, and correctly rethrows non-ENOENTread errors instead of swallowing them.- Omit-when-empty is exactly right and now matches
telemetry/index.ts:1570. buildCliContext's handler distinguishesENOENTfrom real failures and logs the code and path.buildAuthorizeUrlis a clean extraction — removing thecli_contextparameter would now fail a test.- The env-var opt-out test is well built: it plants a value that must not appear, asserts key absence rather than emptiness, and saves/restores the variable properly.
- The code comment now states the
posthog.aliaslinkage openly rather than describing the payload as simply non-PII.
Tests pass: 10 pass, 27 expect calls.
- Extract getOrCreateMachineId() to util/machine-id.ts with wx exclusive-create, UUID v4 regex validation, 512-byte size cap, and differentiated error logging - Update all 3 call sites (telemetry/index.ts, plugin/altimate.ts, cli/welcome.ts) to use the shared helper instead of inline copies - Add security tradeoff comment in buildCliContext explaining why cli_context stays as a query param (non-PII UUID, Referrer-Policy mitigation noted) - Update test values to valid RFC 4122 v4 UUIDs so UUID validation passes - Add failure mode tests: non-UUID content, oversized file, wrong UUID version - Update telemetry.md and security-faq.md with CLI auth flow disclosure
- honour config.telemetry.disabled (not just the env var) in buildCliContext by awaiting Config.get(), mirroring telemetry/index.ts::doInit - move cli_context into the URL fragment (#cli_context=) so the durable machine_id never reaches server access logs or the Referer header - reject symlinks / non-regular files via lstat in getOrCreateMachineId - fix welcome.ts fresh-install probe: use existsSync before minting so new users are no longer misclassified as upgrades - add failure-mode tests (empty file, directory, symlink); update tests for async buildCliContext/buildAuthorizeUrl and the fragment-based URL Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
👋 This PR was automatically closed by our quality checks. Common reasons:
If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you. |
Summary
Appends a
cli_contextquery param to the browser auth URL opened by the CLI during sign-in.buildCliContext()encodes{ v: 1, machine_id, cli_version }as a base64url JSON blobmachine_idis read from~/.altimate/machine-id(random UUID, non-PII) — the same value already sent to Azure App Insights telemetrycli_contextis decoded and passed toposthog.register()+posthog.alias()to link the CLI device to the authenticated userCompanion PR: AltimateAI/altimate-frontend#3106
Requested by @saravmajestic via harness
Summary by cubic
Adds a base64url-encoded
cli_contextto the CLI auth URL (now in the URL fragment) so the web app can link the browser session to the CLI device in PostHog. Uses a shared, race‑safemachine_idhelper with UUID v4 validation and a 512‑byte cap; honors both env and config telemetry opt‑out.Refactors
getOrCreateMachineId()toaltimate/util/machine-id.ts(exclusive-create, UUID v4 check, 512‑byte cap,lstatrejects symlinks/non-regular files; returns "" and logs on errors).telemetry,altimateplugin, andcli/welcometo use it;buildCliContext()respectsALTIMATE_TELEMETRY_DISABLED=trueandconfig.telemetry.disabled;cli_contextis appended viabuildAuthorizeUrl()in the URL fragment; added tests for URL shape and failure modes; updated telemetry/security docs.Bug Fixes
cli/welcome.tsby checking file existence before minting the machine ID.Written for commit 43fb343. Summary will update on new commits.