Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions packages/cli/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export const api = {
if (path === "/bootstrap") return unwrap<T>(c.bootstrap.$get());
if (path === "/keys") return unwrap<T>(c.keys.$get());
if (path === "/projects") return unwrap<T>(c.projects.$get());
// For /auth/verify and other unregistered paths, fall through to the generic fetch below
const projectEnvsMatch = path.match(/^\/projects\/([^/]+)\/environments$/);
if (projectEnvsMatch) {
const project = decodeURIComponent(projectEnvsMatch[1]);
Expand Down
8 changes: 6 additions & 2 deletions packages/cli/src/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ora from "ora";
import { api, KeyflareApiError } from "../api/client.js";
import { writeConfig, writeApiKey, getApiUrl, readApiKey } from "../config.js";
import { log, error, bold, dim } from "../output/log.js";
import type { AuthVerifyResponse } from "@keyflare/shared";

export async function runLogin() {
log(bold("\n🔑 Keyflare Login\n"));
Expand Down Expand Up @@ -82,8 +83,10 @@ export async function runLogin() {

// Verify credentials by calling the API
const spinner = ora("Verifying credentials...").start();
let verifiedKeyType: "user" | "system" = "user";
try {
await api.get("/keys");
const verify = await api.get<AuthVerifyResponse>("/auth/verify");
verifiedKeyType = verify.key_type;
spinner.succeed("Credentials verified");
} catch (err: any) {
spinner.fail("Failed to verify credentials");
Expand Down Expand Up @@ -115,8 +118,9 @@ export async function runLogin() {
delete process.env.KEYFLARE_API_KEY;
}

const keyTypeLabel = verifiedKeyType === "system" ? "system key" : "user key";
log(
`\n${bold("✓ Logged in!")}\n\n${dim("API URL:")} ${apiUrl}\n${dim("Credentials saved to:")} ~/.config/keyflare/\n`
`\n${bold("✓ Logged in!")}\n\n${dim("API URL:")} ${apiUrl}\n${dim("Key type:")} ${keyTypeLabel}\n${dim("Credentials saved to:")} ~/.config/keyflare/\n`
);
}

Expand Down
23 changes: 22 additions & 1 deletion packages/server/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Hono } from "hono";
import { VERSION } from "@keyflare/shared";
import type { HealthResponse } from "@keyflare/shared";
import type { HealthResponse, AuthVerifyResponse } from "@keyflare/shared";
import { dbAndKeysMiddleware, authMiddleware } from "./middleware/hono.js";
import { loggerMiddleware } from "./middleware/logger.js";
import { handleBootstrap, handleBootstrapStatus } from "./routes/bootstrap.js";
Expand Down Expand Up @@ -65,6 +65,27 @@ app.get(
(_c) => jsonOk<HealthResponse>({ ok: true, version: VERSION })
);

app.get(
"/auth/verify",
dbAndKeysMiddleware,
authMiddleware,
async (c) => {
const auth = c.get("auth");
const db = c.get("db");
const derivedKeys = c.get("derivedKeys");
// Look up the key prefix from the Authorization header
const { sha256 } = await import("./crypto/hash.js");
const apiKey = c.req.header("Authorization")?.slice(7).trim() ?? "";
const keyHash = await sha256(apiKey);
const { getKeyByHash } = await import("./db/queries.js");
const row = await getKeyByHash(db, keyHash);
return jsonOk<AuthVerifyResponse>({
key_type: auth.keyType,
key_prefix: row?.keyPrefix ?? "",
});
}
);

app.get(
"/bootstrap",
describeBootstrapStatusRoute(),
Expand Down
7 changes: 7 additions & 0 deletions packages/shared/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,10 @@ export interface HealthResponse {
ok: true;
version: string;
}

// ─── Auth Verify ───

export interface AuthVerifyResponse {
key_type: "user" | "system";
key_prefix: string;
}