Skip to content
Merged
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
161 changes: 58 additions & 103 deletions apps/web/lib/actions/api-keys.ts
Original file line number Diff line number Diff line change
@@ -1,133 +1,88 @@
"use server";

import { getServerSession } from "@/lib/server-auth";
import { db } from "@/db";
import { apiKeys } from "@/db/schema";
import { eq, and, isNull } from "drizzle-orm";
import { forwardToCP, getPrimaryOrgId } from "@/lib/cp-proxy";

function generateId(): string {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
const array = new Uint8Array(24);
crypto.getRandomValues(array);
for (const byte of array) {
result += chars[byte % chars.length];
}
return result;
}

function generateApiKey(): string {
const chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let key = "vl_live_sk_";
const array = new Uint8Array(32);
crypto.getRandomValues(array);
for (const byte of array) {
key += chars[byte % chars.length];
}
return key;
}
// API keys are minted by the CONTROL PLANE (POST /admin/v1/orgs/{orgId}/
// api-keys), which is what actually authenticates requests to the engine.
// The dashboard used to write keys to a *local* table — those never worked
// against the API. These actions now proxy to the CP so keys are real.

async function hashKey(key: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(key);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
interface CpAPIKey {
id: string;
name: string;
prefix: string;
scopes?: string[];
last_used_at?: string | null;
created_at?: string;
}

export async function createApiKey(data: {
name: string;
permissions?: string;
expiresAt?: string;
}) {
const session = await getServerSession();
if (!session) {
return { error: "Unauthorized" };
}

try {
const rawKey = generateApiKey();
const keyHash = await hashKey(rawKey);
const keyPrefix = rawKey.slice(0, 12);
const id = generateId();
const orgId = await getPrimaryOrgId();
if (!orgId) return { error: "No organization found on your account." };

await db.insert(apiKeys).values({
id,
userId: session.user.id,
name: data.name,
keyPrefix,
keyHash,
permissions: data.permissions || "full",
expiresAt: data.expiresAt ? new Date(data.expiresAt) : null,
});

// Return the raw key - it can only be shown once
return {
success: true,
key: rawKey,
keyId: id,
name: data.name,
prefix: keyPrefix,
};
} catch (err) {
const { ok, status, data: resp } = await forwardToCP(
`/admin/v1/orgs/${orgId}/api-keys`,
{ method: "POST", orgId, body: { name: data.name } },
);
if (!ok) {
return {
error:
err instanceof Error ? err.message : "Failed to create API key",
(resp as { error?: string } | null)?.error ??
`Failed to create API key (HTTP ${status})`,
};
}

const r = (resp ?? {}) as CpAPIKey & { key?: string };
return {
success: true,
key: r.key, // plaintext, shown once
keyId: r.id,
name: r.name ?? data.name,
prefix: r.prefix ?? r.key?.slice(0, 12),
};
}

export async function revokeApiKey(keyId: string) {
const session = await getServerSession();
if (!session) {
return { error: "Unauthorized" };
}
const orgId = await getPrimaryOrgId();
if (!orgId) return { error: "No organization found on your account." };

try {
await db
.update(apiKeys)
.set({ revokedAt: new Date() })
.where(and(eq(apiKeys.id, keyId), eq(apiKeys.userId, session.user.id)));

return { success: true };
} catch (err) {
const { ok, status, data } = await forwardToCP(
`/admin/v1/orgs/${orgId}/api-keys/${keyId}`,
{ method: "DELETE", orgId },
);
if (!ok) {
return {
error:
err instanceof Error ? err.message : "Failed to revoke API key",
(data as { error?: string } | null)?.error ??
`Failed to revoke API key (HTTP ${status})`,
};
}
return { success: true };
}

export async function listApiKeys() {
const session = await getServerSession();
if (!session) {
return { error: "Unauthorized", keys: [] };
}
const orgId = await getPrimaryOrgId();
if (!orgId) return { error: "No organization found", keys: [] };

try {
const keys = await db
.select({
id: apiKeys.id,
name: apiKeys.name,
keyPrefix: apiKeys.keyPrefix,
permissions: apiKeys.permissions,
rateLimit: apiKeys.rateLimit,
lastUsedAt: apiKeys.lastUsedAt,
expiresAt: apiKeys.expiresAt,
createdAt: apiKeys.createdAt,
})
.from(apiKeys)
.where(
and(eq(apiKeys.userId, session.user.id), isNull(apiKeys.revokedAt))
);
const { ok, data } = await forwardToCP(`/admin/v1/orgs/${orgId}/api-keys`, {
orgId,
});
if (!ok) return { error: "Failed to list API keys", keys: [] };

return { success: true, keys };
} catch (err) {
return {
error:
err instanceof Error ? err.message : "Failed to list API keys",
keys: [],
};
}
const keys = ((data as CpAPIKey[]) ?? []).map((k) => ({
id: k.id,
name: k.name,
keyPrefix: k.prefix,
permissions: (k.scopes ?? []).join(", ") || "full",
rateLimit: null as number | null,
lastUsedAt: k.last_used_at ? new Date(k.last_used_at) : null,
expiresAt: null as Date | null,
createdAt: k.created_at ? new Date(k.created_at) : new Date(),
}));

return { success: true, keys };
}
Loading