feat(auth): support direct api key login

This commit is contained in:
Patrick Wozniak
2026-08-18 10:55:58 +02:00
parent c4d25d1db1
commit 0603291396
4 changed files with 217 additions and 20 deletions
+53 -14
View File
@@ -1,13 +1,13 @@
/**
* Command Code OAuth provider for pi's /login flow.
*
* Implements a browser-assisted API key retrieval flow:
* 1. Starts a local HTTP server on a Command Code CLI-compatible port
* 2. Opens the Command Code Studio auth page in the browser
* 3. The user authenticates on the Command Code website
* 4. The website POSTs the API key back to the local server
* 5. If browser transfer fails, the user can paste the API key manually
* 6. The API key is stored in pi's auth.json as OAuth credentials
* Implements two API key retrieval flows:
* 1. Browser-assisted login opens Command Code Studio and waits for the
* website to POST the API key back to a local callback server.
* 2. Direct API key login prompts the user to paste a Studio API key.
*
* If browser transfer fails, the user can still paste the API key manually.
* The API key is stored in pi's auth.json as OAuth credentials.
*
* Since Command Code API keys don't expire, we store them as
* OAuth credentials with a far-future expiry.
@@ -101,13 +101,35 @@ async function promptForApiKey(callbacks: OAuthLoginCallbacks, message: string)
return credentialsFromApiKey(apiKey)
}
/**
* Starts the browser-based login flow for Command Code.
*
* Returns OAuth credentials where access == refresh == the user's API key.
* The keys don't expire, so we set a far-future expiry.
*/
export async function login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
type LoginChoice = { type: "browser" } | { type: "prompt" } | { type: "apiKey"; apiKey: string }
async function chooseLoginFlow(callbacks: OAuthLoginCallbacks): Promise<LoginChoice> {
const input = sanitizeApiKey(
await callbacks.onPrompt({
message:
"Command Code login: press Enter for browser login, type 'key' to paste an API key, or paste the API key directly:",
}),
)
const normalized = input.toLowerCase()
if (!input || normalized === "1" || normalized === "b" || normalized === "browser") {
return { type: "browser" }
}
if (
normalized === "2" ||
normalized === "k" ||
normalized === "key" ||
normalized === "api" ||
normalized === "paste"
) {
return { type: "prompt" }
}
return { type: "apiKey", apiKey: input }
}
async function browserLogin(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
let authServer
try {
authServer = await startAuthServer()
@@ -151,6 +173,23 @@ export async function login(callbacks: OAuthLoginCallbacks): Promise<OAuthCreden
return credentialsFromApiKey(callback.apiKey)
}
/**
* Starts the login flow for Command Code.
*
* Returns OAuth credentials where access == refresh == the user's API key.
* The keys don't expire, so we set a far-future expiry.
*/
export async function login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
const choice = await chooseLoginFlow(callbacks)
if (choice.type === "apiKey") return credentialsFromApiKey(choice.apiKey)
if (choice.type === "prompt") {
return promptForApiKey(callbacks, "Paste your Command Code API key:")
}
return browserLogin(callbacks)
}
/**
* Command Code API keys don't expire, so "refresh" is a no-op.
* Returns the same credentials with an updated far-future expiry.