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:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user