Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion cli/auth/exit-code.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe("cli/auth exit codes", () => {
code: "AUTHENTICATION_ERROR",
slug: "authentication-required",
registrySlug: "authentication-required",
message: "Not logged in. Run 'veryfront login' to authenticate.",
message: "Not logged in. Run 'veryfront login', or set VERYFRONT_API_KEY to an API key.",
context: { authenticated: false },
},
});
Expand Down
180 changes: 175 additions & 5 deletions cli/auth/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -836,7 +836,7 @@ describe("Login Module", { sanitizeOps: false, sanitizeResources: false }, () =>
code: "AUTHENTICATION_ERROR",
slug: "authentication-required",
registrySlug: "authentication-required",
message: "Not logged in. Set VERYFRONT_API_TOKEN or run in interactive mode.",
message: "Not logged in. Set VERYFRONT_API_KEY or run in interactive mode.",
},
});
assertEquals(output.join("\n").includes("Enter your API token"), false);
Expand Down Expand Up @@ -1202,7 +1202,7 @@ describe("Login Module", { sanitizeOps: false, sanitizeResources: false }, () =>
code: "AUTHENTICATION_ERROR",
slug: "authentication-required",
registrySlug: "authentication-required",
message: "Not logged in. Set VERYFRONT_API_TOKEN or run in interactive mode.",
message: "Not logged in. Set VERYFRONT_API_KEY or run in interactive mode.",
});
assertEquals(requestedAuth, ["Bearer env-invalid-token"]);
assertEquals(output.join("\n").includes("stored@example.com"), false);
Expand Down Expand Up @@ -1487,7 +1487,7 @@ describe("Login Module", { sanitizeOps: false, sanitizeResources: false }, () =>
code: "AUTHENTICATION_ERROR",
slug: "authentication-required",
registrySlug: "authentication-required",
message: "Not logged in. Set VERYFRONT_API_TOKEN or run in interactive mode.",
message: "Not logged in. Set VERYFRONT_API_KEY or run in interactive mode.",
});
assertEquals(requestedAuth, ["Bearer config-invalid-token"]);
assertEquals(output.join("\n").includes("stored@example.com"), false);
Expand Down Expand Up @@ -2296,6 +2296,80 @@ describe("Login Module", { sanitizeOps: false, sanitizeResources: false }, () =>
}
});

it("tells you to unset VERYFRONT_API_KEY when the key supplied the credential", async () => {
const originalLog = console.log;
const originalPrompt = globalThis.prompt;
const savedToken = getEnv("VERYFRONT_API_TOKEN");
const savedKey = getEnv("VERYFRONT_API_KEY");
const { __resetEnvLoaderForTests, loadEnv } = await import("veryfront/utils/env-loader");
const envDir = await makeTempDir({ prefix: "login-api-key-hints-" });
const rejectedKey = () => Promise.resolve(new Response(null, { status: 401 }));
const responses: Array<() => Promise<Response>> = [
rejectedKey,
() => Promise.reject(new DOMException("deadline exceeded", "TimeoutError")),
() => Promise.resolve(Response.json({ id: "key-user", email: "key-user@example.com" })),
rejectedKey,
];
const printedByCase: string[] = [];

try {
for (const [index, respond] of responses.entries()) {
const output: string[] = [];
deleteEnv("VERYFRONT_API_TOKEN");
setEnv("VERYFRONT_API_KEY", "vf_test_key_credential");
__resetEnvLoaderForTests();
await loadEnv({ cwd: envDir });
console.log = (message?: unknown) => output.push(String(message));
globalThis.prompt = (() => "replacement-token") as typeof prompt;

const { ensureAuthenticated, login } = await import("./login.ts");
const env = { ...testEnv, apiToken: getEnv("VERYFRONT_API_TOKEN") };
await withMockFetch(() => respond(), async () => {
// The last case runs the silent-validation path, which warns instead of prompting.
if (index === responses.length - 1) await ensureAuthenticated(env, envDir);
else await login(undefined, env);
});
printedByCase.push(output.join("\n"));
}
} finally {
globalThis.prompt = originalPrompt;
console.log = originalLog;
__resetEnvLoaderForTests();
if (savedToken === undefined) deleteEnv("VERYFRONT_API_TOKEN");
else setEnv("VERYFRONT_API_TOKEN", savedToken);
if (savedKey === undefined) deleteEnv("VERYFRONT_API_KEY");
else setEnv("VERYFRONT_API_KEY", savedKey);
resetInteractiveMode();
await safeDeleteToken();
await remove(envDir, { recursive: true });
}

const [rejected, unavailable, existing, warned] = printedByCase;
assertStringIncludes(rejected!, "VERYFRONT_API_KEY was rejected by the Veryfront API.");
assertStringIncludes(
rejected!,
"Unset VERYFRONT_API_KEY or replace the variable before signing in with another method.",
);
assertStringIncludes(
unavailable!,
"Timed out while checking VERYFRONT_API_KEY with the Veryfront API.",
);
assertStringIncludes(
unavailable!,
"Unset VERYFRONT_API_KEY before signing in with another method.",
);
assertStringIncludes(existing!, "Using VERYFRONT_API_KEY; no stored login was created.");
assertStringIncludes(
existing!,
"Unset VERYFRONT_API_KEY before using another login method, or replace the variable to switch tokens.",
);
assertStringIncludes(warned!, "Warning: VERYFRONT_API_KEY is invalid");
for (const printed of printedByCase) {
assertEquals(printed.includes("VERYFRONT_API_TOKEN"), false, "the user set only the key");
assertEquals(printed.includes("vf_test_key_credential"), false, "the key is never printed");
}
});

