feat(auth): support direct api key login
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import { existsSync, readFileSync } from "node:fs"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function stringValue(value: unknown): string | undefined {
|
||||
return typeof value === "string" ? value : undefined
|
||||
}
|
||||
|
||||
function defaultAuthPaths(home: string): string[] {
|
||||
return [
|
||||
join(home, ".commandcode", "auth.json"),
|
||||
join(home, ".pi", "agent", "auth.json"),
|
||||
join(home, ".omp", "agent", "auth.json"),
|
||||
]
|
||||
}
|
||||
|
||||
function apiKeyFromCredential(value: unknown): string | undefined {
|
||||
if (!isRecord(value)) return undefined
|
||||
|
||||
if (stringValue(value.type) === "oauth") return stringValue(value.access)
|
||||
if (stringValue(value.type) === "api") return stringValue(value.key)
|
||||
return stringValue(value.access) ?? stringValue(value.key)
|
||||
}
|
||||
|
||||
export function getConfiguredApiKey(
|
||||
options: {
|
||||
env?: NodeJS.ProcessEnv
|
||||
authPaths?: readonly string[]
|
||||
homeDir?: () => string
|
||||
} = {},
|
||||
): string | undefined {
|
||||
const env = options.env ?? process.env
|
||||
if (env.COMMANDCODE_API_KEY) return env.COMMANDCODE_API_KEY
|
||||
|
||||
const home = options.homeDir?.() ?? homedir()
|
||||
const authPaths = options.authPaths ?? defaultAuthPaths(home)
|
||||
|
||||
for (const authPath of authPaths) {
|
||||
try {
|
||||
if (!existsSync(authPath)) continue
|
||||
const parsed: unknown = JSON.parse(readFileSync(authPath, "utf-8"))
|
||||
if (!isRecord(parsed)) continue
|
||||
|
||||
const apiKey = stringValue(parsed.apiKey)
|
||||
if (apiKey) return apiKey
|
||||
|
||||
const commandcode = stringValue(parsed.commandcode)
|
||||
if (commandcode) return commandcode
|
||||
|
||||
const providerKey = apiKeyFromCredential(parsed.commandcode)
|
||||
if (providerKey) return providerKey
|
||||
|
||||
const commandCode = stringValue(parsed["command-code"])
|
||||
if (commandCode) return commandCode
|
||||
|
||||
const commandCodeKey = apiKeyFromCredential(parsed["command-code"])
|
||||
if (commandCodeKey) return commandCodeKey
|
||||
} catch {
|
||||
// Ignore malformed or unreadable auth files.
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
+53
-14
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user