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
34 changes: 34 additions & 0 deletions apps/api/src/routes/v1_keys_deleteKey.happy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,37 @@ test("soft deletes key", async (t) => {
expect(found!.deletedAt).toBeDefined();
expect(found!.deletedAt!.getTime() - Date.now()).toBeLessThan(10_000); // 10s play});
});

test("hard deletes key", async (t) => {
const h = await IntegrationHarness.init(t);
const keyId = newId("test");
const key = new KeyV1({ prefix: "test", byteLength: 16 }).toString();
await h.db.primary.insert(schema.keys).values({
id: keyId,
keyAuthId: h.resources.userKeyAuth.id,
hash: await sha256(key),
start: key.slice(0, 8),
workspaceId: h.resources.userWorkspace.id,
createdAt: new Date(),
});

const root = await h.createRootKey([`api.${h.resources.userApi.id}.delete_key`]);
const res = await h.post<V1KeysDeleteKeyRequest, V1KeysDeleteKeyResponse>({
url: "/v1/keys.deleteKey",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${root.key}`,
},
body: {
keyId,
permanent: true,
},
});

expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200);

const found = await h.db.primary.query.keys.findFirst({
where: (table, { eq }) => eq(table.id, keyId),
});
expect(found).toBeUndefined();
});
17 changes: 14 additions & 3 deletions apps/api/src/routes/v1_keys_deleteKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ const route = createRoute({
description: "The id of the key to revoke",
example: "key_1234",
}),
permanent: z.boolean().default(false).optional().openapi({
description:
"By default Unkey soft deletes keys, so they may be recovered later. If you want to permanently delete it, set permanent=true. This might be necessary if you run into NOT_UNIQUE errors during key migration.",
}),
}),
},
},
Expand Down Expand Up @@ -53,7 +57,7 @@ export type V1KeysDeleteKeyResponse = z.infer<

export const registerV1KeysDeleteKey = (app: App) =>
app.openapi(route, async (c) => {
const { keyId } = c.req.valid("json");
const { keyId, permanent } = c.req.valid("json");
const { cache, db } = c.get("services");

const data = await cache.keyById.swr(keyId, async () => {
Expand Down Expand Up @@ -124,7 +128,14 @@ export const registerV1KeysDeleteKey = (app: App) =>
const rootKeyId = auth.key.id;

await db.primary.transaction(async (tx) => {
await tx.update(schema.keys).set({ deletedAt: new Date() }).where(eq(schema.keys.id, key.id));
if (permanent) {
await tx.delete(schema.keys).where(eq(schema.keys.id, key.id));
} else {
await tx
.update(schema.keys)
.set({ deletedAt: new Date() })
.where(eq(schema.keys.id, key.id));
}

await insertUnkeyAuditLog(c, tx, {
workspaceId: authorizedWorkspaceId,
Expand All @@ -133,7 +144,7 @@ export const registerV1KeysDeleteKey = (app: App) =>
type: "key",
id: rootKeyId,
},
description: `Deleted ${key.id}`,
description: `${permanent ? "Permanently deleted" : "Deleted"} ${key.id}`,
resources: [
{
type: "key",
Expand Down