Skip to content

Commit b8c72c5

Browse files
author
saravmajestic
committed
feat: [AI] add cli_context to browser auth URL for PostHog session correlation
- 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.
1 parent 8ae6c02 commit b8c72c5

2 files changed

Lines changed: 75 additions & 1 deletion

File tree

packages/opencode/src/altimate/plugin/altimate.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import open from "open"
55
import { AltimateApi } from "../api/client"
66
// altimate_change — onboarding telemetry for the gateway sign-in funnel
77
import * as OnboardingTelemetry from "../telemetry/onboarding"
8+
import fs from "fs"
9+
import os from "os"
10+
import path from "path"
11+
import { InstallationVersion } from "@opencode-ai/core/installation/version"
12+
import { Log } from "@/altimate/util/log"
813

914
/**
1015
* Why a failure reason is attached at the rejection site rather than inferred from the message:
@@ -47,6 +52,23 @@ const DEFAULT_WEB_URL = "https://app.myaltimate.com"
4752
// deliver.
4853
const DEFAULT_API_URL = "https://api.myaltimate.com"
4954

55+
const log = Log.create({ service: "altimate-plugin" })
56+
57+
// Build a base64url-encoded context blob so the frontend can correlate this
58+
// browser auth session with CLI telemetry. Fields are minimal and non-PII:
59+
// machine_id is a random UUID stored locally, never an email or real identity.
60+
export function buildCliContext(machineIdPath?: string): string {
61+
const idPath = machineIdPath ?? path.join(os.homedir(), ".altimate", "machine-id")
62+
let machineId = ""
63+
try {
64+
machineId = fs.readFileSync(idPath, "utf8").trim()
65+
} catch {
66+
log.debug("machine-id file not found — cli_context will omit machine_id")
67+
}
68+
const ctx = { v: 1, machine_id: machineId, cli_version: InstallationVersion }
69+
return Buffer.from(JSON.stringify(ctx)).toString("base64url")
70+
}
71+
5072
// The one-time login_token is POSTed to the callback-supplied API base, so that
5173
// base must be trusted — otherwise a crafted callback could exfiltrate the token
5274
// to an attacker's server. Allow only HTTPS Altimate-owned hosts, an explicitly
@@ -344,7 +366,8 @@ export async function AltimateAuthPlugin(_input: PluginInput): Promise<Hooks> {
344366
const authorizeUrl =
345367
`${webUrl}/register?client=altimate-code` +
346368
`&redirect=${encodeURIComponent(redirect)}` +
347-
`&state=${state}`
369+
`&state=${state}` +
370+
`&cli_context=${encodeURIComponent(buildCliContext())}`
348371

349372
// Try to open the browser. Failure is silent because the URL is
350373
// already surfaced elsewhere: the auth dialog in packages/tui/src/
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// altimate_change — tests for cli_context auth URL parameter
2+
import { describe, expect, test } from "bun:test"
3+
import * as fs from "fs"
4+
import * as os from "os"
5+
import * as path from "path"
6+
import { buildCliContext } from "../../src/altimate/plugin/altimate"
7+
8+
describe("buildCliContext", () => {
9+
test("returns a valid base64url-encoded JSON blob with machine_id", () => {
10+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "altimate-ctx-"))
11+
const idPath = path.join(tmpDir, "machine-id")
12+
fs.writeFileSync(idPath, "test-uuid-1234", "utf8")
13+
14+
const encoded = buildCliContext(idPath)
15+
16+
// base64url: only A-Z a-z 0-9 - _ (no +/=)
17+
expect(encoded).toMatch(/^[A-Za-z0-9\-_]+$/)
18+
19+
const ctx = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")) as Record<string, unknown>
20+
expect(ctx["v"]).toBe(1)
21+
expect(ctx["machine_id"]).toBe("test-uuid-1234")
22+
expect(typeof ctx["cli_version"]).toBe("string")
23+
24+
fs.rmSync(tmpDir, { recursive: true, force: true })
25+
})
26+
27+
test("omits machine_id value when file does not exist", () => {
28+
const nonExistentPath = path.join(os.tmpdir(), `altimate-no-such-${Date.now()}`, "machine-id")
29+
30+
const encoded = buildCliContext(nonExistentPath)
31+
const ctx = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")) as Record<string, unknown>
32+
33+
expect(ctx["v"]).toBe(1)
34+
// machine_id is empty string, not omitted — frontend can tell "error reading" from "no key"
35+
expect(ctx["machine_id"]).toBe("")
36+
})
37+
38+
test("trims whitespace from machine-id file", () => {
39+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "altimate-ctx-ws-"))
40+
const idPath = path.join(tmpDir, "machine-id")
41+
// Many editors/tools write a trailing newline
42+
fs.writeFileSync(idPath, " trimmed-uuid \n", "utf8")
43+
44+
const encoded = buildCliContext(idPath)
45+
const ctx = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")) as Record<string, unknown>
46+
47+
expect(ctx["machine_id"]).toBe("trimmed-uuid")
48+
49+
fs.rmSync(tmpDir, { recursive: true, force: true })
50+
})
51+
})

0 commit comments

Comments
 (0)