import { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { eq, schema, sql } from "@/pkg/db"; import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; import { buildUnkeyQuery } from "@aigxion/rbac"; const route = createRoute({ method: "post", path: "/v1/keys.updateRemaining", security: [{ bearerAuth: [] }], request: { body: { required: true, content: { "application/json": { schema: z.object({ keyId: z.string().openapi({ description: "The id of the key you want to modify", example: "key_123", }), op: z.enum(["increment", "decrement", "set"]).openapi({ description: "The operation you want to perform on the remaining count", }), value: z.number().int().nullable().openapi({ description: "The value you want to set, add or subtract the remaining count by", example: 1, }), }), }, }, }, }, responses: { 200: { description: "The configuration for an api", content: { "application/json": { schema: z.object({ remaining: z.number().int().nullable().openapi({ description: "The number of remaining requests for this key after updating it. `null` means unlimited.", example: 100, }), }), }, }, }, ...openApiErrorResponses, }, }); export type Route = typeof route; export type V1KeysUpdateRemainingRequest = z.infer< (typeof route.request.body.content)["application/json"]["schema"] >; export type V1KeysUpdateRemainingResponse = z.infer< (typeof route.responses)[200]["content"]["application/json"]["schema"] >; export const registerV1KeysUpdateRemaining = (app: App) => app.openapi(route, async (c) => { const req = c.req.valid("json"); const { cache, db, usageLimiter, analytics } = c.get("services"); const key = await db.query.keys.findFirst({ where: (table, { eq }) => eq(table.id, req.keyId), with: { keyAuth: { with: { api: true, }, }, }, }); if (!key) { throw new UnkeyApiError({ code: "NOT_FOUND", message: `key ${req.keyId} not found` }); } const auth = await rootKeyAuth( c, buildUnkeyQuery(({ or }) => or("*", "api.*.update_key", `api.${key.keyAuth.api.id}.update_key`), ), ); if (key.workspaceId !== auth.authorizedWorkspaceId) { throw new UnkeyApiError({ code: "NOT_FOUND", message: `key ${req.keyId} not found` }); } const authorizedWorkspaceId = auth.authorizedWorkspaceId; const rootKeyId = auth.key.id; switch (req.op) { case "increment": { if (key.remaining === null) { throw new UnkeyApiError({ code: "BAD_REQUEST", message: "cannot increment a key with unlimited remaining requests, please 'set' a value instead.", }); } if (req.value === null) { throw new UnkeyApiError({ code: "BAD_REQUEST", message: "cannot increment a key by null.", }); } await db .update(schema.keys) .set({ remaining: sql`remaining_requests + ${req.value}`, }) .where(eq(schema.keys.id, req.keyId)); break; } case "decrement": { if (key.remaining === null) { throw new UnkeyApiError({ code: "BAD_REQUEST", message: "cannot decrement a key with unlimited remaining requests, please 'set' a value instead.", }); } if (req.value === null) { throw new UnkeyApiError({ code: "BAD_REQUEST", message: "cannot decrement a key by null.", }); } await db .update(schema.keys) .set({ remaining: sql`remaining_requests - ${req.value}`, }) .where(eq(schema.keys.id, req.keyId)); break; } case "set": { await db .update(schema.keys) .set({ remaining: req.value, }) .where(eq(schema.keys.id, req.keyId)); break; } } await Promise.all([ usageLimiter.revalidate({ keyId: key.id }), cache.remove(c, "keyByHash", key.hash), cache.remove(c, "keyById", key.id), ]); const keyAfterUpdate = await db.query.keys.findFirst({ where: (table, { eq }) => eq(table.id, req.keyId), }); if (!keyAfterUpdate) { throw new UnkeyApiError({ code: "INTERNAL_SERVER_ERROR", message: "key not found after update, this should not happen", }); } await analytics.ingestAuditLogs({ actor: { type: "key", id: rootKeyId, }, event: "key.update", workspaceId: authorizedWorkspaceId, description: `Changed remaining to ${keyAfterUpdate.remaining}`, resources: [ { type: "keyAuth", id: key.keyAuthId, }, { type: "key", id: key.id, }, ], context: { location: c.get("location"), userAgent: c.get("userAgent"), }, }); return c.json({ remaining: keyAfterUpdate.remaining, }); });