Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions extensions/agent-usage/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Agent Usage Changelog
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 CHANGELOG not updated for this PR

The custom repo policy requires a new CHANGELOG entry for every pull request, using the {PR_MERGE_DATE} placeholder. The current top of the file still points to the previous release (2026-03-16).

Please prepend a new entry, for example:

Suggested change
# Agent Usage Changelog
## [Encrypted Auth v2 Support] - {PR_MERGE_DATE}
- Support Factory Droid's new `auth.v2.*` encrypted credential format (AES-256-GCM)
- Fall back to legacy `auth.json` / `auth.encrypted` when v2 files are absent
# Agent Usage Changelog

Rule Used: What: Ensure that CHANGELOG.md is created or updat... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/agent-usage/CHANGELOG.md
Line: 1

Comment:
**CHANGELOG not updated for this PR**

The custom repo policy requires a new CHANGELOG entry for every pull request, using the `{PR_MERGE_DATE}` placeholder. The current top of the file still points to the previous release (`2026-03-16`).

Please prepend a new entry, for example:

```suggestion
## [Encrypted Auth v2 Support] - {PR_MERGE_DATE}

- Support Factory Droid's new `auth.v2.*` encrypted credential format (AES-256-GCM)
- Fall back to legacy `auth.json` / `auth.encrypted` when v2 files are absent

# Agent Usage Changelog
```

**Rule Used:** What: Ensure that CHANGELOG.md is created or updat... ([source](https://app.greptile.com/review/custom-context?memory=97cd51bc-963b-43f5-acc3-9ba85fe7bb2d))

How can I resolve this? If you propose a fix, please make it concise.


## [Support Droid Encrypted Auth v2] - {PR_MERGE_DATE}

### Improvements

- Support Factory Droid's new encrypted auth v2 format with backward compatibility for legacy auth files

## [Progress Bars & Zero-Config Auth] - 2026-03-16

### New Features
Expand Down
74 changes: 72 additions & 2 deletions extensions/agent-usage/src/droid/auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import * as crypto from "crypto";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";

const AUTH_PATHS = [
const AUTH_V2_FILE = path.join(os.homedir(), ".factory", "auth.v2.file");
const AUTH_V2_KEY = path.join(os.homedir(), ".factory", "auth.v2.key");

const AUTH_LEGACY_PATHS = [
path.join(os.homedir(), ".factory", "auth.json"),
path.join(os.homedir(), ".factory", "auth.encrypted"),
];
Expand Down Expand Up @@ -89,6 +93,11 @@ async function refreshAccessToken(refreshToken: string): Promise<AuthTokens | nu

function saveAuthToFile(filePath: string, tokens: AuthTokens): void {
try {
if (filePath === AUTH_V2_FILE) {
saveAuthV2(tokens);
return;
}

let existing: Record<string, unknown> = {};
if (fs.existsSync(filePath)) {
const raw = fs.readFileSync(filePath, "utf-8");
Expand Down Expand Up @@ -116,8 +125,69 @@ function saveAuthToFile(filePath: string, tokens: AuthTokens): void {
}
}

function saveAuthV2(tokens: AuthTokens): void {
try {
if (!fs.existsSync(AUTH_V2_KEY)) return;

const keyB64 = fs.readFileSync(AUTH_V2_KEY, "utf-8").trim();
const key = Buffer.from(keyB64, "base64");
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);

const plaintext = JSON.stringify({
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
});

let encrypted = cipher.update(plaintext, "utf-8");
encrypted = Buffer.concat([encrypted, cipher.final()]);
const tag = cipher.getAuthTag();

const content = [iv.toString("base64"), tag.toString("base64"), encrypted.toString("base64")].join(":");
fs.writeFileSync(AUTH_V2_FILE, content, "utf-8");
} catch {
// best-effort
}
}

function tryParseAuthV2(): { tokens: AuthTokens; filePath: string } | null {
try {
if (!fs.existsSync(AUTH_V2_FILE) || !fs.existsSync(AUTH_V2_KEY)) return null;

const keyB64 = fs.readFileSync(AUTH_V2_KEY, "utf-8").trim();
const fileContent = fs.readFileSync(AUTH_V2_FILE, "utf-8").trim();

const parts = fileContent.split(":");
if (parts.length !== 3) return null;

const key = Buffer.from(keyB64, "base64");
const iv = Buffer.from(parts[0], "base64");
const tag = Buffer.from(parts[1], "base64");
const ciphertext = Buffer.from(parts[2], "base64");

const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
decipher.setAuthTag(tag);
let decrypted = decipher.update(ciphertext, undefined, "utf-8");
decrypted += decipher.final("utf-8");

const parsed = JSON.parse(decrypted) as Record<string, unknown>;
const accessToken = (parsed.access_token ?? parsed.accessToken) as string | undefined;
const refreshToken = (parsed.refresh_token ?? parsed.refreshToken) as string | undefined;

if (!accessToken) return null;
return { tokens: { access_token: accessToken, refresh_token: refreshToken ?? null }, filePath: AUTH_V2_FILE };
} catch {
return null;
}
}

function loadAuthFromFiles(): AuthState | null {
for (const filePath of AUTH_PATHS) {
// Try v2 encrypted format first
const v2 = tryParseAuthV2();
if (v2) return { tokens: v2.tokens, source: "file", filePath: v2.filePath };

// Fall back to legacy plain-text files
for (const filePath of AUTH_LEGACY_PATHS) {
const tokens = tryParseAuthFile(filePath);
if (tokens) return { tokens, source: "file", filePath };
}
Expand Down
3 changes: 2 additions & 1 deletion extensions/agent-usage/src/droid/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ export function useDroidUsage(enabled = true) {
setUsage(null);
setError({
type: "not_configured",
message: "Droid not configured. Run `droid` to log in (auto-detected from ~/.factory/auth.*).",
message:
"Droid not configured. Run `droid` to log in (auto-detected from ~/.factory/auth.v2.* or ~/.factory/auth.*).",
});
setIsLoading(false);
setHasInitialFetch(true);
Expand Down
Loading