import { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; import { and, eq, isNull, sql } from "drizzle-orm"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; import { schema } from "@aigxion/db"; import { keySchema } from "./schema"; const route = createRoute({ method: "get", path: "/v1/apis/{apiId}/keys", request: { header: z.object({ authorization: z .string() .regex(/^Bearer [a-zA-Z0-9_]+/) .openapi({ description: "A root key to authorize the request formatted as bearer token", example: "Bearer unkey_1234", }), }), params: z.object({ apiId: z.string().min(1).openapi({ description: "The id of the api to fetch", example: "api_1234", }), }), query: z.object({ limit: z.coerce.number().int().min(1).max(100).optional().default(100).openapi({ description: "The maximum number of keys to return", example: 100, }), offset: z.coerce.number().optional().openapi({ description: "Use this to fetch the next page of results. A new cursor will be returned in the response if there are more results.", }), ownerId: z.string().min(1).optional().openapi({ description: "If provided, this will only return keys where the `ownerId` matches.", }), }), }, responses: { 200: { description: "Keys belonging to the api", content: { "application/json": { schema: z.object({ keys: z.array(keySchema), total: z.number().int().openapi({ description: "The total number of keys for this api", }), }), }, }, }, ...openApiErrorResponses, }, }); export type Route = typeof route; export type LegacyApisListKeysResponse = z.infer< (typeof route.responses)[200]["content"]["application/json"]["schema"] >; export const registerLegacyApisListKeys = (app: App) => app.openapi(route, async (c) => { const { db, cache } = c.get("services"); const auth = await rootKeyAuth(c); const apiId = c.req.param("apiId"); const { limit, offset, ownerId } = c.req.query(); const { val: api, err } = await cache.withCache(c, "apiById", apiId, async () => { return ( (await db.query.apis.findFirst({ where: (table, { eq, and, isNull }) => and(eq(table.id, apiId), isNull(table.deletedAt)), })) ?? null ); }); if (err) { throw new UnkeyApiError({ code: "INTERNAL_SERVER_ERROR", message: `unable to load api: ${err.message}`, }); } if (!api || api.workspaceId !== auth.authorizedWorkspaceId) { throw new UnkeyApiError({ code: "NOT_FOUND", message: `api ${apiId} not found`, }); } if (!api.keyAuthId) { throw new UnkeyApiError({ code: "PRECONDITION_FAILED", message: `api ${apiId} is not setup to handle keys`, }); } const keysWhere: Parameters = [ isNull(schema.keys.deletedAt), eq(schema.keys.keyAuthId, api.keyAuthId), ]; if (ownerId) { keysWhere.push(eq(schema.keys.ownerId, ownerId)); } const keys = await db.query.keys.findMany({ where: and(...keysWhere), limit: parseInt(limit), orderBy: schema.keys.id, offset: offset ? parseInt(offset) : undefined, }); const total = await db // @ts-ignore, mysql sucks .select({ count: sql`count(*)` }) .from(schema.keys) .where(and(eq(schema.keys.keyAuthId, api.keyAuthId), isNull(schema.keys.deletedAt))); return c.json({ keys: keys.map((k) => ({ id: k.id, start: k.start, apiId: api.id, workspaceId: k.workspaceId, name: k.name ?? undefined, ownerId: k.ownerId ?? undefined, meta: k.meta ? JSON.parse(k.meta) : undefined, createdAt: k.createdAt.getTime() ?? undefined, expires: k.expires?.getTime() ?? undefined, ratelimit: k.ratelimitType && k.ratelimitLimit && k.ratelimitRefillRate && k.ratelimitRefillInterval ? { type: k.ratelimitType, limit: k.ratelimitLimit, refillRate: k.ratelimitRefillRate, refillInterval: k.ratelimitRefillInterval, } : undefined, remaining: k.remaining ?? undefined, })), // @ts-ignore, mysql sucks total: parseInt(total.at(0)?.count ?? "0"), cursor: keys.at(-1)?.id ?? undefined, }); });