it("stops under an unavailable shell environment token in human mode", async () => {
const originalFetch = globalThis.fetch;
const originalLog = console.log;
Expand Down Expand Up @@ -3148,7 +3222,8 @@ describe("Login Module", { sanitizeOps: false, sanitizeResources: false }, () =>
code: "AUTHENTICATION_ERROR",
slug: "authentication-required",
registrySlug: "authentication-required",
message: "Not logged in. Run 'veryfront login' to authenticate.",
message:
"Not logged in. Run 'veryfront login', or set VERYFRONT_API_KEY to an API key.",
context: { authenticated: false },
},
});
Expand Down Expand Up @@ -3316,7 +3391,8 @@ describe("Login Module", { sanitizeOps: false, sanitizeResources: false }, () =>
code: "AUTHENTICATION_ERROR",
slug: "authentication-required",
registrySlug: "authentication-required",
message: "Not logged in. Run 'veryfront login' to authenticate.",
message:
"Not logged in. Run 'veryfront login', or set VERYFRONT_API_KEY to an API key.",
context: { authenticated: false },
},
});
Expand Down Expand Up @@ -3452,6 +3528,100 @@ describe("Login Module", { sanitizeOps: false, sanitizeResources: false }, () =>
);
});

it("names VERYFRONT_API_KEY when the key supplied the credential", async () => {
const originalLog = console.log;
const output: string[] = [];
const { __resetEnvLoaderForTests, loadEnv } = await import("veryfront/utils/env-loader");
const envDir = await makeTempDir({ prefix: "whoami-api-key-" });
const savedKey = getEnv("VERYFRONT_API_KEY");

try {
deleteEnv("VERYFRONT_API_TOKEN");
deleteEnv("VERYFRONT_API_KEY");
__resetEnvLoaderForTests();
await Deno.writeTextFile(`${envDir}/.env`, "VERYFRONT_API_KEY=vf_test_file_key\n");
await loadEnv({ cwd: envDir });
console.log = (message?: unknown) => output.push(String(message));

await withMockFetch(
() =>
Promise.resolve(
new Response(JSON.stringify({ data: [], page_info: {} }), {
status: 200,
headers: { "content-type": "application/json" },
}),
),
async () => {
const { whoami } = await import("./login.ts");
await whoami({
...testEnv,
apiBaseUrl: "https://auth.example.test",
apiUrl: undefined,
apiToken: getEnv("VERYFRONT_API_TOKEN"),
});
},
);

const printed = output.join("\n");
assertStringIncludes(printed, "(via VERYFRONT_API_KEY from .env)");
assertEquals(printed.includes("VERYFRONT_API_TOKEN"), false, "the user set only the key");
assertEquals(printed.includes("vf_test_file_key"), false, "the key is never printed");
} finally {
console.log = originalLog;
__resetEnvLoaderForTests();
deleteEnv("VERYFRONT_API_TOKEN");
if (savedKey === undefined) deleteEnv("VERYFRONT_API_KEY");
else setEnv("VERYFRONT_API_KEY", savedKey);
await remove(envDir, { recursive: true });
}
});

