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
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,10 @@ import {
} from "@onecli/ui/components/accordion";
import { Badge } from "@onecli/ui/components/badge";
import { createSecret, updateSecret } from "@/lib/actions/secrets";

const detectAnthropicKeyType = (
val: string,
): "api_key" | "oauth_token" | null => {
if (val.startsWith("sk-ant-api")) return "api_key";
if (val.startsWith("sk-ant-oat")) return "oauth_token";
return null;
};
import {
detectAnthropicAuthMode,
looksLikeAnthropicKey,
} from "@/lib/validations/secret";

type SecretType = "anthropic" | "generic";

Expand Down Expand Up @@ -267,14 +263,39 @@ export const SecretDialog = ({
: "Enter secret value"
}
value={value}
onChange={(e) => setValue(e.target.value)}
onChange={(e) => {
const val = e.target.value;
setValue(val);
if (type === "anthropic" && !name.trim()) {
const detected = detectAnthropicAuthMode(val);
if (detected === "api-key") setName("Anthropic API Key");
else if (detected === "oauth")
setName("Anthropic OAuth Token");
}
}}
/>
<div className="flex items-center gap-2">
<p className="text-muted-foreground text-xs">
{type === "anthropic"
? "Paste your API key or OAuth token from the Anthropic Console."
: "Encrypted at rest. You won\u2019t be able to view this value again."}
</p>
{type === "anthropic" &&
value.trim() &&
!looksLikeAnthropicKey(value) ? (
<p className="text-xs text-amber-600 dark:text-amber-400">
{detectAnthropicAuthMode(value) !== null ? (
"This key looks incomplete. Make sure you copied the full value."
) : (
<>
Keys typically start with{" "}
<code className="text-[11px]">sk-ant-api</code> or{" "}
<code className="text-[11px]">sk-ant-oat</code>
</>
)}
</p>
) : (
<p className="text-muted-foreground text-xs">
{type === "anthropic"
? "Paste your API key or OAuth token from the Anthropic Console."
: "Encrypted at rest. You won\u2019t be able to view this value again."}
</p>
)}
{type === "anthropic" && <AnthropicKeyBadge value={value} />}
</div>
</div>
Expand Down Expand Up @@ -440,7 +461,7 @@ const TypeStep = ({ onSelect }: { onSelect: (type: SecretType) => void }) => (
);

const AnthropicKeyBadge = ({ value }: { value: string }) => {
const detected = detectAnthropicKeyType(value);
const detected = detectAnthropicAuthMode(value);
if (!detected) return null;

return (
Expand All @@ -450,12 +471,12 @@ const AnthropicKeyBadge = ({ value }: { value: string }) => {
>
<span
className={
detected === "api_key"
detected === "api-key"
? "bg-brand size-1.5 rounded-full"
: "bg-blue-500 size-1.5 rounded-full"
}
/>
{detected === "api_key" ? "API Key" : "OAuth Token"}
{detected === "api-key" ? "API Key" : "OAuth Token"}
</Badge>
);
};
6 changes: 4 additions & 2 deletions apps/web/src/lib/services/secret-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ export const createSecret = async (

const metadata =
input.type === "anthropic"
? ({ authMode: detectAnthropicAuthMode(value) } as Prisma.InputJsonValue)
? ({
authMode: detectAnthropicAuthMode(value) ?? "api-key",
} as Prisma.InputJsonValue)
: Prisma.JsonNull;

const secret = await db.secret.create({
Expand Down Expand Up @@ -153,7 +155,7 @@ export const updateSecret = async (
// Re-detect auth mode when value changes for Anthropic secrets
if (secret.type === "anthropic") {
data.metadata = {
authMode: detectAnthropicAuthMode(value),
authMode: detectAnthropicAuthMode(value) ?? "api-key",
} as Prisma.InputJsonValue;
}
}
Expand Down
18 changes: 15 additions & 3 deletions apps/web/src/lib/validations/secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ export const updateSecretSchema = z

export type UpdateSecretInput = z.infer<typeof updateSecretSchema>;

// ── Secret metadata ────────────────────────────────────────────────────
// ── Anthropic key helpers ──────────────────────────────────────────────

export const ANTHROPIC_KEY_MIN_LENGTH = 40;

export const anthropicAuthModes = ["api-key", "oauth"] as const;
export type AnthropicAuthMode = (typeof anthropicAuthModes)[number];
Expand All @@ -59,8 +61,18 @@ export interface AnthropicSecretMetadata {
}

/** Detect the auth mode from a plaintext Anthropic secret value. */
export const detectAnthropicAuthMode = (value: string): AnthropicAuthMode =>
value.startsWith("sk-ant-oat") ? "oauth" : "api-key";
export const detectAnthropicAuthMode = (
value: string,
): AnthropicAuthMode | null => {
if (value.startsWith("sk-ant-api")) return "api-key";
if (value.startsWith("sk-ant-oat")) return "oauth";
return null;
};

/** Returns true if the value has a known Anthropic prefix and meets the minimum length. */
export const looksLikeAnthropicKey = (value: string): boolean =>
detectAnthropicAuthMode(value) !== null &&
value.length >= ANTHROPIC_KEY_MIN_LENGTH;

/** Type-safe accessor for Anthropic metadata from a Prisma Json field. */
export const parseAnthropicMetadata = (
Expand Down
Loading