feat(hwlab-cli): v0.2 client auth whoami reads HWLAB_API_KEY first

- requestJson now prefers HWLAB_API_KEY (env or --apiKey / --bearerToken) and sends Authorization: Bearer hwl_live_...
- new client auth whoami command returns actor + authMethod via /v1/users/me
- client auth status reports apiKey.source and apiKey.prefix
- authVisibility + authDiagnosis surface apiKeySource/Prefix; invalid key returns api_key_invalid diagnosis
- legacy cookie / auto-login paths still work when no HWLAB_API_KEY is set
This commit is contained in:
Codex
2026-06-04 02:08:00 +08:00
committed by Codex Agent
parent 49dc7cc6e1
commit ad53ff6ffb
+59 -1
View File
@@ -153,6 +153,14 @@ async function authCommand(context: any) {
if (subcommand === "login") return authLogin(context);
if (subcommand === "status") return authStatus(context);
if (subcommand === "profiles") return authProfiles(context);
if (subcommand === "whoami") {
const response = await requestJson({ ...context, method: "GET", path: "/v1/users/me" });
return responsePayload("client.auth.whoami", response, context, {
route: route("GET", "/v1/users/me"),
actor: response.body?.actor ?? null,
authMethod: response.body?.authMethod ?? null
});
}
if (subcommand === "session") {
const localSession = sessionStateSummary(await readSessionState({ parsed: context.parsed, env: context.env, cwd: context.cwd ?? process.cwd() }));
const response = await requestJson({ ...context, method: "GET", path: "/auth/session" });
@@ -173,6 +181,7 @@ function authHelp() {
commands: [
"login --username USER --password-env HWLAB_PASSWORD [--profile NAME]",
"status [--profile NAME]",
"whoami",
"session [--profile NAME]",
"profiles",
"logout [--profile NAME]"
@@ -183,11 +192,13 @@ function authHelp() {
async function authStatus(context: any) {
const endpoint = runtimeEndpoint(context.parsed, context.env, "web");
const localSession = sessionStateSummary(await readSessionState({ parsed: context.parsed, env: context.env, cwd: context.cwd ?? process.cwd() }));
const apiKey = explicitApiKey(context.parsed, context.env);
return ok("client.auth.status", {
baseUrl: endpoint.baseUrl,
runtimeEndpoint: runtimeEndpointVisibility(endpoint),
stateFile: localSession.stateFile,
localSession,
apiKey: apiKey ? { source: "env-or-explicit", prefix: apiKeyPrefixSummary(apiKey) } : { source: null, prefix: null },
nextCommands: authNextCommands({ baseUrl: endpoint.baseUrl, username: text(context.parsed.username ?? context.env.HWLAB_USERNAME) || "admin", stateFile: localSession.stateFile })
});
}
@@ -1711,6 +1722,25 @@ async function requestJson(context: any) {
const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
try {
const effectiveAuth = auth && parsed.noAuth !== true;
const apiKey = effectiveAuth ? explicitApiKey(parsed, env) : "";
if (apiKey) {
const response = await sendJsonRequest({ context, url, method, body, apiKey, extraHeaders, signal: controller.signal, pathName, timeoutMs: requestTimeoutMs });
const authState = {
required: effectiveAuth,
baseUrl: resolvedBaseUrl,
username: null,
stateFile: null,
localSession: null,
cookieSource: null,
apiKeySource: "env-or-explicit",
apiKeyPrefix: apiKeyPrefixSummary(apiKey),
autoLoginAttempted: false,
autoLoginStatus: null,
autoLoginHttpStatus: null,
retryAfterLogin: false
};
return { ...response, runtimeEndpoint: endpoint, auth: authVisibility(authState), authDiagnosis: authDiagnosis(response, authState) };
}
const explicitCookie = explicitAuthCookie(parsed, env);
const sessionState = effectiveAuth && !explicitCookie && parsed.noSession !== true
? await readSessionState({ parsed, env, cwd: cwd ?? process.cwd() })
@@ -1758,10 +1788,11 @@ async function requestJson(context: any) {
}
}
async function sendJsonRequest({ context, url, method, body, cookie, extraHeaders, signal, pathName, timeoutMs }: any) {
async function sendJsonRequest({ context, url, method, body, cookie, apiKey, extraHeaders, signal, pathName, timeoutMs }: any) {
const headers = clean({
accept: "application/json",
...(body ? { "content-type": "application/json" } : {}),
...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
...(cookie ? { cookie } : {}),
...extraHeaders
});
@@ -1875,6 +1906,20 @@ function explicitAuthCookie(parsed: ParsedArgs, env: EnvLike) {
return explicit ? explicit.includes("=") ? explicit : `hwlab_session=${encodeURIComponent(explicit)}` : null;
}
const HWLAB_API_KEY_PREFIX = "hwl_live_";
function explicitApiKey(parsed: ParsedArgs, env: EnvLike) {
const explicit = text(parsed.apiKey ?? parsed.bearerToken ?? env.HWLAB_API_KEY ?? env.HWLAB_BEARER_TOKEN);
if (!explicit) return "";
if (explicit.startsWith(HWLAB_API_KEY_PREFIX)) return explicit;
return "";
}
function apiKeyPrefixSummary(secret) {
const text = String(secret ?? "");
if (!text) return "";
if (text.length <= 12) return text;
return `${text.slice(0, 12)}\u2026`;
}
function autoAuthAllowed(parsed: ParsedArgs) {
return parsed.noAuth !== true && parsed.noAutoAuth !== true;
}
@@ -2022,6 +2067,8 @@ function authVisibility(authState: any) {
cookieSource: authState.cookieSource,
stateFile: authState.stateFile,
localSession: authState.localSession,
apiKeySource: authState.apiKeySource,
apiKeyPrefix: authState.apiKeyPrefix,
autoLoginAttempted: authState.autoLoginAttempted,
autoLoginStatus: authState.autoLoginStatus,
autoLoginHttpStatus: authState.autoLoginHttpStatus,
@@ -2032,6 +2079,17 @@ function authVisibility(authState: any) {
function authDiagnosis(response: any, authState: any) {
if (!authState?.required) return null;
if (response.status !== 401 && response.status !== 403) return null;
if (response.status === 401 && authState.apiKeySource) {
return {
code: "api_key_invalid",
status: "unauthorized",
message: "HWLAB_API_KEY was rejected by the cloud-api; regenerate the key with `client auth default` or check that the env value matches the one shown in the Web API key page.",
httpStatus: response.status,
apiKeySource: authState.apiKeySource,
apiKeyPrefix: authState.apiKeyPrefix,
nextCommands: ["bun tools/hwlab-cli/bin/hwlab-cli.ts client request POST /v1/api-keys -H 'content-type: application/json' --body '{\"name\":\"rotated\"}' --help"]
};
}
if (response.status === 403) {
return {
code: "auth_forbidden",