it("names VERYFRONT_API_KEY for an exported key", async () => {
const originalLog = console.log;
const output: string[] = [];
const { __resetEnvLoaderForTests, loadEnv } = await import("veryfront/utils/env-loader");
const envDir = await makeTempDir({ prefix: "whoami-api-key-shell-" });
const savedKey = getEnv("VERYFRONT_API_KEY");

try {
deleteEnv("VERYFRONT_API_TOKEN");
setEnv("VERYFRONT_API_KEY", "vf_test_shell_key");
__resetEnvLoaderForTests();
await loadEnv({ cwd: envDir });
console.log = (message?: unknown) => output.push(String(message));

await withMockFetch(
() =>
Promise.resolve(
new Response(JSON.stringify({ data: [], page_info: {} }), {
status: 200,
headers: { "content-type": "application/json" },
}),
),
async () => {
const { whoami } = await import("./login.ts");
await whoami({
...testEnv,
apiBaseUrl: "https://auth.example.test",
apiUrl: undefined,
apiToken: getEnv("VERYFRONT_API_TOKEN"),
});
},
);

const printed = output.join("\n");
assertStringIncludes(printed, "(via VERYFRONT_API_KEY)");
assertEquals(printed.includes("vf_test_shell_key"), false, "the key is never printed");
} finally {
console.log = originalLog;
__resetEnvLoaderForTests();
deleteEnv("VERYFRONT_API_TOKEN");
if (savedKey === undefined) deleteEnv("VERYFRONT_API_KEY");
else setEnv("VERYFRONT_API_KEY", savedKey);
await remove(envDir, { recursive: true });
}
});

it("does not attribute an injected API token to a different loaded .env token", async () => {
const originalFetch = globalThis.fetch;
const originalLog = console.log;
Expand Down
50 changes: 32 additions & 18 deletions cli/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ import {
outputJson,
} from "../shared/json-output.ts";
import { isInteractive } from "../shared/interactive.ts";
import { getEnvSource } from "veryfront/utils/env-loader";
import {
getEnvSource,
isVeryfrontApiTokenFromKey,
veryfrontApiCredentialEnvName,
} from "veryfront/utils/env-loader";
import { basename, isAbsolute, relative } from "veryfront/platform/path";
import { cwd, getEnv } from "veryfront/platform";

Expand Down Expand Up @@ -68,12 +72,15 @@ export function formatEnvSourcePathForDisplay(file: string, currentCwd = cwd()):
}

function describeApiTokenSource(token: string): string {
// The name the user actually set: `VERYFRONT_API_KEY` fills the token slot
// when only the key is set, and the user never typed `VERYFRONT_API_TOKEN`.
const name = isVeryfrontApiTokenFromKey() ? "VERYFRONT_API_KEY" : "VERYFRONT_API_TOKEN";
const origin = getEnvSource("VERYFRONT_API_TOKEN");
if (origin.source !== "env-file" || getEnv("VERYFRONT_API_TOKEN") !== token) {
return "(via VERYFRONT_API_TOKEN)";
return `(via ${name})`;
}

return `(via VERYFRONT_API_TOKEN from ${formatEnvSourcePathForDisplay(origin.file)})`;
return `(via ${name} from ${formatEnvSourcePathForDisplay(origin.file)})`;
}

type BrowserAuthMethod = "google" | "github" | "microsoft";
Expand Down Expand Up @@ -142,7 +149,7 @@ async function outputLoginAuthenticationRequiredJson(): Promise<void> {
code: "AUTHENTICATION_ERROR",
slug: "authentication-required",
registrySlug: "authentication-required",
message: "Not logged in. Set VERYFRONT_API_TOKEN or run in interactive mode.",
message: "Not logged in. Set VERYFRONT_API_KEY or run in interactive mode.",
}));
}

