Skip to content

Commit 8f95bed

Browse files
authored
feat: refresh CLI version on account sessions without re-login (#458)
Store session.cliVersion via Better Auth additional fields and keep it fresh after device login so /account/profile can show the installed package version when the CLI is upgraded.
1 parent 2a2d2e4 commit 8f95bed

17 files changed

Lines changed: 324 additions & 26 deletions

.changeset/session-cli-version.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@buildinternet/uploads": patch
3+
---
4+
5+
Device login now saves a session token and keeps `session.cliVersion` fresh so
6+
the account Sessions list can show your current CLI version after upgrades
7+
without re-login.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- CLI package version on the session row (schema: session.cliVersion).
2+
-- Written by Better Auth session.additionalFields + CLI POST /update-session
3+
-- after device login / on throttled CLI heartbeats. Displayed on
4+
-- /account/profile sessions without re-parsing user_agent.
5+
6+
ALTER TABLE session ADD COLUMN cli_version TEXT;

apps/auth/src/auth.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,11 @@ function buildAuth(
618618
// We only use it for account UX, not high-sensitivity actions — disable.
619619
freshAge: 0,
620620
cookieCache: { enabled: true, maxAge: 5 * 60 },
621+
// CLI package version — set/refreshed via POST /update-session (not core
622+
// userAgent, which Better Auth freezes after create). See account Sessions.
623+
additionalFields: {
624+
cliVersion: { type: "string", required: false },
625+
},
621626
},
622627
databaseHooks: {
623628
session: {

apps/auth/src/schema.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ export const user = sqliteTable("user", {
6464
* columns), `migrations/20260712210000_admin_plugin.sql` (`impersonated_by`,
6565
* written by the `admin` plugin's impersonation feature — Phase 2),
6666
* `migrations/20260712220000_organization.sql` (`active_organization_id`,
67-
* written by the `organization` plugin — Phase 3).
67+
* written by the `organization` plugin — Phase 3),
68+
* `migrations/20260723120000_session_cli_version.sql` (`cli_version`).
6869
*/
6970
export const session = sqliteTable(
7071
"session",
@@ -81,6 +82,8 @@ export const session = sqliteTable(
8182
updatedAt: timestampCol("updated_at"),
8283
impersonatedBy: text("impersonated_by"),
8384
activeOrganizationId: text("active_organization_id"),
85+
/** Installed `@buildinternet/uploads` version for CLI device sessions. */
86+
cliVersion: text("cli_version"),
8487
},
8588
(t) => [index("idx_session_user_id").on(t.userId)],
8689
);

apps/web/src/lib/auth-client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export interface AuthSession {
5050
expiresAt: string | Date;
5151
ipAddress?: string | null;
5252
userAgent?: string | null;
53+
/** Installed CLI package version (session.additionalFields.cliVersion). */
54+
cliVersion?: string | null;
5355
}
5456

5557
export interface SessionResponse {

apps/web/src/lib/session-device.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ describe("deviceLabel", () => {
2121
expect(deviceLabel("Mozilla/5.0 (Windows NT 10.0) Firefox/128.0")).toBe("Firefox on Windows");
2222
expect(deviceLabel(null)).toBe("Unknown device");
2323
});
24+
25+
it("prefers session.cliVersion over the create-time user-agent version", () => {
26+
expect(deviceLabel("@buildinternet/uploads/1.0.0", { cliVersion: "1.9.0" })).toBe(
27+
"uploads CLI 1.9.0",
28+
);
29+
expect(deviceLabel("@buildinternet/uploads", { cliVersion: "2.0.0" })).toBe(
30+
"uploads CLI 2.0.0",
31+
);
32+
// Additional field alone is enough (upgrade path after UA was versionless).
33+
expect(deviceLabel(null, { cliVersion: "1.2.3" })).toBe("uploads CLI 1.2.3");
34+
});
2435
});
2536

2637
describe("formatSessionTime", () => {

apps/web/src/lib/session-device.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,17 @@ export function isCliUserAgent(ua?: string | null): boolean {
2424
return Boolean(ua && CLI_USER_AGENT_RE.test(ua));
2525
}
2626

27-
/** "Chrome on macOS" / "uploads CLI 1.2.3" / "Unknown device". */
28-
export function deviceLabel(ua?: string | null): string {
29-
if (!ua) return "Unknown device";
30-
if (isCliUserAgent(ua)) {
31-
const version = ua.match(/@buildinternet\/uploads\/([\w.-]+)/i)?.[1];
27+
/**
28+
* "Chrome on macOS" / "uploads CLI 1.2.3" / "Unknown device".
29+
* Prefer session.cliVersion when present (refreshed after CLI upgrade).
30+
*/
31+
export function deviceLabel(ua?: string | null, opts?: { cliVersion?: string | null }): string {
32+
const cliVersion = opts?.cliVersion?.trim();
33+
if (cliVersion || isCliUserAgent(ua)) {
34+
const version = cliVersion || ua?.match(/@buildinternet\/uploads\/([\w.-]+)/i)?.[1];
3235
return version ? `uploads CLI ${version}` : "uploads CLI";
3336
}
37+
if (!ua) return "Unknown device";
3438
const browser = BROWSERS.find(([re]) => re.test(ua))?.[1] ?? "Browser";
3539
const os = OSES.find(([re]) => re.test(ua))?.[1] ?? "";
3640
return os ? `${browser} on ${os}` : browser;

apps/web/src/pages/account/profile.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ export const prerender = false;
276276
return `<li>
277277
<div class="detail-main">
278278
<div class="detail-title">
279-
<span>${escapeHtml(deviceLabel(s.userAgent))}</span>
279+
<span>${escapeHtml(deviceLabel(s.userAgent, { cliVersion: s.cliVersion }))}</span>
280280
${badge}
281281
</div>
282282
<div class="detail-meta">${meta}</div>

packages/uploads/src/cli.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { runReport } from "./commands/report.js";
4343
import { runScreenshot } from "./commands/screenshot.js";
4444
import { packageVersion } from "./package-version.js";
4545
import { checkForUpdate, maybeHintUpdate } from "./update-check.js";
46+
import { maybeSyncSessionCliVersion } from "./session-cli-version.js";
4647
import {
4748
errorCodeFromUnknown,
4849
maybeShowFirstRunNotice,
@@ -262,6 +263,11 @@ export async function runCli(argv: string[]): Promise<number> {
262263
const quiet = parsed.globals.quiet ?? false;
263264
apiUrl = resolveApiUrl(parsed.globals);
264265

266+
// Refresh session.cliVersion when the installed package changes (no-op without a session token).
267+
if (parsed.command && parsed.command !== "login" && parsed.command !== "logout") {
268+
maybeSyncSessionCliVersion({ apiUrl, envFile: parsed.globals.envFile });
269+
}
270+
265271
if (parsed.globals.version) {
266272
process.stdout.write(`${packageVersion()}\n`);
267273
return 0;

packages/uploads/src/commands/config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Keys:
3131
UPLOADS_API_URL API base URL (default: ${DEFAULT_API_URL})
3232
UPLOADS_WORKSPACE Workspace / bucket tenant (default: ${DEFAULT_WORKSPACE})
3333
UPLOADS_TOKEN Bearer token for the workspace
34+
UPLOADS_SESSION_TOKEN Device-flow session (CLI version on account sessions)
3435
UPLOADS_DEFAULT_PREFIX Default key prefix for put/list
3536
UPLOADS_DEFAULT_REPO Default repo segment for put
3637
UPLOADS_DEFAULT_REF Default ref segment for put
@@ -234,7 +235,11 @@ Examples:
234235
const path = flagString(parsed.flags, "--path") ?? resolveConfigPath({ envFile: opts.envFile });
235236
const force = flagBool(parsed.flags, "--force");
236237
const result = writeConfigKeys(path, { [key]: value }, { force });
237-
const payload = { ...result, key, value: key === "UPLOADS_TOKEN" ? redactToken(value) : value };
238+
const payload = {
239+
...result,
240+
key,
241+
value: key === "UPLOADS_TOKEN" || key === "UPLOADS_SESSION_TOKEN" ? redactToken(value) : value,
242+
};
238243

239244
if (opts.json) writeJson(payload);
240245
else process.stdout.write(`set ${key} in ${result.path}\n`);

0 commit comments

Comments
 (0)