Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 9 additions & 1 deletion auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,15 @@ function mapTrpcError(
return { code: ExitCode.NOT_FOUND, errorCode: backendCode ?? "NOT_FOUND" };
}
if (httpStatus === 409) {
return { code: ExitCode.CONFLICT, errorCode: backendCode ?? "CONFLICT" };
const hint = backendCode === "SLUG_ALREADY_IN_USE"
? "A resource with that name already exists. Use a different name, " +
"or run the corresponding update/publish command against the existing one."
: undefined;
return {
code: ExitCode.CONFLICT,
errorCode: backendCode ?? "CONFLICT",
hint,
};
}
if (httpStatus !== undefined && httpStatus >= 500) {
return { code: ExitCode.NETWORK, errorCode: backendCode ?? "BACKEND" };
Expand Down
65 changes: 63 additions & 2 deletions deploy/mod.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { Command, ValidationError } from "@cliffy/command";
import { green, red, setColorEnabled, yellow } from "@std/fmt/colors";
import { error, renderTemporalTimestamp } from "../util.ts";
import {
error,
renderTemporalTimestamp,
tablePrinter,
writeJsonResult,
} from "../util.ts";
import { createSwitchCommand, type GlobalContext } from "../main.ts";
import { VERSION } from "../version.ts";
import { actionHandler, getApp, getOrg } from "../config.ts";
Expand Down Expand Up @@ -234,6 +239,61 @@ const logoutCommand = new Command()
console.log(`${green("✔")} Successfully logged out`);
});

interface WhoamiOrg {
id: string;
name: string;
slug: string;
plan: string | null;
}

const whoamiCommand = new Command<GlobalContext>()
.description(
"Verify the current Deno Deploy token and list reachable organizations",
)
.example(
"Check that DENO_DEPLOY_TOKEN works",
"whoami --json",
)
.action(actionHandler(async (config, options) => {
config.noCreate();
// Touch tokenStorage via the tRPC client; this will surface a clean
// AUTH_INVALID_TOKEN envelope from the errorLink if the token is bad,
// without ever calling `requireInteractive()` or opening a browser.
const trpcClient = createTrpcClient(options);
const orgs = await trpcClient.query("orgs.list") as WhoamiOrg[];

if (options.json) {
writeJsonResult({
authenticated: true,
// The deployng tRPC router does not currently expose user identity,
// so we surface what we can (orgs the token can reach). When that
// procedure lands, this output will gain a `user` field; existing
// consumers reading `authenticated` / `orgs[]` keep working.
user: null,
orgs: orgs.map((org) => ({
id: org.id,
slug: org.slug,
name: org.name,
plan: org.plan,
})),
});
return;
}

console.log(
`${green("✔")} Authenticated. ${orgs.length} reachable organization${
orgs.length === 1 ? "" : "s"
}:`,
);
if (orgs.length > 0) {
tablePrinter(
["SLUG", "NAME", "PLAN"],
orgs,
(org) => [org.slug, org.name, org.plan ?? "—"],
);
}
}));

export const deployCommand = new Command()
.name("deno deploy")
.version(VERSION)
Expand Down Expand Up @@ -341,4 +401,5 @@ deploy your local directory to the specified application.`)
.command("setup-gcp", setupGCPCommand)
.command("tunnel-login", tunnelLoginCommand)
.command("switch", createSwitchCommand(true))
.command("logout", logoutCommand);
.command("logout", logoutCommand)
.command("whoami", whoamiCommand);
14 changes: 14 additions & 0 deletions tests/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,20 @@ Deno.test("setup-aws --non-interactive without --policies surfaces MISSING_FLAG"
);
});

Deno.test("whoami --json with bad token emits AUTH envelope (exit 3, no browser)", async () => {
const res = await deployRaw(
"--json",
"--token",
"obviously-invalid-token",
"--endpoint",
"http://127.0.0.1:1",
"whoami",
);
assertEquals(res.code, 3, `stderr: ${res.stderr}`);
const envelope = JSON.parse(res.stderr.trim().split("\n").pop()!);
assertEquals(envelope.error.code, "AUTH_INVALID_TOKEN");
});

Deno.test("non-zero exit code matches taxonomy for invalid flag (USAGE=2)", async () => {
// Cliffy's ValidationError handler exits with code 1 by default;
// verify the agent can pattern-match on stderr text either way.
Expand Down
Loading