Expand Down Expand Up @@ -539,7 +546,9 @@ function writeConfigFileSwitchingGuidance(): void {
function writeEnvironmentConfigFileSwitchingGuidance(): void {
console.log(
" " +
dim("Remove or replace apiToken in veryfront.json after unsetting VERYFRONT_API_TOKEN."),
dim(
`Remove or replace apiToken in veryfront.json after unsetting ${veryfrontApiCredentialEnvName()}.`,
),
);
}

Expand All @@ -549,11 +558,13 @@ function writeAuthoritativeCredentialRejectedMessage(
): void {
console.log();
if (source === "environment") {
console.log(" " + error("✗") + " VERYFRONT_API_TOKEN was rejected by the Veryfront API.");
console.log(
" " + error("✗") + ` ${veryfrontApiCredentialEnvName()} was rejected by the Veryfront API.`,
Comment thread
mattboon marked this conversation as resolved.
Resolved
);
console.log(
" " +
dim(
"Unset VERYFRONT_API_TOKEN or replace the variable before signing in with another method.",
`Unset ${veryfrontApiCredentialEnvName()} or replace the variable before signing in with another method.`,
),
);
if (hasConfigToken) writeEnvironmentConfigFileSwitchingGuidance();
Expand All @@ -567,7 +578,9 @@ function writeAuthoritativeCredentialRejectedMessage(
}

function credentialSourceForDisplay(source: "config-file" | "environment"): string {
return source === "environment" ? "VERYFRONT_API_TOKEN" : "apiToken from veryfront.json";
return source === "environment"
? veryfrontApiCredentialEnvName()
: "apiToken from veryfront.json";
}

function writeAuthoritativeCredentialUnavailableMessage(
Expand Down Expand Up @@ -595,7 +608,7 @@ function writeAuthoritativeCredentialUnavailableMessage(

if (source === "environment") {
console.log(
" " + dim("Unset VERYFRONT_API_TOKEN before signing in with another method."),
" " + dim(`Unset ${veryfrontApiCredentialEnvName()} before signing in with another method.`),
);
if (hasConfigToken) writeEnvironmentConfigFileSwitchingGuidance();
return;
Expand Down Expand Up @@ -631,22 +644,23 @@ function writeExistingSession(
// the case `whoami` now names, so the session ends at the directory
// boundary. Say so rather than let them discover it elsewhere.
if (source === "environment") {
const envName = veryfrontApiCredentialEnvName();
if (hasStoredToken) {
console.log(
" " + dim("Using VERYFRONT_API_TOKEN; it takes precedence over a stored credential."),
" " + dim(`Using ${envName}; it takes precedence over a stored credential.`),
);
console.log(
" " +
dim(
"Unset VERYFRONT_API_TOKEN before attempting to use the stored credential, or replace the variable to switch tokens.",
`Unset ${envName} before attempting to use the stored credential, or replace the variable to switch tokens.`,
),
);
} else {
console.log(" " + dim("Using VERYFRONT_API_TOKEN; no stored login was created."));
console.log(" " + dim(`Using ${envName}; no stored login was created.`));
console.log(
" " +
dim(
"Unset VERYFRONT_API_TOKEN before using another login method, or replace the variable to switch tokens.",
`Unset ${envName} before using another login method, or replace the variable to switch tokens.`,
),
);
}
Expand Down Expand Up @@ -898,7 +912,7 @@ export async function login(
await assertApiUrlAcceptsNewCredential(env, projectDir);

if (!isInteractive() && (method === undefined || method === "token")) {
cliLogger.error("Not logged in. Set VERYFRONT_API_TOKEN or run in interactive mode.");
cliLogger.error("Not logged in. Set VERYFRONT_API_KEY or run in interactive mode.");
return null;
}

Expand All @@ -913,7 +927,7 @@ export async function login(

function warnRejectedCredentialSource(source: ApiTokenSource, humanOutput: boolean): void {
if (source === "env" && humanOutput) {
console.log(" " + warning("Warning: VERYFRONT_API_TOKEN is invalid"));
console.log(" " + warning(`Warning: ${veryfrontApiCredentialEnvName()} is invalid`));
}
if (source === "config-file" && humanOutput) {
console.log(" " + warning("Warning: apiToken from veryfront.json is invalid"));
Expand All @@ -936,7 +950,7 @@ async function requestInteractiveAuthentication(
validationOptions: CredentialValidationOptions,
): Promise<AuthIdentity | null> {
if (!isTTY() || !isInteractive()) {
cliLogger.error("Not logged in. Set VERYFRONT_API_TOKEN or run in interactive mode.");
cliLogger.error("Not logged in. Set VERYFRONT_API_KEY or run in interactive mode.");
return null;
}

Expand Down Expand Up @@ -1091,15 +1105,15 @@ export async function whoami(
code: "AUTHENTICATION_ERROR",
slug: "authentication-required",
registrySlug: "authentication-required",
message: "Not logged in. Run 'veryfront login' to authenticate.",
message: "Not logged in. Run 'veryfront login', or set VERYFRONT_API_KEY to an API key.",
context: { authenticated: false },
}));
return null;
}

console.log();
console.log(" " + error("✗") + " Not logged in");
console.log(" " + dim("Run 'veryfront login' to authenticate"));
console.log(" " + dim("Run 'veryfront login', or set VERYFRONT_API_KEY to an API key"));

return null;
}
Expand Down
Loading
Loading