diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 0c2503067e..768f1b8e4d 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -16,7 +16,7 @@ jobs: uses: ./.github/actions/install with: ts: true - + - name: Build run: pnpm turbo run build --filter=./apps/api @@ -31,11 +31,11 @@ jobs: EOF working-directory: apps/api - name: Run worker - run: pnpm dev > api.logs & + run: pnpm dev & sleep 15 working-directory: apps/api - - - + + + - name: Load Schema into MySQL run: pnpm drizzle-kit push @@ -43,8 +43,8 @@ jobs: env: DRIZZLE_DATABASE_URL: "mysql://unkey:password@localhost:3306/unkey" - - + + - name: Build run: pnpm build env: @@ -57,6 +57,3 @@ jobs: UNKEY_WEBHOOK_KEYS_API_ID: "not-empty" AGENT_URL: "http://localhost:8080" AGENT_TOKEN: "not-empty" - - - diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 151c772ace..ba3efc3d74 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -12,8 +12,6 @@ jobs: uses: ./.github/workflows/test_agent_local.yaml build_agent_image: - needs: - - agent_local_test uses: ./.github/workflows/job_build_agent_image.yaml secrets: GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} @@ -41,7 +39,6 @@ jobs: agent_staging_deployment: needs: - - agent_local_test - build_agent_image uses: ./.github/workflows/job_deploy_agent_staging.yaml secrets: diff --git a/.github/workflows/job_test_api_local.yaml b/.github/workflows/job_test_api_local.yaml index 6ba536ddf8..3cffc06e98 100644 --- a/.github/workflows/job_test_api_local.yaml +++ b/.github/workflows/job_test_api_local.yaml @@ -10,6 +10,10 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Delete huge unnecessary tools folder + run: rm -rf /opt/hostedtoolcache + + - name: Run containers run: docker compose -f ./deployment/docker-compose.yaml up -d diff --git a/.github/workflows/unit_test.yaml b/.github/workflows/job_test_unit.yaml similarity index 100% rename from .github/workflows/unit_test.yaml rename to .github/workflows/job_test_unit.yaml diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index d3302a778c..d32411542b 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -10,7 +10,7 @@ on: jobs: test_packages: name: Test Packages - uses: ./.github/workflows/unit_test.yaml + uses: ./.github/workflows/job_test_unit.yaml build: name: Build diff --git a/apps/api/src/pkg/keys/service.ts b/apps/api/src/pkg/keys/service.ts index f1b349b73d..366fb802b7 100644 --- a/apps/api/src/pkg/keys/service.ts +++ b/apps/api/src/pkg/keys/service.ts @@ -385,9 +385,6 @@ export class KeyService { return Ok({ valid: false, code: "NOT_FOUND" }); } - this.logger.info("data from cache or db", { - data, - }); // Quick fix if (!data.workspace) { this.logger.warn("workspace not found, trying again", { diff --git a/apps/api/src/routes/v1_keys_getVerifications.ts b/apps/api/src/routes/v1_keys_getVerifications.ts index d4d593dfe3..cd95b50a5c 100644 --- a/apps/api/src/routes/v1_keys_getVerifications.ts +++ b/apps/api/src/routes/v1_keys_getVerifications.ts @@ -80,7 +80,7 @@ export const registerV1KeysGetVerifications = (app: App) => app.openapi(route, async (c) => { const { keyId, ownerId, start, end } = c.req.valid("query"); - const { analytics, cache, db } = c.get("services"); + const { analytics, cache, db, logger } = c.get("services"); const ids: { keyId: string; @@ -240,6 +240,10 @@ export const registerV1KeysGetVerifications = (app: App) => [time: number]: { success: number; rateLimited: number; usageExceeded: number }; } = {}; for (const dataPoint of verificationsFromAllKeys) { + if (dataPoint.err) { + logger.error(dataPoint.err.message); + continue; + } for (const d of dataPoint.val!) { if (!verifications[d.time]) { verifications[d.time] = { success: 0, rateLimited: 0, usageExceeded: 0 }; diff --git a/apps/api/src/routes/v1_ratelimits_deleteOverride.error.test.ts b/apps/api/src/routes/v1_ratelimits_deleteOverride.error.test.ts new file mode 100644 index 0000000000..3dab832f8c --- /dev/null +++ b/apps/api/src/routes/v1_ratelimits_deleteOverride.error.test.ts @@ -0,0 +1,57 @@ +import { expect, test } from "vitest"; + +import { randomUUID } from "node:crypto"; +import { IntegrationHarness } from "src/pkg/testutil/integration-harness"; + +import { schema } from "@unkey/db"; +import { newId } from "@unkey/id"; +import type { + V1RatelimitDeleteOverrideRequest, + V1RatelimitDeleteOverrideResponse, +} from "./v1_ratelimits_deleteOverride"; + +test("Missing Namespace", async (t) => { + const h = await IntegrationHarness.init(t); + + const overrideId = newId("test"); + const identifier = randomUUID(); + const namespaceId = newId("test"); + const namespace = { + id: namespaceId, + workspaceId: h.resources.userWorkspace.id, + createdAt: new Date(), + name: newId("test"), + }; + await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace); + + await h.db.primary.insert(schema.ratelimitOverrides).values({ + id: overrideId, + workspaceId: h.resources.userWorkspace.id, + namespaceId, + identifier, + limit: 1, + duration: 60_000, + async: false, + }); + + const root = await h.createRootKey(["ratelimit.*.delete_override"]); + const res = await h.post({ + url: "/v1/ratelimits.deleteOverride", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + body: { + identifier, + }, + }); + + expect(res.status, `expected 400, received: ${JSON.stringify(res, null, 2)}`).toBe(400); + expect(res.body).toMatchObject({ + error: { + code: "BAD_REQUEST", + docs: "https://unkey.dev/docs/api-reference/errors/code/BAD_REQUEST", + message: "You must provide a namespaceId or a namespaceName", + }, + }); +}); diff --git a/apps/api/src/routes/v1_ratelimits_deleteOverride.happy.test.ts b/apps/api/src/routes/v1_ratelimits_deleteOverride.happy.test.ts new file mode 100644 index 0000000000..a2bbf6b939 --- /dev/null +++ b/apps/api/src/routes/v1_ratelimits_deleteOverride.happy.test.ts @@ -0,0 +1,56 @@ +import { expect, test } from "vitest"; + +import { randomUUID } from "node:crypto"; +import { IntegrationHarness } from "src/pkg/testutil/integration-harness"; + +import { isNull, schema } from "@unkey/db"; +import { newId } from "@unkey/id"; +import type { + V1RatelimitDeleteOverrideRequest, + V1RatelimitDeleteOverrideResponse, +} from "./v1_ratelimits_deleteOverride"; + +test("deletes override", async (t) => { + const h = await IntegrationHarness.init(t); + + const overrideId = newId("test"); + const identifier = randomUUID(); + const namespaceId = newId("test"); + const namespace = { + id: namespaceId, + workspaceId: h.resources.userWorkspace.id, + createdAt: new Date(), + name: newId("test"), + }; + await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace); + + await h.db.primary.insert(schema.ratelimitOverrides).values({ + id: overrideId, + workspaceId: h.resources.userWorkspace.id, + namespaceId, + identifier, + limit: 1, + duration: 60_000, + async: false, + }); + + const root = await h.createRootKey(["ratelimit.*.delete_override"]); + const res = await h.post({ + url: "/v1/ratelimits.deleteOverride", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + body: { + namespaceId, + identifier, + }, + }); + + expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200); + + const found = await h.db.primary.query.ratelimitOverrides.findFirst({ + where: (table, { eq, and }) => and(eq(table.id, overrideId), isNull(table.deletedAt)), + }); + expect(found).toBeUndefined(); +}); diff --git a/apps/api/src/routes/v1_ratelimits_deleteOverride.security.test.ts b/apps/api/src/routes/v1_ratelimits_deleteOverride.security.test.ts new file mode 100644 index 0000000000..0b481f5c90 --- /dev/null +++ b/apps/api/src/routes/v1_ratelimits_deleteOverride.security.test.ts @@ -0,0 +1,158 @@ +import { randomUUID } from "node:crypto"; +import { runCommonRouteTests } from "@/pkg/testutil/common-tests"; +import { IntegrationHarness } from "@/pkg/testutil/integration-harness"; +import { schema } from "@unkey/db"; +import { newId } from "@unkey/id"; +import { describe, expect, test } from "vitest"; +import type { + V1RatelimitDeleteOverrideRequest, + V1RatelimitDeleteOverrideResponse, +} from "./v1_ratelimits_deleteOverride"; + +runCommonRouteTests({ + prepareRequest: async (rh) => { + const overrideId = newId("test"); + const identifier = randomUUID(); + const namespaceId = newId("test"); + const namespace = { + id: namespaceId, + workspaceId: rh.resources.userWorkspace.id, + createdAt: new Date(), + name: newId("test"), + }; + await rh.db.primary.insert(schema.ratelimitNamespaces).values(namespace); + await rh.db.primary.insert(schema.ratelimitOverrides).values({ + id: overrideId, + workspaceId: rh.resources.userWorkspace.id, + namespaceId, + identifier, + limit: 1, + duration: 60_000, + async: false, + }); + + return { + method: "POST", + url: "/v1/ratelimits.deleteOverride", + headers: { + "Content-Type": "application/json", + }, + body: { + namespaceId, + identifier, + }, + }; + }, +}); +describe("correct roles", () => { + describe.each([{ name: "delete override", roles: ["ratelimit.*.delete_override"] }])( + "$name", + ({ roles }) => { + test("returns 200", async (t) => { + const h = await IntegrationHarness.init(t); + const overrideId = newId("test"); + const identifier = randomUUID(); + const namespaceId = newId("test"); + const namespace = { + id: namespaceId, + workspaceId: h.resources.userWorkspace.id, + createdAt: new Date(), + name: newId("test"), + }; + await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace); + await h.db.primary.insert(schema.ratelimitOverrides).values({ + id: overrideId, + workspaceId: h.resources.userWorkspace.id, + namespaceId, + identifier, + limit: 1, + duration: 60_000, + async: false, + }); + const root = await h.createRootKey(roles); + + const res = await h.post< + V1RatelimitDeleteOverrideRequest, + V1RatelimitDeleteOverrideResponse + >({ + url: "/v1/ratelimits.deleteOverride", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + body: { + namespaceId, + identifier, + }, + }); + + expect( + res.status, + `expected status 200, received: ${JSON.stringify(res, null, 2)}`, + ).toEqual(200); + + const found = await h.db.primary.query.ratelimitOverrides.findFirst({ + where: (table, { eq, and, isNull }) => + and(isNull(table.deletedAt), eq(table.id, overrideId)), + }); + expect(found).toBeUndefined(); + }); + }, + ); +}); + +describe("incorrect roles", () => { + describe.each([{ name: "delete override", roles: ["ratelimit.*.create_override"] }])( + "$name", + ({ roles }) => { + test("returns 403", async (t) => { + const h = await IntegrationHarness.init(t); + const overrideId = newId("test"); + const identifier = randomUUID(); + const namespaceId = newId("test"); + const namespace = { + id: namespaceId, + workspaceId: h.resources.userWorkspace.id, + createdAt: new Date(), + name: newId("test"), + }; + await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace); + await h.db.primary.insert(schema.ratelimitOverrides).values({ + id: overrideId, + workspaceId: h.resources.userWorkspace.id, + namespaceId, + identifier, + limit: 1, + duration: 60_000, + async: false, + }); + const root = await h.createRootKey(roles); + + const res = await h.post< + V1RatelimitDeleteOverrideRequest, + V1RatelimitDeleteOverrideResponse + >({ + url: "/v1/ratelimits.deleteOverride", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + body: { + namespaceId, + identifier, + }, + }); + + expect( + res.status, + `expected status 403, received: ${JSON.stringify(res, null, 2)}`, + ).toEqual(403); + + const found = await h.db.primary.query.ratelimitOverrides.findFirst({ + where: (table, { eq }) => eq(table.id, overrideId), + }); + expect(found?.id).toEqual(overrideId); + }); + }, + ); +}); diff --git a/apps/api/src/routes/v1_ratelimits_deleteOverride.ts b/apps/api/src/routes/v1_ratelimits_deleteOverride.ts new file mode 100644 index 0000000000..f902d10d55 --- /dev/null +++ b/apps/api/src/routes/v1_ratelimits_deleteOverride.ts @@ -0,0 +1,133 @@ +import { insertUnkeyAuditLog } from "@/pkg/audit"; +import { rootKeyAuth } from "@/pkg/auth/root_key"; +import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; +import type { App } from "@/pkg/hono/app"; +import { createRoute, z } from "@hono/zod-openapi"; +import { eq, schema } from "@unkey/db"; +import { buildUnkeyQuery } from "@unkey/rbac"; + +const route = createRoute({ + tags: ["ratelimits"], + operationId: "deleteOverride", + method: "post", + path: "/v1/ratelimits.deleteOverride", + security: [{ bearerAuth: [] }], + request: { + body: { + required: true, + content: { + "application/json": { + schema: z.object({ + namespaceId: z.string().optional().openapi({ + description: + "The id of the namespace. Either namespaceId or namespaceName must be provided", + example: "rlns_1234", + }), + namespaceName: z.string().optional().openapi({ + description: + "The name of the namespace. Namespaces group different limits together for better analytics. You might have a namespace for your public API and one for internal tRPC routes.", + example: "email.outbound", + }), + identifier: z.string().openapi({ + description: + "Identifier of your user, this can be their userId, an email, an ip or anything else. Wildcards ( * ) can be used to match multiple identifiers, More info can be found at https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules", + example: "user_123", + }), + }), + }, + }, + }, + }, + responses: { + 200: { + description: "Successfully deleted a ratelimit override", + content: { + "application/json": { + schema: z.object({}), + }, + }, + }, + ...openApiErrorResponses, + }, +}); + +export type Route = typeof route; +export type V1RatelimitDeleteOverrideRequest = z.infer< + (typeof route.request.body.content)["application/json"]["schema"] +>; +export type V1RatelimitDeleteOverrideResponse = z.infer< + (typeof route.responses)[200]["content"]["application/json"]["schema"] +>; + +export const registerV1RatelimitDeleteOverride = (app: App) => + app.openapi(route, async (c) => { + const { namespaceId, namespaceName, identifier } = c.req.valid("json"); + const auth = await rootKeyAuth( + c, + buildUnkeyQuery(({ or }) => or("*", "ratelimit.*.delete_override")), + ); + if (!namespaceId && !namespaceName) { + throw new UnkeyApiError({ + code: "BAD_REQUEST", + message: "You must provide a namespaceId or a namespaceName", + }); + } + const { db } = c.get("services"); + + const authorizedWorkspaceId = auth.authorizedWorkspaceId; + + await db.primary.transaction(async (tx) => { + const namespace = await db.primary.query.ratelimitNamespaces.findFirst({ + where: (table, { eq, and }) => + and( + eq(table.workspaceId, authorizedWorkspaceId), + namespaceId ? eq(table.id, namespaceId) : eq(table.name, namespaceName!), + ), + with: { + overrides: { + where: (table, { eq, and, isNull }) => + and(isNull(table.deletedAt), eq(table.identifier, identifier)), + }, + }, + }); + + if (!namespace) { + throw new UnkeyApiError({ + code: "NOT_FOUND", + message: `Namespace ${namespaceId ? namespaceId : namespaceName} not found`, + }); + } + const override = namespace.overrides.at(0); + + if (!override) { + throw new UnkeyApiError({ + code: "NOT_FOUND", + message: `Override with ${identifier} identifier not found`, + }); + } + await tx + .update(schema.ratelimitOverrides) + .set({ deletedAt: new Date() }) + .where(eq(schema.ratelimitOverrides.id, override.id)); + + await insertUnkeyAuditLog(c, tx, { + workspaceId: auth.authorizedWorkspaceId, + event: "ratelimit.delete_override", + actor: { + type: "key", + id: auth.key.id, + }, + description: `Deleted ratelimit override ${override.id}`, + resources: [ + { + type: "ratelimitOverride", + id: override.id, + }, + ], + + context: { location: c.get("location"), userAgent: c.get("userAgent") }, + }); + }); + + return c.json({}); + }); diff --git a/apps/api/src/routes/v1_ratelimits_getOverride.error.test.ts b/apps/api/src/routes/v1_ratelimits_getOverride.error.test.ts new file mode 100644 index 0000000000..a61d8e0cb7 --- /dev/null +++ b/apps/api/src/routes/v1_ratelimits_getOverride.error.test.ts @@ -0,0 +1,48 @@ +import { randomUUID } from "node:crypto"; +import { schema } from "@unkey/db"; +import { newId } from "@unkey/id"; +import { IntegrationHarness } from "src/pkg/testutil/integration-harness"; +import { expect, test } from "vitest"; +import type { V1RatelimitGetOverrideResponse } from "./v1_ratelimits_getOverride"; + +test("Missing Namespace", async (t) => { + const h = await IntegrationHarness.init(t); + const root = await h.createRootKey(["ratelimit.*.read_override"]); + const namespaceId = newId("test"); + const namespaceName = "Test.Name"; + const overrideId = newId("test"); + const identifier = randomUUID(); + + const namespace = { + id: namespaceId, + name: namespaceName, + workspaceId: h.resources.userWorkspace.id, + createdAt: new Date(), + }; + await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace); + + await h.db.primary.insert(schema.ratelimitOverrides).values({ + id: overrideId, + workspaceId: h.resources.userWorkspace.id, + namespaceId: namespaceId, + identifier: identifier, + limit: 1, + duration: 60_000, + async: false, + }); + + const res = await h.get({ + url: `/v1/ratelimits.getOverride?namespaceId=&identifier=${identifier}`, + headers: { + Authorization: `Bearer ${root.key}`, + }, + }); + expect(res.status, `expected 400, received: ${JSON.stringify(res, null, 2)}`).toBe(400); + expect(res.body).toMatchObject({ + error: { + code: "BAD_REQUEST", + docs: "https://unkey.dev/docs/api-reference/errors/code/BAD_REQUEST", + message: "You must provide a namespaceId or a namespaceName", + }, + }); +}); diff --git a/apps/api/src/routes/v1_ratelimits_getOverride.happy.test.ts b/apps/api/src/routes/v1_ratelimits_getOverride.happy.test.ts new file mode 100644 index 0000000000..0035b1248e --- /dev/null +++ b/apps/api/src/routes/v1_ratelimits_getOverride.happy.test.ts @@ -0,0 +1,89 @@ +import { randomUUID } from "node:crypto"; +import { schema } from "@unkey/db"; +import { newId } from "@unkey/id"; +import { IntegrationHarness } from "src/pkg/testutil/integration-harness"; +import { expect, test } from "vitest"; +import type { V1RatelimitGetOverrideResponse } from "./v1_ratelimits_getOverride"; + +test("return a single override using namespaceId", async (t) => { + const h = await IntegrationHarness.init(t); + const root = await h.createRootKey(["ratelimit.*.read_override"]); + const namespaceId = newId("test"); + const namespaceName = randomUUID(); + const overrideId = newId("test"); + const identifier = randomUUID(); + + // Namespace + const namespace = { + id: namespaceId, + name: namespaceName, + workspaceId: h.resources.userWorkspace.id, + createdAt: new Date(), + }; + await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace); + // Initial Override + await h.db.primary.insert(schema.ratelimitOverrides).values({ + id: overrideId, + workspaceId: h.resources.userWorkspace.id, + namespaceId: namespaceId, + identifier: identifier, + limit: 1, + duration: 60_000, + async: false, + }); + + const res = await h.get({ + url: `/v1/ratelimits.getOverride?namespaceId=${namespaceId}&identifier=${identifier}`, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + }); + expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200); + expect(res.body.id).toBe(overrideId); + expect(res.body.identifier).toEqual(identifier); + expect(res.body.limit).toEqual(1); + expect(res.body.duration).toEqual(60_000); + expect(res.body.async).toEqual(false); +}); + +test("return a single override using namespaceName", async (t) => { + const h = await IntegrationHarness.init(t); + const root = await h.createRootKey(["ratelimit.*.read_override"]); + const namespaceId = newId("test"); + const namespaceName = randomUUID(); + const overrideId = newId("test"); + const identifier = randomUUID(); + + // Namespace + const namespace = { + id: namespaceId, + name: namespaceName, + workspaceId: h.resources.userWorkspace.id, + createdAt: new Date(), + }; + await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace); + await h.db.primary.insert(schema.ratelimitOverrides).values({ + id: overrideId, + workspaceId: h.resources.userWorkspace.id, + namespaceId: namespaceId, + identifier: identifier, + limit: 1, + duration: 60_000, + async: false, + }); + + const res = await h.get({ + url: `/v1/ratelimits.getOverride?namespaceName=${namespaceName}&identifier=${identifier}`, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + }); + expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200); + expect(res.body.id).toBe(overrideId); + expect(res.body.identifier).toEqual(identifier); + expect(res.body.limit).toEqual(1); + expect(res.body.duration).toEqual(60_000); + expect(res.body.async).toEqual(false); +}); diff --git a/apps/api/src/routes/v1_ratelimits_getOverride.security.test.ts b/apps/api/src/routes/v1_ratelimits_getOverride.security.test.ts new file mode 100644 index 0000000000..238bd251d1 --- /dev/null +++ b/apps/api/src/routes/v1_ratelimits_getOverride.security.test.ts @@ -0,0 +1,123 @@ +import { randomUUID } from "node:crypto"; +import { runCommonRouteTests } from "@/pkg/testutil/common-tests"; +import { IntegrationHarness } from "@/pkg/testutil/integration-harness"; +import { schema } from "@unkey/db"; +import { newId } from "@unkey/id"; +import { describe, expect, test } from "vitest"; +import type { + V1RatelimitGetOverrideRequest, + V1RatelimitGetOverrideResponse, +} from "./v1_ratelimits_getOverride"; + +runCommonRouteTests({ + prepareRequest: async (rh) => { + const overrideId = newId("test"); + const identifier = randomUUID(); + const namespaceId = newId("test"); + const namespace = { + id: namespaceId, + workspaceId: rh.resources.userWorkspace.id, + createdAt: new Date(), + name: newId("test"), + }; + await rh.db.primary.insert(schema.ratelimitNamespaces).values(namespace); + await rh.db.primary.insert(schema.ratelimitOverrides).values({ + id: overrideId, + workspaceId: rh.resources.userWorkspace.id, + namespaceId, + identifier, + limit: 1, + duration: 60_000, + async: false, + }); + + return { + method: "GET", + url: `/v1/ratelimits.getOverride?namespaceId=${namespaceId}&identifier=${identifier}`, + headers: { + "Content-Type": "application/json", + }, + }; + }, +}); +describe("correct roles", () => { + describe.each([{ name: "get override", roles: ["ratelimit.*.read_override"] }])( + "$name", + ({ roles }) => { + test("returns 200", async (t) => { + const h = await IntegrationHarness.init(t); + const overrideId = newId("test"); + const identifier = randomUUID(); + const namespaceId = newId("test"); + const namespace = { + id: namespaceId, + workspaceId: h.resources.userWorkspace.id, + createdAt: new Date(), + name: randomUUID(), + }; + await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace); + await h.db.primary.insert(schema.ratelimitOverrides).values({ + id: overrideId, + workspaceId: h.resources.userWorkspace.id, + namespaceId, + identifier, + limit: 1, + duration: 60_000, + async: false, + }); + + const root = await h.createRootKey(roles); + const res = await h.get({ + url: `/v1/ratelimits.getOverride?namespaceId=${namespaceId}&identifier=${identifier}`, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + }); + + expect( + res.status, + `expected status 200, received: ${JSON.stringify(res, null, 2)}`, + ).toEqual(200); + }); + }, + ); +}); + +describe("incorrect roles", () => { + describe.each([{ name: "get override", roles: [] }])("$name", ({ roles }) => { + test("returns 403", async (t) => { + const h = await IntegrationHarness.init(t); + const overrideId = newId("test"); + const identifier = randomUUID(); + const namespaceId = newId("test"); + const namespace = { + id: namespaceId, + workspaceId: h.resources.userWorkspace.id, + createdAt: new Date(), + name: randomUUID(), + }; + await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace); + await h.db.primary.insert(schema.ratelimitOverrides).values({ + id: overrideId, + workspaceId: h.resources.userWorkspace.id, + namespaceId, + identifier, + limit: 1, + duration: 60_000, + async: false, + }); + const root = await h.createRootKey(roles); + const res = await h.get({ + url: `/v1/ratelimits.getOverride?namespaceId=${namespaceId}&identifier=${identifier}`, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + }); + expect(res.status, `expected status 403, received: ${JSON.stringify(res, null, 2)}`).toEqual( + 403, + ); + }); + }); +}); diff --git a/apps/api/src/routes/v1_ratelimits_getOverride.ts b/apps/api/src/routes/v1_ratelimits_getOverride.ts new file mode 100644 index 0000000000..38b7be9580 --- /dev/null +++ b/apps/api/src/routes/v1_ratelimits_getOverride.ts @@ -0,0 +1,110 @@ +import { rootKeyAuth } from "@/pkg/auth/root_key"; +import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; +import type { App } from "@/pkg/hono/app"; +import { createRoute, z } from "@hono/zod-openapi"; + +import { buildUnkeyQuery } from "@unkey/rbac"; + +const route = createRoute({ + tags: ["ratelimit"], + operationId: "getOverride", + method: "get", + path: "/v1/ratelimits.getOverride", + security: [{ bearerAuth: [] }], + request: { + query: z.object({ + namespaceId: z.string().optional().openapi({ + description: + "The id of the namespace. Either namespaceId or namespaceName must be provided", + example: "rlns_1234", + }), + namespaceName: z.string().optional().openapi({ + description: + "Namespaces group different limits together for better analytics. You might have a namespace for your public API and one for internal tRPC routes. Wildcards can also be used, more info can be found at https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules", + example: "email.outbound", + }), + identifier: z.string().openapi({ + description: + "Identifier of your user, this can be their userId, an email, an ip or anything else. Wildcards ( * ) can be used to match multiple identifiers, More info can be found at https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules", + example: "user_123", + }), + }), + }, + responses: { + 200: { + description: "Details of the override for the given identifier", + content: { + "application/json": { + schema: z.object({ + id: z.string(), + identifier: z.string(), + limit: z.number().int(), + duration: z.number().int(), + async: z.boolean().nullable().optional(), + }), + }, + }, + }, + ...openApiErrorResponses, + }, +}); + +export type Route = typeof route; + +export type V1RatelimitGetOverrideResponse = z.infer< + (typeof route.responses)[200]["content"]["application/json"]["schema"] +>; +export type V1RatelimitGetOverrideRequest = z.infer<(typeof route.request)["query"]>; +export const registerV1RatelimitGetOverride = (app: App) => + app.openapi(route, async (c) => { + const { namespaceId, namespaceName, identifier } = c.req.valid("query"); + const { db } = c.get("services"); + + const auth = await rootKeyAuth( + c, + buildUnkeyQuery(({ or }) => or("ratelimit.*.read_override")), + ); + + const authorizedWorkspaceId = auth.authorizedWorkspaceId; + if (!authorizedWorkspaceId) { + throw new UnkeyApiError({ + code: "UNAUTHORIZED", + message: "Missing required permission: ratelimit.*.read_override", + }); + } + if (!namespaceId && !namespaceName) { + throw new UnkeyApiError({ + code: "BAD_REQUEST", + message: "You must provide a namespaceId or a namespaceName", + }); + } + const namespace = await db.primary.query.ratelimitNamespaces.findFirst({ + where: (table, { eq, and }) => + and( + eq(table.workspaceId, authorizedWorkspaceId), + namespaceId ? eq(table.id, namespaceId) : eq(table.name, namespaceName!), + ), + with: { + overrides: { + where: (table, { eq, and, isNull }) => + and(isNull(table.deletedAt), eq(table.identifier, identifier)), + }, + }, + }); + + if (!namespace) { + throw new UnkeyApiError({ code: "NOT_FOUND", message: "Namespace not found" }); + } + + const override = namespace.overrides.at(0); + if (!override) { + throw new UnkeyApiError({ code: "NOT_FOUND", message: "Override not found" }); + } + return c.json({ + id: override.id, + identifier: override.identifier, + limit: override.limit, + duration: override.duration, + async: override.async, + }); + }); diff --git a/apps/api/src/routes/v1_ratelimit_limit.accuracy.test.ts b/apps/api/src/routes/v1_ratelimits_limit.accuracy.test.ts similarity index 98% rename from apps/api/src/routes/v1_ratelimit_limit.accuracy.test.ts rename to apps/api/src/routes/v1_ratelimits_limit.accuracy.test.ts index b4a5a9a734..1de1dcb162 100644 --- a/apps/api/src/routes/v1_ratelimit_limit.accuracy.test.ts +++ b/apps/api/src/routes/v1_ratelimits_limit.accuracy.test.ts @@ -6,7 +6,7 @@ import { schema } from "@unkey/db"; import { newId } from "@unkey/id"; import { IntegrationHarness } from "src/pkg/testutil/integration-harness"; -import type { V1RatelimitLimitRequest, V1RatelimitLimitResponse } from "./v1_ratelimit_limit"; +import type { V1RatelimitLimitRequest, V1RatelimitLimitResponse } from "./v1_ratelimits_limit"; /** * As a rule of thumb, the test duration (seconds) should be at least 10x the duration of the rate limit window diff --git a/apps/api/src/routes/v1_ratelimit_limit.consistency.test.ts.skipped b/apps/api/src/routes/v1_ratelimits_limit.consistency.test.ts.skipped similarity index 98% rename from apps/api/src/routes/v1_ratelimit_limit.consistency.test.ts.skipped rename to apps/api/src/routes/v1_ratelimits_limit.consistency.test.ts.skipped index ecd7edf0ba..76f8d3d9aa 100644 --- a/apps/api/src/routes/v1_ratelimit_limit.consistency.test.ts.skipped +++ b/apps/api/src/routes/v1_ratelimits_limit.consistency.test.ts.skipped @@ -4,7 +4,7 @@ import { randomUUID } from "node:crypto"; import { IntegrationHarness } from "@/pkg/testutil/integration-harness"; import { schema } from "@unkey/db"; import { newId } from "@unkey/id"; -import type { V1RatelimitLimitRequest, V1RatelimitLimitResponse } from "./v1_ratelimit_limit"; +import type { V1RatelimitLimitRequest, V1RatelimitLimitResponse } from "./v1_ratelimits_limit"; describe.each<{ limit: number; duration: number; n: number }>([ { limit: 10, duration: 1_000, n: 100 }, diff --git a/apps/api/src/routes/v1_ratelimit_limit.happy.test.ts b/apps/api/src/routes/v1_ratelimits_limit.happy.test.ts similarity index 97% rename from apps/api/src/routes/v1_ratelimit_limit.happy.test.ts rename to apps/api/src/routes/v1_ratelimits_limit.happy.test.ts index 28c255f597..a07d1f242b 100644 --- a/apps/api/src/routes/v1_ratelimit_limit.happy.test.ts +++ b/apps/api/src/routes/v1_ratelimits_limit.happy.test.ts @@ -4,7 +4,7 @@ import { randomUUID } from "node:crypto"; import { IntegrationHarness } from "@/pkg/testutil/integration-harness"; import { schema } from "@unkey/db"; import { newId } from "@unkey/id"; -import type { V1RatelimitLimitRequest, V1RatelimitLimitResponse } from "./v1_ratelimit_limit"; +import type { V1RatelimitLimitRequest, V1RatelimitLimitResponse } from "./v1_ratelimits_limit"; test("setting cost=0 returns the limit without modifying", async (t) => { const h = await IntegrationHarness.init(t); const namespace = { diff --git a/apps/api/src/routes/v1_ratelimit_limit.overrides.test.ts b/apps/api/src/routes/v1_ratelimits_limit.overrides.test.ts similarity index 99% rename from apps/api/src/routes/v1_ratelimit_limit.overrides.test.ts rename to apps/api/src/routes/v1_ratelimits_limit.overrides.test.ts index 3ac8190b56..2f9228e4fd 100644 --- a/apps/api/src/routes/v1_ratelimit_limit.overrides.test.ts +++ b/apps/api/src/routes/v1_ratelimits_limit.overrides.test.ts @@ -4,7 +4,7 @@ import { randomUUID } from "node:crypto"; import { IntegrationHarness } from "@/pkg/testutil/integration-harness"; import { schema } from "@unkey/db"; import { newId } from "@unkey/id"; -import type { V1RatelimitLimitRequest, V1RatelimitLimitResponse } from "./v1_ratelimit_limit"; +import type { V1RatelimitLimitRequest, V1RatelimitLimitResponse } from "./v1_ratelimits_limit"; describe("without override", () => { test("should use the hardcoded limit", async (t) => { diff --git a/apps/api/src/routes/v1_ratelimit_limit.ts b/apps/api/src/routes/v1_ratelimits_limit.ts similarity index 100% rename from apps/api/src/routes/v1_ratelimit_limit.ts rename to apps/api/src/routes/v1_ratelimits_limit.ts diff --git a/apps/api/src/routes/v1_ratelimits_listOverrides.error.test.ts b/apps/api/src/routes/v1_ratelimits_listOverrides.error.test.ts new file mode 100644 index 0000000000..15ab7d0d8a --- /dev/null +++ b/apps/api/src/routes/v1_ratelimits_listOverrides.error.test.ts @@ -0,0 +1,50 @@ +import { randomUUID } from "node:crypto"; +import { schema } from "@unkey/db"; +import { newId } from "@unkey/id"; +import { IntegrationHarness } from "src/pkg/testutil/integration-harness"; +import { expect, test } from "vitest"; +import type { V1RatelimitListOverridesResponse } from "./v1_ratelimits_listOverrides"; + +test("Missing Namespace", async (t) => { + const h = await IntegrationHarness.init(t); + const root = await h.createRootKey(["ratelimit.*.read_override"]); + const namespaceId = newId("test"); + const namespaceName = "test.Name"; + const overrideId = newId("test"); + const identifier = randomUUID(); + + // Namespace + const namespace = { + id: namespaceId, + name: namespaceName, + workspaceId: h.resources.userWorkspace.id, + createdAt: new Date(), + }; + await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace); + // Initial Override + await h.db.primary.insert(schema.ratelimitOverrides).values({ + id: overrideId, + workspaceId: h.resources.userWorkspace.id, + namespaceId: namespaceId, + identifier: identifier, + limit: 1, + duration: 60_000, + async: false, + }); + + const res = await h.get({ + url: `/v1/ratelimits.listOverrides?identifier=${identifier}`, + headers: { + Authorization: `Bearer ${root.key}`, + }, + }); + + expect(res.status, `expected 400, received: ${JSON.stringify(res, null, 2)}`).toBe(400); + expect(res.body).toMatchObject({ + error: { + code: "BAD_REQUEST", + docs: "https://unkey.dev/docs/api-reference/errors/code/BAD_REQUEST", + message: "You must provide a namespaceId or a namespaceName", + }, + }); +}); diff --git a/apps/api/src/routes/v1_ratelimits_listOverrides.happy.test.ts b/apps/api/src/routes/v1_ratelimits_listOverrides.happy.test.ts new file mode 100644 index 0000000000..0923267335 --- /dev/null +++ b/apps/api/src/routes/v1_ratelimits_listOverrides.happy.test.ts @@ -0,0 +1,111 @@ +import { randomUUID } from "node:crypto"; +import { schema } from "@unkey/db"; +import { newId } from "@unkey/id"; +import { IntegrationHarness } from "src/pkg/testutil/integration-harness"; +import { expect, test } from "vitest"; +import type { V1RatelimitListOverridesResponse } from "./v1_ratelimits_listOverrides"; + +// Test case for Multiple Overrides for the Same Namespace +test("return multiple overrides for the same namespace", async (t) => { + const h = await IntegrationHarness.init(t); + const root = await h.createRootKey(["ratelimit.*.read_override"]); + const namespaceId = newId("test"); + const namespaceName = randomUUID(); + + // Insert namespace + await h.db.primary.insert(schema.ratelimitNamespaces).values({ + id: namespaceId, + name: namespaceName, + workspaceId: h.resources.userWorkspace.id, + createdAt: new Date(), + }); + + // Insert multiple overrides + const overrides = [ + { + id: newId("test"), + workspaceId: h.resources.userWorkspace.id, + namespaceId: namespaceId, + identifier: randomUUID(), + limit: 1, + duration: 60_000, + async: false, + }, + { + id: newId("test"), + workspaceId: h.resources.userWorkspace.id, + namespaceId: namespaceId, + identifier: randomUUID(), + limit: 2, + duration: 120_000, + async: true, + }, + ]; + await h.db.primary.insert(schema.ratelimitOverrides).values(overrides); + + const res = await h.get({ + url: `/v1/ratelimits.listOverrides?namespaceId=${namespaceId}`, + headers: { + Authorization: `Bearer ${root.key}`, + }, + }); + + expect(res.status).toBe(200); + expect(res.body.total).toBe(2); + expect(res.body.overrides.length).toBe(2); +}); + +// Test case for No Overrides Found +test("return empty list when no overrides exist", async (t) => { + const h = await IntegrationHarness.init(t); + const root = await h.createRootKey(["ratelimit.*.read_override"]); + const namespaceId = newId("test"); + + // Insert namespace without overrides + await h.db.primary.insert(schema.ratelimitNamespaces).values({ + id: namespaceId, + name: randomUUID(), + workspaceId: h.resources.userWorkspace.id, + createdAt: new Date(), + }); + + const res = await h.get({ + url: `/v1/ratelimits.listOverrides?namespaceId=${namespaceId}`, + headers: { + Authorization: `Bearer ${root.key}`, + }, + }); + + expect(res.status).toBe(200); + expect(res.body.total).toBe(0); + expect(res.body.overrides.length).toBe(0); +}); + +// Test case for Invalid Identifier +test("return empty list when none exist", async (t) => { + const h = await IntegrationHarness.init(t); + const root = await h.createRootKey(["ratelimit.*.read_override"]); + const namespaceId = newId("test"); + const invalidIdentifier = randomUUID(); + + // Insert namespace + await h.db.primary.insert(schema.ratelimitNamespaces).values({ + id: namespaceId, + name: randomUUID(), + workspaceId: h.resources.userWorkspace.id, + createdAt: new Date(), + }); + + // Insert an override with a different identifier + + const res = await h.get({ + url: `/v1/ratelimits.listOverrides?namespaceId=${namespaceId}&identifier=${invalidIdentifier}`, + headers: { + Authorization: `Bearer ${root.key}`, + }, + }); + + expect(res.status).toBe(200); + expect(res.body.total).toBe(0); + expect(res.body.overrides.length).toBe(0); +}); diff --git a/apps/api/src/routes/v1_ratelimits_listOverrides.security.test.ts b/apps/api/src/routes/v1_ratelimits_listOverrides.security.test.ts new file mode 100644 index 0000000000..0e85ad61a9 --- /dev/null +++ b/apps/api/src/routes/v1_ratelimits_listOverrides.security.test.ts @@ -0,0 +1,153 @@ +import { randomUUID } from "node:crypto"; +import { runCommonRouteTests } from "@/pkg/testutil/common-tests"; +import { IntegrationHarness } from "@/pkg/testutil/integration-harness"; +import { schema } from "@unkey/db"; +import { newId } from "@unkey/id"; +import { describe, expect, test } from "vitest"; +import type { V1RatelimitGetOverrideResponse } from "./v1_ratelimits_getOverride"; +import type { + V1RatelimitListOverridesRequest, + V1RatelimitListOverridesResponse, +} from "./v1_ratelimits_listOverrides"; + +runCommonRouteTests({ + prepareRequest: async (rh) => { + const overrideId = newId("test"); + const identifier = randomUUID(); + const namespaceId = newId("test"); + const namespace = { + id: namespaceId, + workspaceId: rh.resources.userWorkspace.id, + createdAt: new Date(), + name: randomUUID(), + }; + await rh.db.primary.insert(schema.ratelimitNamespaces).values(namespace); + await rh.db.primary.insert(schema.ratelimitOverrides).values({ + id: overrideId, + workspaceId: rh.resources.userWorkspace.id, + namespaceId, + identifier, + limit: 1, + duration: 60_000, + async: false, + }); + + return { + method: "GET", + url: `/v1/ratelimits.listOverrides?namespaceId=${namespaceId}`, + headers: { + "Content-Type": "application/json", + }, + }; + }, +}); +describe("correct roles", () => { + describe.each([{ name: "list override", roles: ["ratelimit.*.read_override"] }])( + "$name", + ({ roles }) => { + test("returns 200", async (t) => { + const h = await IntegrationHarness.init(t); + const overrideId = newId("test"); + const identifier = randomUUID(); + const namespaceId = newId("test"); + const namespace = { + id: namespaceId, + workspaceId: h.resources.userWorkspace.id, + createdAt: new Date(), + name: randomUUID(), + }; + await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace); + await h.db.primary.insert(schema.ratelimitOverrides).values({ + id: overrideId, + workspaceId: h.resources.userWorkspace.id, + namespaceId, + identifier, + limit: 1, + duration: 60_000, + async: false, + }); + + const root = await h.createRootKey(roles); + const res = await h.get({ + url: `/v1/ratelimits.listOverrides?namespaceId=${namespaceId}`, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + }); + + expect( + res.status, + `expected status 200, received: ${JSON.stringify(res, null, 2)}`, + ).toEqual(200); + }); + }, + ); +}); + +describe("incorrect roles", () => { + describe.each([ + { name: "no roles", roles: [] }, + { name: "insufficient roles", roles: ["ratelimit.*.write_override"] }, + { name: "wrong namespace permissions", roles: ["ratelimit.othernamespace.read_override"] }, + { name: "expired token", roles: ["ratelimit.*.read_override"], tokenExpired: true }, + { name: "invalid token", roles: ["ratelimit.*.read_override"], tokenInvalid: true }, + ])("$name", ({ roles, tokenExpired, tokenInvalid }) => { + test("returns appropriate status code", async (t) => { + const h = await IntegrationHarness.init(t); + const overrideId = newId("test"); + const identifier = randomUUID(); + const namespaceId = newId("test"); + + // Insert namespace and override into the database + const namespace = { + id: namespaceId, + workspaceId: h.resources.userWorkspace.id, + createdAt: new Date(), + name: newId("test"), + }; + await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace); + await h.db.primary.insert(schema.ratelimitOverrides).values({ + id: overrideId, + workspaceId: h.resources.userWorkspace.id, + namespaceId, + identifier, + limit: 1, + duration: 60_000, + async: false, + }); + + // Create root key with specified roles and token conditions + const rootOptions: any = { roles }; + if (tokenExpired) { + rootOptions.expiresAt = new Date(Date.now() - 60 * 60 * 1000); // Set expiration in the past + } + if (tokenInvalid) { + rootOptions.key = "invalid_key"; // Use an invalid key + } + const root = await h.createRootKey(rootOptions); + + // Make the API request + const res = await h.get({ + url: `/v1/ratelimits.getOverride?namespaceId=${namespaceId}&identifier=${identifier}`, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + }); + + // Determine the expected status code based on the scenario + let expectedStatus = 200; + if ( + !roles.includes("ratelimit.*.read_override") || + tokenExpired || + tokenInvalid || + roles.includes("ratelimit.othernamespace.read_override") + ) { + expectedStatus = 403; + } + + expect(res.status).toEqual(expectedStatus); + }); + }); +}); diff --git a/apps/api/src/routes/v1_ratelimits_listOverrides.ts b/apps/api/src/routes/v1_ratelimits_listOverrides.ts new file mode 100644 index 0000000000..8c96f179d7 --- /dev/null +++ b/apps/api/src/routes/v1_ratelimits_listOverrides.ts @@ -0,0 +1,143 @@ +import { rootKeyAuth } from "@/pkg/auth/root_key"; +import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; +import type { App } from "@/pkg/hono/app"; +import { createRoute, z } from "@hono/zod-openapi"; +import { and, eq, gt, isNull, schema, sql } from "@unkey/db"; +import { buildUnkeyQuery } from "@unkey/rbac"; + +const route = createRoute({ + tags: ["ratelimit"], + operationId: "listOverrides", + method: "get", + path: "/v1/ratelimits.listOverrides", + security: [{ bearerAuth: [] }], + request: { + query: z.object({ + // Todo: Refine the descriptions and examples once working + namespaceId: z.string().optional().openapi({ + description: + "The id of the namespace. Either namespaceId or namespaceName must be provided", + example: "rlns_1234", + }), + namespaceName: z.string().optional().openapi({ + description: + "The name of the namespace. Namespaces group different limits together for better analytics. You might have a namespace for your public API and one for internal tRPC routes.", + example: "email.outbound", + }), + limit: z.coerce.number().int().min(1).max(100).optional().default(100).openapi({ + description: "The maximum number of keys to return", + example: 100, + }), + cursor: z.string().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.", + }), + }), + }, + responses: { + 200: { + description: "List of overrides for the given namespace.", + content: { + "application/json": { + schema: z.object({ + overrides: z.array( + z.object({ + id: z.string(), + identifier: z.string(), + limit: z.number().int(), + duration: z.number().int(), + async: z.boolean().nullable().optional(), + }), + ), + cursor: z.string().optional().openapi({ + description: + "The cursor to use for the next page of results, if no cursor is returned, there are no more results", + example: "eyJrZXkiOiJrZXlfMTIzNCJ9", + }), + total: z.number().int().openapi({ + description: "The total number of overrides for the namespace", + }), + }), + }, + }, + }, + ...openApiErrorResponses, + }, +}); + +export type Route = typeof route; +export type V1RatelimitListOverridesResponse = z.infer< + (typeof route.responses)[200]["content"]["application/json"]["schema"] +>; +export type V1RatelimitListOverridesRequest = z.infer<(typeof route.request)["query"]>; +export const registerV1RatelimitListOverrides = (app: App) => + app.openapi(route, async (c) => { + const { namespaceId, namespaceName, limit, cursor } = c.req.valid("query"); + const { db } = c.get("services"); + if (!namespaceId && !namespaceName) { + throw new UnkeyApiError({ + code: "BAD_REQUEST", + message: "You must provide a namespaceId or a namespaceName", + }); + } + const auth = await rootKeyAuth( + c, + buildUnkeyQuery(({ or }) => or("*", "ratelimit.*.read_override")), + ); + const authorizedWorkspaceId = auth.authorizedWorkspaceId; + if (!authorizedWorkspaceId) { + throw new UnkeyApiError({ + code: "UNAUTHORIZED", + message: "Missing required permission: ratelimit.*.read_override", + }); + } + + const namespace = await db.readonly.query.ratelimitNamespaces.findFirst({ + where: (table, { and, eq }) => + and( + eq(table.workspaceId, authorizedWorkspaceId), + namespaceId ? eq(table.id, namespaceId) : eq(table.name, namespaceName!), + ), + }); + if (!namespace) { + throw new Error(`Namespace ${namespaceId ? namespaceId : namespaceName} not found`); + } + + const [overrides, total] = await Promise.all([ + db.readonly.query.ratelimitOverrides.findMany({ + where: (table, { and, eq }) => + and( + ...[ + isNull(schema.ratelimitOverrides.deletedAt), + eq(table.workspaceId, authorizedWorkspaceId), + eq(table.namespaceId, namespace.id), + cursor ? gt(schema.ratelimitOverrides.id, cursor) : undefined, + ].filter(Boolean), + ), + limit: limit, + orderBy: schema.ratelimitOverrides.id, + }), + + db.readonly + .select({ count: sql`count(*)` }) + .from(schema.ratelimitOverrides) + .where( + and( + eq(schema.ratelimitOverrides.namespaceId, namespace?.id), + isNull(schema.ratelimitOverrides.deletedAt), + ), + ), + ]); + return c.json({ + overrides: + overrides.map((k) => ({ + id: k.id, + identifier: k.identifier, + limit: k.limit, + duration: k.duration, + async: k.async ?? undefined, + })) ?? [], + total: Number(total.at(0)?.count ?? 0), + cursor: overrides.at(-1)?.id ?? undefined, + }); + }); diff --git a/apps/api/src/routes/v1_ratelimits_setOverride.error.test.ts b/apps/api/src/routes/v1_ratelimits_setOverride.error.test.ts new file mode 100644 index 0000000000..d0b8e4d11f --- /dev/null +++ b/apps/api/src/routes/v1_ratelimits_setOverride.error.test.ts @@ -0,0 +1,91 @@ +import { expect, test } from "vitest"; + +import { randomUUID } from "node:crypto"; +import { IntegrationHarness } from "src/pkg/testutil/integration-harness"; + +import { schema } from "@unkey/db"; +import { newId } from "@unkey/id"; +import type { + V1RatelimitSetOverrideRequest, + V1RatelimitSetOverrideResponse, +} from "./v1_ratelimits_setOverride"; + +test("Missing Namespace", async (t) => { + const h = await IntegrationHarness.init(t); + const root = await h.createRootKey([ + "*", + "ratelimit.*.set_Override", + "ratelimit.*.create_namespace", + "ratelimit.*.read_override", + ]); + const identifier = randomUUID(); + const namespaceId = newId("test"); + + const override = { + namespaceId: namespaceId, + identifier: identifier, + limit: 10, + duration: 6500, + async: true, + }; + const res = await h.post({ + url: "/v1/ratelimits.setOverride", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + body: override, + }); + expect(res.status, `expected 404, received: ${JSON.stringify(res, null, 2)}`).toBe(404); + expect(res.body).toMatchObject({ + error: { + code: "NOT_FOUND", + docs: "https://unkey.dev/docs/api-reference/errors/code/NOT_FOUND", + message: "Namespace not found", + }, + }); +}); +test("Empty Identifier string", async (t) => { + const h = await IntegrationHarness.init(t); + const root = await h.createRootKey([ + "*", + "ratelimit.*.set_Override", + "ratelimit.*.create_namespace", + "ratelimit.*.read_override", + ]); + + const namespaceId = newId("test"); + + const namespace = { + id: namespaceId, + workspaceId: h.resources.userWorkspace.id, + name: randomUUID(), + createdAt: new Date(), + }; + + await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace); + + const override = { + namespaceId: namespaceId, + identifier: "", + limit: 10, + duration: 6500, + async: true, + }; + const res = await h.post({ + url: "/v1/ratelimits.setOverride", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + body: override, + }); + expect(res.status, `expected 400, received: ${JSON.stringify(res, null, 2)}`).toBe(400); + expect(res.body).toMatchObject({ + error: { + code: "BAD_REQUEST", + docs: "https://unkey.dev/docs/api-reference/errors/code/BAD_REQUEST", + message: "identifier: String must contain at least 3 character(s)", + }, + }); +}); diff --git a/apps/api/src/routes/v1_ratelimits_setOverride.happy.test.ts b/apps/api/src/routes/v1_ratelimits_setOverride.happy.test.ts new file mode 100644 index 0000000000..777b63d69b --- /dev/null +++ b/apps/api/src/routes/v1_ratelimits_setOverride.happy.test.ts @@ -0,0 +1,90 @@ +import { expect, test } from "vitest"; + +import { randomUUID } from "node:crypto"; +import { IntegrationHarness } from "src/pkg/testutil/integration-harness"; + +import { schema } from "@unkey/db"; +import { newId } from "@unkey/id"; +import type { + V1RatelimitSetOverrideRequest, + V1RatelimitSetOverrideResponse, +} from "./v1_ratelimits_setOverride"; + +test("Set ratelimit override", async (t) => { + const h = await IntegrationHarness.init(t); + const root = await h.createRootKey([ + "*", + "ratelimit.*.set_override", + "ratelimit.*.create_namespace", + "ratelimit.*.read_override", + ]); + const identifier = randomUUID(); + const namespaceId = newId("test"); + + const namespace = { + id: namespaceId, + workspaceId: h.resources.userWorkspace.id, + name: randomUUID(), + createdAt: new Date(), + }; + + await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace); + + await h.db.primary.query.ratelimitNamespaces.findFirst({ + where: (table, { eq }) => eq(table.id, namespaceId), + }); + + const override = { + namespaceId: namespaceId, + identifier: identifier, + limit: 10, + duration: 6500, + async: true, + }; + const res = await h.post({ + url: "/v1/ratelimits.setOverride", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + body: override, + }); + + expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200); + + const resInit = await h.db.primary.query.ratelimitOverrides.findFirst({ + where: (table, { eq, and }) => + and(eq(table.namespaceId, namespaceId), eq(table.identifier, identifier)), + }); + expect(resInit).toBeDefined(); + + const resUpdate = await h.post({ + url: "/v1/ratelimits.setOverride", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + body: { + namespaceId: resInit?.namespaceId, + identifier: resInit?.identifier!, + limit: 10, + duration: 50000, + async: true, + }, + }); + + expect(resUpdate.status, `expected 200, received: ${JSON.stringify(resUpdate, null, 2)}`).toBe( + 200, + ); + + const resNew = await h.db.primary.query.ratelimitOverrides.findFirst({ + where: (table, { eq, and }) => + and(eq(table.namespaceId, namespaceId), eq(table.identifier, identifier)), + }); + + expect(resNew?.identifier).toEqual(identifier); + expect(resNew?.namespaceId).toEqual(namespaceId); + expect(resNew?.limit).toEqual(10); + expect(resNew?.duration).toEqual(50000); + expect(resNew?.async).toEqual(true); +}); diff --git a/apps/api/src/routes/v1_ratelimits_setOverride.security.test.ts b/apps/api/src/routes/v1_ratelimits_setOverride.security.test.ts new file mode 100644 index 0000000000..0a9b04a6de --- /dev/null +++ b/apps/api/src/routes/v1_ratelimits_setOverride.security.test.ts @@ -0,0 +1,159 @@ +import { randomUUID } from "node:crypto"; +import { runCommonRouteTests } from "@/pkg/testutil/common-tests"; +import { IntegrationHarness } from "@/pkg/testutil/integration-harness"; +import { schema } from "@unkey/db"; +import { newId } from "@unkey/id"; +import { describe, expect, test } from "vitest"; +import type { + V1RatelimitSetOverrideRequest, + V1RatelimitSetOverrideResponse, +} from "./v1_ratelimits_setOverride"; + +runCommonRouteTests({ + prepareRequest: async (rh) => { + const identifier = randomUUID(); + const namespaceId = newId("test"); + + const namespace = { + id: namespaceId, + workspaceId: rh.resources.userWorkspace.id, + name: randomUUID(), + createdAt: new Date(), + }; + + await rh.db.primary.insert(schema.ratelimitNamespaces).values(namespace); + + await rh.db.primary.query.ratelimitNamespaces.findFirst({ + where: (table, { eq }) => eq(table.id, namespaceId), + }); + + const override = { + namespaceId: namespaceId, + identifier: identifier, + limit: 10, + duration: 6500, + async: true, + }; + return { + method: "POST", + url: "/v1/ratelimits.setOverride", + headers: { + "Content-Type": "application/json", + }, + body: override, + }; + }, +}); +describe("correct roles", () => { + describe.each([{ name: "set override", roles: ["ratelimit.*.set_override"] }])( + "$name", + ({ roles }) => { + test("returns 200", async (t) => { + const h = await IntegrationHarness.init(t); + const identifier = randomUUID(); + const namespaceId = newId("test"); + const namespace = { + id: namespaceId, + workspaceId: h.resources.userWorkspace.id, + createdAt: new Date(), + name: newId("test"), + }; + await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace); + + const root = await h.createRootKey(roles); + + const res = await h.post({ + url: "/v1/ratelimits.setOverride", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + body: { + namespaceId, + identifier, + limit: 1, + duration: 60_000, + async: false, + }, + }); + + expect( + res.status, + `expected status 200, received: ${JSON.stringify(res, null, 2)}`, + ).toEqual(200); + await h.post({ + url: "/v1/ratelimits.setOverride", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + body: { + namespaceId, + identifier, + limit: 7, + duration: 60_000, + async: false, + }, + }); + expect( + res.status, + `expected status 200, received: ${JSON.stringify(res, null, 2)}`, + ).toEqual(200); + + const found = await h.db.primary.query.ratelimitOverrides.findFirst({ + where: (table, { eq }) => eq(table.id, res.body.overrideId), + }); + + expect(found?.limit).toEqual(7); + }); + }, + ); +}); + +describe("incorrect roles", () => { + describe.each([ + { name: "empty roles", roles: [] }, + { name: "invalid role", roles: ["wrong.role"] }, + { name: "insufficient role", roles: ["ratelimit.*.view"] }, + ])("$name", ({ roles }) => { + test("returns 403", async (t) => { + const h = await IntegrationHarness.init(t); + const overrideId = newId("test"); + const identifier = randomUUID(); + const namespaceId = newId("test"); + const namespace = { + id: namespaceId, + workspaceId: h.resources.userWorkspace.id, + createdAt: new Date(), + name: newId("test"), + }; + await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace); + + const root = await h.createRootKey(roles); + + const res = await h.post({ + url: "/v1/ratelimits.setOverride", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + body: { + namespaceId, + identifier, + limit: 1, + duration: 60_000, + async: false, + }, + }); + + expect(res.status, `expected status 403, received: ${JSON.stringify(res, null, 2)}`).toEqual( + 403, + ); + + const found = await h.db.primary.query.ratelimitOverrides.findFirst({ + where: (table, { eq }) => eq(table.id, overrideId), + }); + expect(found?.id).toEqual(undefined); + }); + }); +}); diff --git a/apps/api/src/routes/v1_ratelimits_setOverride.ts b/apps/api/src/routes/v1_ratelimits_setOverride.ts new file mode 100644 index 0000000000..2846dd6c45 --- /dev/null +++ b/apps/api/src/routes/v1_ratelimits_setOverride.ts @@ -0,0 +1,184 @@ +import type { App } from "@/pkg/hono/app"; +import { createRoute, z } from "@hono/zod-openapi"; + +import { insertUnkeyAuditLog } from "@/pkg/audit"; +import { rootKeyAuth } from "@/pkg/auth/root_key"; +import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; +import { eq, schema } from "@unkey/db"; +import { newId } from "@unkey/id"; +import { buildUnkeyQuery } from "@unkey/rbac"; + +const route = createRoute({ + tags: ["ratelimit"], + operationId: "ratelimit.setOverride", + method: "post", + path: "/v1/ratelimits.setOverride", + security: [{ bearerAuth: [] }], + request: { + body: { + required: true, + content: { + "application/json": { + schema: z.object({ + namespaceId: z.string().optional().openapi({ + description: + "The id of the namespace. Either namespaceId or namespaceName must be provided", + example: "rlns_1234", + }), + namespaceName: z.string().optional().openapi({ + description: + "Namespaces group different limits together for better analytics. You might have a namespace for your public API and one for internal tRPC routes. Wildcards can also be used, more info can be found at https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules", + example: "email.outbound", + }), + identifier: z.string().min(3).openapi({ + description: + "Identifier of your user, this can be their userId, an email, an ip or anything else. Wildcards ( * ) can be used to match multiple identifiers, More info can be found at https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules", + example: "user_123", + }), + limit: z.number().int().positive().openapi({ + description: "How many requests may pass in a given window.", + example: 10, + }), + duration: z.number().int().min(1000).openapi({ + description: "The window duration in milliseconds", + example: 60_000, + }), + async: z.boolean().optional().default(false).openapi({ + description: + "Async will return a response immediately, lowering latency at the cost of accuracy.", + }), + }), + }, + }, + }, + }, + responses: { + 200: { + description: "Sucessfully created a ratelimit override", + content: { + "application/json": { + schema: z.object({ + overrideId: z.string().openapi({ + description: "The id of the override. This is used internally", + example: "over_123", + }), + }), + }, + }, + }, + ...openApiErrorResponses, + }, +}); + +export type Route = typeof route; +export type V1RatelimitSetOverrideRequest = z.infer< + (typeof route.request.body.content)["application/json"]["schema"] +>; +export type V1RatelimitSetOverrideResponse = z.infer< + (typeof route.responses)[200]["content"]["application/json"]["schema"] +>; + +export const registerV1RatelimitSetOverride = (app: App) => + app.openapi(route, async (c) => { + const req = c.req.valid("json"); + if (!req.namespaceId && !req.namespaceName) { + throw new UnkeyApiError({ + code: "BAD_REQUEST", + message: "You must provide a namespaceId or a namespaceName", + }); + } + const auth = await rootKeyAuth( + c, + buildUnkeyQuery(({ or }) => or("*", "ratelimit.*.set_override")), + ); + + const { db } = c.get("services"); + const authorizedWorkspaceId = auth.authorizedWorkspaceId; + + const overrideId = await db.primary.transaction(async (tx) => { + const namespace = await tx.query.ratelimitNamespaces.findFirst({ + where: (table, { and, eq }) => + and( + eq(table.workspaceId, authorizedWorkspaceId), + req.namespaceId ? eq(table.id, req.namespaceId) : eq(table.name, req.namespaceName!), + ), + with: { + overrides: { + where: (table, { eq }) => eq(table.identifier, req.identifier), + }, + }, + }); + + if (!namespace) { + throw new UnkeyApiError({ + code: "NOT_FOUND", + message: "Namespace not found", + }); + } + + const override = namespace.overrides.at(0); + const overrideId = override?.id ?? newId("ratelimitOverride"); + if (override) { + await tx + .update(schema.ratelimitOverrides) + .set({ + limit: req.limit, + duration: req.duration, + async: req.async, + updatedAt: new Date(), + }) + .where(eq(schema.ratelimitOverrides.id, override.id)); + + await insertUnkeyAuditLog(c, tx, { + workspaceId: auth.authorizedWorkspaceId, + event: "ratelimit.set_override", + actor: { + type: "key", + id: auth.key.id, + }, + description: `Set ratelimit override for ${req.namespaceId} and ${req.identifier}`, + resources: [ + { + type: "ratelimitOverride", + id: override.id, + }, + ], + + context: { location: c.get("location"), userAgent: c.get("userAgent") }, + }); + } else { + await tx.insert(schema.ratelimitOverrides).values({ + id: overrideId, + workspaceId: auth.authorizedWorkspaceId, + createdAt: new Date(), + namespaceId: namespace.id, + identifier: req.identifier, + limit: req.limit, + duration: req.duration, + async: req.async, + }); + + await insertUnkeyAuditLog(c, tx, { + workspaceId: auth.authorizedWorkspaceId, + event: "ratelimit.set_override", + actor: { + type: "key", + id: auth.key.id, + }, + description: `Set ratelimit override for ${req.namespaceId} and ${req.identifier}`, + resources: [ + { + type: "ratelimitOverride", + id: overrideId, + }, + ], + + context: { location: c.get("location"), userAgent: c.get("userAgent") }, + }); + } + return overrideId; + }); + return c.json({ + overrideId, + }); + }); diff --git a/apps/api/src/worker.ts b/apps/api/src/worker.ts index 913cc4bfc5..553521426a 100644 --- a/apps/api/src/worker.ts +++ b/apps/api/src/worker.ts @@ -15,7 +15,7 @@ import { registerV1KeysUpdateRemaining } from "./routes/v1_keys_updateRemaining" import { registerV1KeysVerifyKey } from "./routes/v1_keys_verifyKey"; import { registerV1KeysWhoAmI } from "./routes/v1_keys_whoami"; import { registerV1Liveness } from "./routes/v1_liveness"; -import { registerV1RatelimitLimit } from "./routes/v1_ratelimit_limit"; +import { registerV1RatelimitLimit } from "./routes/v1_ratelimits_limit"; // Legacy Routes import { registerLegacyKeysCreate } from "./routes/legacy_keys_createKey"; @@ -51,6 +51,10 @@ import { registerV1PermissionsGetPermission } from "./routes/v1_permissions_getP import { registerV1PermissionsGetRole } from "./routes/v1_permissions_getRole"; import { registerV1PermissionsListPermissions } from "./routes/v1_permissions_listPermissions"; import { registerV1PermissionsListRoles } from "./routes/v1_permissions_listRoles"; +import { registerV1RatelimitDeleteOverride } from "./routes/v1_ratelimits_deleteOverride"; +import { registerV1RatelimitGetOverride } from "./routes/v1_ratelimits_getOverride"; +import { registerV1RatelimitListOverrides } from "./routes/v1_ratelimits_listOverrides"; +import { registerV1RatelimitSetOverride } from "./routes/v1_ratelimits_setOverride"; const app = newApp(); @@ -93,6 +97,10 @@ registerV1ApisDeleteKeys(app); // ratelimit registerV1RatelimitLimit(app); +registerV1RatelimitSetOverride(app); +registerV1RatelimitListOverrides(app); +registerV1RatelimitDeleteOverride(app); +registerV1RatelimitGetOverride(app); // migrations registerV1MigrationsCreateKeys(app); diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx index f37da70b3b..8694bb34bb 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx @@ -259,6 +259,7 @@ export const CreateKey: React.FC = ({ apiId, keyAuthId, defaultBytes, def form.resetField("limit", undefined); }; + // biome-ignore lint: only run once useEffect(() => { // React hook form + zod doesn't play nice with nested objects, so we need to reset them on load. resetRateLimit(); diff --git a/apps/dashboard/app/(app)/settings/root-keys/[keyId]/permissions/permissions.ts b/apps/dashboard/app/(app)/settings/root-keys/[keyId]/permissions/permissions.ts index c0964b033a..3fd38752e6 100644 --- a/apps/dashboard/app/(app)/settings/root-keys/[keyId]/permissions/permissions.ts +++ b/apps/dashboard/app/(app)/settings/root-keys/[keyId]/permissions/permissions.ts @@ -71,6 +71,18 @@ export const workspacePermissions = { description: "Delete namespaces in this workspace.", permission: "ratelimit.*.delete_namespace", }, + set_override: { + description: "Set a ratelimit override for an identifier.", + permission: "ratelimit.*.set_override", + }, + read_override: { + description: "read ratelimit override for an identifier.", + permission: "ratelimit.*.read_override", + }, + delete_override: { + description: "Delete ratelimit override for an identifier.", + permission: "ratelimit.*.delete_override", + }, }, Permissions: { create_role: { diff --git a/apps/dashboard/app/(app)/settings/root-keys/new/client.tsx b/apps/dashboard/app/(app)/settings/root-keys/new/client.tsx index 3fe8c58a45..c74be97650 100644 --- a/apps/dashboard/app/(app)/settings/root-keys/new/client.tsx +++ b/apps/dashboard/app/(app)/settings/root-keys/new/client.tsx @@ -93,6 +93,7 @@ export const Client: React.FC = ({ apis }) => { })); }; + // biome-ignore lint: should only run once useEffect(() => { const initialSelectedApiSet = new Set(); selectedPermissions.forEach((permission) => { diff --git a/apps/dashboard/app/auth/sign-in/email-signin.tsx b/apps/dashboard/app/auth/sign-in/email-signin.tsx index a88a998e96..f3f4fe583d 100644 --- a/apps/dashboard/app/auth/sign-in/email-signin.tsx +++ b/apps/dashboard/app/auth/sign-in/email-signin.tsx @@ -20,7 +20,7 @@ export function EmailSignIn(props: { const router = useRouter(); const [lastUsed, setLastUsed] = useLastUsed(); - // biome-igmore lint: this works + // biome-ignore lint: this works React.useEffect(() => { const signUpOrgUser = async () => { const ticket = new URL(window.location.href).searchParams.get(param); diff --git a/apps/dashboard/app/auth/sign-up/email-signup.tsx b/apps/dashboard/app/auth/sign-up/email-signup.tsx index ff7c527d3f..a75efc945b 100644 --- a/apps/dashboard/app/auth/sign-up/email-signup.tsx +++ b/apps/dashboard/app/auth/sign-up/email-signup.tsx @@ -20,6 +20,7 @@ export const EmailSignUp: React.FC = ({ setError, setVerification }) => { const [isLoading, setIsLoading] = React.useState(false); const [_transferLoading, setTransferLoading] = React.useState(true); const router = useRouter(); + // biome-ignore lint: works fine as is React.useEffect(() => { const signUpFromParams = async () => { const ticket = new URL(window.location.href).searchParams.get("__clerk_ticket"); diff --git a/apps/dashboard/lib/trpc/routers/ratelimit/createOverride.ts b/apps/dashboard/lib/trpc/routers/ratelimit/createOverride.ts index e78415fd55..839424c874 100644 --- a/apps/dashboard/lib/trpc/routers/ratelimit/createOverride.ts +++ b/apps/dashboard/lib/trpc/routers/ratelimit/createOverride.ts @@ -86,7 +86,7 @@ export const createOverride = t.procedure type: "user", id: ctx.user.id, }, - event: "ratelimitOverride.create", + event: "ratelimit.set_override", description: `Created ${input.identifier}`, resources: [ { diff --git a/apps/dashboard/lib/trpc/routers/ratelimit/deleteNamespace.ts b/apps/dashboard/lib/trpc/routers/ratelimit/deleteNamespace.ts index eea6566bd0..02f327c48f 100644 --- a/apps/dashboard/lib/trpc/routers/ratelimit/deleteNamespace.ts +++ b/apps/dashboard/lib/trpc/routers/ratelimit/deleteNamespace.ts @@ -92,7 +92,7 @@ export const deleteNamespace = t.procedure type: "user", id: ctx.user.id, }, - event: "ratelimitOverride.delete", + event: "ratelimit.delete_override", description: `Deleted ${id} as part of the ${namespace.id} deletion`, resources: [ { diff --git a/apps/dashboard/lib/trpc/routers/ratelimit/deleteOverride.ts b/apps/dashboard/lib/trpc/routers/ratelimit/deleteOverride.ts index c190f830cd..e505ac5f22 100644 --- a/apps/dashboard/lib/trpc/routers/ratelimit/deleteOverride.ts +++ b/apps/dashboard/lib/trpc/routers/ratelimit/deleteOverride.ts @@ -65,7 +65,7 @@ export const deleteOverride = t.procedure type: "user", id: ctx.user.id, }, - event: "ratelimitOverride.delete", + event: "ratelimit.delete_override", description: `Deleted ${override.id}`, resources: [ { diff --git a/apps/dashboard/lib/trpc/routers/ratelimit/updateOverride.ts b/apps/dashboard/lib/trpc/routers/ratelimit/updateOverride.ts index f570ca407d..9ab5058972 100644 --- a/apps/dashboard/lib/trpc/routers/ratelimit/updateOverride.ts +++ b/apps/dashboard/lib/trpc/routers/ratelimit/updateOverride.ts @@ -74,7 +74,7 @@ export const updateOverride = t.procedure type: "user", id: ctx.user.id, }, - event: "ratelimitOverride.update", + event: "ratelimit.set_override", description: `Changed ${override.id} limits from ${override.limit}/${override.duration} to ${input.limit}/${input.duration}`, resources: [ { diff --git a/apps/www/components/shiny-card.tsx b/apps/www/components/shiny-card.tsx index b7c8830773..7450e7a71f 100644 --- a/apps/www/components/shiny-card.tsx +++ b/apps/www/components/shiny-card.tsx @@ -42,6 +42,7 @@ export const ShinyCardGroup: React.FC = ({ } }, []); + // biome-ignore lint: works fine const onMouseMove = useCallback(() => { if (containerRef.current) { const rect = containerRef.current.getBoundingClientRect(); @@ -93,7 +94,7 @@ export const ShinyCard: React.FC> = ({ }) => { return (
{children} @@ -107,7 +108,7 @@ export const WhiteShinyCard: React.FC> = ({ }) => { return (
{children} diff --git a/deployment/docker-compose.yaml b/deployment/docker-compose.yaml index 05421db6fb..4b3d2b5bc1 100644 --- a/deployment/docker-compose.yaml +++ b/deployment/docker-compose.yaml @@ -134,30 +134,30 @@ services: - agent_lb - clickhouse - prometheus: - image: prom/prometheus - container_name: prometheus - command: - - '--config.file=/etc/prometheus/prometheus.yaml' - ports: - - 9090:9090 - restart: unless-stopped - volumes: - - ./prometheus:/etc/prometheus - - prometheus:/prometheus - - - grafana: - image: grafana/grafana - container_name: grafana - ports: - - 4000:3000 - restart: unless-stopped - environment: - - GF_SECURITY_ADMIN_USER=admin - - GF_SECURITY_ADMIN_PASSWORD=grafana - volumes: - - ./grafana:/etc/grafana/provisioning/datasources + # prometheus: + # image: prom/prometheus + # container_name: prometheus + # command: + # - '--config.file=/etc/prometheus/prometheus.yaml' + # ports: + # - 9090:9090 + # restart: unless-stopped + # volumes: + # - ./prometheus:/etc/prometheus + # - prometheus:/prometheus + + + # grafana: + # image: grafana/grafana + # container_name: grafana + # ports: + # - 4000:3000 + # restart: unless-stopped + # environment: + # - GF_SECURITY_ADMIN_USER=admin + # - GF_SECURITY_ADMIN_PASSWORD=grafana + # volumes: + # - ./grafana:/etc/grafana/provisioning/datasources volumes: mysql: grafana: diff --git a/internal/clickhouse/src/insert_verifications.test.ts b/internal/clickhouse/src/insert_verifications.test.ts index 3d5bc5e2a3..520619474f 100644 --- a/internal/clickhouse/src/insert_verifications.test.ts +++ b/internal/clickhouse/src/insert_verifications.test.ts @@ -31,9 +31,10 @@ test( keyId: verification.key_id, }); - expect(latestVerifications.length).toBe(1); - expect(latestVerifications[0].time).toBe(verification.time); - expect(latestVerifications[0].outcome).toBe("VALID"); - expect(latestVerifications[0].region).toBe(verification.region); + expect(latestVerifications.err).toBeUndefined(); + expect(latestVerifications.val!.length).toBe(1); + expect(latestVerifications.val![0].time).toBe(verification.time); + expect(latestVerifications.val![0].outcome).toBe("VALID"); + expect(latestVerifications.val![0].region).toBe(verification.region); }, ); diff --git a/internal/schema/src/auditlog.ts b/internal/schema/src/auditlog.ts index c9a52ad9cf..5c266bbbe2 100644 --- a/internal/schema/src/auditlog.ts +++ b/internal/schema/src/auditlog.ts @@ -17,9 +17,6 @@ export const unkeyAuditLogEvents = z.enum([ "ratelimitNamespace.create", "ratelimitNamespace.update", "ratelimitNamespace.delete", - "ratelimitOverride.create", - "ratelimitOverride.update", - "ratelimitOverride.delete", "vercelIntegration.create", "vercelIntegration.update", "vercelIntegration.delete", @@ -51,6 +48,9 @@ export const unkeyAuditLogEvents = z.enum([ "ratelimit.create", "ratelimit.update", "ratelimit.delete", + "ratelimit.set_override", + "ratelimit.read_override", + "ratelimit.delete_override", "auditLogBucket.create", ]); diff --git a/package.json b/package.json index b3b6483ed8..e1621bf300 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "dev": "turbo run dev", "build": "pnpm turbo run build", "fmt": "pnpm biome format . --write && pnpm biome check . --apply", - "test": "turbo run test", + "test": "turbo run test --concurrency=1", "bootstrap": "turbo run bootstrap", "commit": "cz", "bump-versions": "pnpm changeset version && pnpm install", diff --git a/packages/api/package.json b/packages/api/package.json index 4767813f8f..feb0ce95ff 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -19,7 +19,8 @@ "scripts": { "generate": "openapi-typescript https://api.unkey.dev/openapi.json -o ./src/openapi.d.ts", "build": "pnpm generate && tsup", - "test": "vitest run" + "pull": "docker pull bitnami/clickhouse:latest && docker pull golang:alpine", + "test": "pnpm pull && vitest run" }, "devDependencies": { "@types/node": "^20.14.9", diff --git a/packages/api/src/client.ts b/packages/api/src/client.ts index 9e921c1ff9..c70ddbb35d 100644 --- a/packages/api/src/client.ts +++ b/packages/api/src/client.ts @@ -376,6 +376,60 @@ export class Unkey { body: req, }); }, + // getOverride: async ( + // req: paths["/v1/ratelimits.getOverride"]["get"]["parameters"]["query"], + // ): Promise< + // Result< + // paths["/v1/ratelimits.getOverride"]["get"]["responses"]["200"]["content"]["application/json"] + // > + // > => { + // return await this.fetch({ + // path: ["v1", "ratelimits.getOverride"], + // method: "GET", + // query: req, + // }); + // }, + // listOverrides: async ( + // req: paths["/v1/ratelimits.listOverrides"]["get"]["parameters"]["query"], + // ): Promise< + // Result< + // paths["/v1/ratelimits.listOverrides"]["get"]["responses"]["200"]["content"]["application/json"] + // > + // > => { + // return await this.fetch({ + // path: ["v1", "ratelimits.listOverrides"], + // method: "GET", + // query: req, + // }); + // }, + + // setOverride: async ( + // req: paths["/v1/ratelimits.setOverride"]["post"]["requestBody"]["content"]["application/json"], + // ): Promise< + // Result< + // paths["/v1/ratelimits.setOverride"]["post"]["responses"]["200"]["content"]["application/json"] + // > + // > => { + // return await this.fetch({ + // path: ["v1", "ratelimits.setOverride"], + // method: "POST", + // body: req, + // }); + // }, + + // deleteOverride: async ( + // req: paths["/v1/ratelimits.deleteOverride"]["post"]["requestBody"]["content"]["application/json"], + // ): Promise< + // Result< + // paths["/v1/ratelimits.deleteOverride"]["post"]["responses"]["200"]["content"]["application/json"] + // > + // > => { + // return await this.fetch({ + // path: ["v1", "ratelimits.deleteOverride"], + // method: "POST", + // body: req, + // }); + // }, }; } public get identities() { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 88c879ee59..93a73b4656 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -199,7 +199,7 @@ importers: version: link:../../internal/schema ai: specifier: ^3.4.7 - version: 3.4.7(openai@4.52.1)(react@18.3.1)(svelte@5.1.16)(vue@3.5.12)(zod@3.23.8) + version: 3.4.7(openai@4.52.1)(react@18.3.1)(svelte@5.2.2)(vue@3.5.13)(zod@3.23.8) drizzle-orm: specifier: ^0.33.0 version: 0.33.0(@opentelemetry/api@1.4.1)(@planetscale/database@1.18.0)(@types/react@18.3.11)(react@18.3.1) @@ -857,7 +857,7 @@ importers: version: link:../../internal/worker-logging ai: specifier: ^3.0.23 - version: 3.4.7(openai@4.52.1)(react@18.3.1)(svelte@5.1.16)(vue@3.5.12)(zod@3.23.8) + version: 3.4.7(openai@4.52.1)(react@18.3.1)(svelte@5.2.2)(vue@3.5.13)(zod@3.23.8) drizzle-orm: specifier: generated version: 0.32.0-aaf764c(@cloudflare/workers-types@4.20240603.0)(@planetscale/database@1.18.0)(react@18.3.1) @@ -1163,7 +1163,7 @@ importers: version: link:../../packages/error ai: specifier: ^3.0.23 - version: 3.4.7(openai@4.52.1)(react@18.3.1)(svelte@5.1.16)(vue@3.5.12)(zod@3.23.8) + version: 3.4.7(openai@4.52.1)(react@18.3.1)(svelte@5.2.2)(vue@3.5.13)(zod@3.23.8) zod: specifier: ^3.23.5 version: 3.23.8 @@ -1185,7 +1185,7 @@ importers: devDependencies: checkly: specifier: latest - version: 4.9.1(@types/node@20.14.9)(typescript@5.5.3) + version: 4.10.0(@types/node@20.14.9)(typescript@5.5.3) ts-node: specifier: 10.9.1 version: 10.9.1(@types/node@20.14.9)(typescript@5.5.3) @@ -1418,7 +1418,7 @@ importers: version: 18.3.1 react-email: specifier: 2.1.1 - version: 2.1.1(@babel/core@7.26.0)(eslint@9.14.0)(ts-node@10.9.2) + version: 2.1.1(@babel/core@7.26.0)(eslint@9.15.0)(ts-node@10.9.2) resend: specifier: ^4.0.0 version: 4.0.0(react-dom@18.3.1)(react@18.3.1) @@ -1794,7 +1794,7 @@ packages: p-map: 7.0.2 p-throttle: 6.2.0 quick-lru: 7.0.0 - type-fest: 4.26.1 + type-fest: 4.27.0 zod: 3.23.8 zod-to-json-schema: 3.23.5(zod@3.23.8) zod-validation-error: 3.4.0(zod@3.23.8) @@ -1881,7 +1881,7 @@ packages: - zod dev: false - /@ai-sdk/svelte@0.0.51(svelte@5.1.16)(zod@3.23.8): + /@ai-sdk/svelte@0.0.51(svelte@5.2.2)(zod@3.23.8): resolution: {integrity: sha512-aIZJaIds+KpCt19yUDCRDWebzF/17GCY7gN9KkcA2QM6IKRO5UmMcqEYja0ZmwFQPm1kBZkF2njhr8VXis2mAw==} engines: {node: '>=18'} peerDependencies: @@ -1892,8 +1892,8 @@ packages: dependencies: '@ai-sdk/provider-utils': 1.0.20(zod@3.23.8) '@ai-sdk/ui-utils': 0.0.46(zod@3.23.8) - sswr: 2.1.0(svelte@5.1.16) - svelte: 5.1.16 + sswr: 2.1.0(svelte@5.2.2) + svelte: 5.2.2 transitivePeerDependencies: - zod dev: false @@ -1915,7 +1915,7 @@ packages: zod-to-json-schema: 3.23.2(zod@3.23.8) dev: false - /@ai-sdk/vue@0.0.53(vue@3.5.12)(zod@3.23.8): + /@ai-sdk/vue@0.0.53(vue@3.5.13)(zod@3.23.8): resolution: {integrity: sha512-FNScuIvM8N4Pj4Xto11blgI97c5cjjTelyk0M0MkyU+sLSbpQNDE78CRq5cW1oeVkJzzdv63+xh8jaFNe+2vnQ==} engines: {node: '>=18'} peerDependencies: @@ -1926,8 +1926,8 @@ packages: dependencies: '@ai-sdk/provider-utils': 1.0.20(zod@3.23.8) '@ai-sdk/ui-utils': 0.0.46(zod@3.23.8) - swrv: 1.0.4(vue@3.5.12) - vue: 3.5.12(typescript@5.5.3) + swrv: 1.0.4(vue@3.5.13) + vue: 3.5.13(typescript@5.5.3) transitivePeerDependencies: - zod dev: false @@ -3263,7 +3263,7 @@ packages: '@lifeomic/axios-fetch': 3.1.0 '@opentelemetry/api': 1.4.1 '@opentelemetry/exporter-trace-otlp-http': 0.53.0(@opentelemetry/api@1.4.1) - '@opentelemetry/sdk-metrics': 1.27.0(@opentelemetry/api@1.4.1) + '@opentelemetry/sdk-metrics': 1.28.0(@opentelemetry/api@1.4.1) '@opentelemetry/sdk-node': 0.52.1(@opentelemetry/api@1.4.1) '@opentelemetry/semantic-conventions': 1.13.0 adm-zip: 0.5.16 @@ -3299,7 +3299,7 @@ packages: /@electric-sql/client@0.6.3: resolution: {integrity: sha512-/AYkRrEASKIGcjtNp8IVJ3sAUm+IQ2l0NrGgDvvAG/n1+ifOl7kD1E4dRyg1qdY/b+HdKhGNYlNgsPuwMKO2Mg==} optionalDependencies: - '@rollup/rollup-darwin-arm64': 4.26.0 + '@rollup/rollup-darwin-arm64': 4.27.2 dev: false /@emnapi/runtime@1.3.1: @@ -4803,13 +4803,13 @@ packages: requiresBuild: true optional: true - /@eslint-community/eslint-utils@4.4.1(eslint@9.14.0): + /@eslint-community/eslint-utils@4.4.1(eslint@9.15.0): resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - eslint: 9.14.0 + eslint: 9.15.0 eslint-visitor-keys: 3.4.3 dev: false @@ -4818,8 +4818,8 @@ packages: engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: false - /@eslint/config-array@0.18.0: - resolution: {integrity: sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==} + /@eslint/config-array@0.19.0: + resolution: {integrity: sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: '@eslint/object-schema': 2.1.4 @@ -4829,13 +4829,13 @@ packages: - supports-color dev: false - /@eslint/core@0.7.0: - resolution: {integrity: sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==} + /@eslint/core@0.9.0: + resolution: {integrity: sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dev: false - /@eslint/eslintrc@3.1.0: - resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} + /@eslint/eslintrc@3.2.0: + resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: ajv: 6.12.6 @@ -4851,8 +4851,8 @@ packages: - supports-color dev: false - /@eslint/js@9.14.0: - resolution: {integrity: sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==} + /@eslint/js@9.15.0: + resolution: {integrity: sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dev: false @@ -4861,8 +4861,8 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dev: false - /@eslint/plugin-kit@0.2.2: - resolution: {integrity: sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==} + /@eslint/plugin-kit@0.2.3: + resolution: {integrity: sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: levn: 0.4.1 @@ -4905,8 +4905,8 @@ packages: /@floating-ui/utils@0.2.8: resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==} - /@formatjs/intl-localematcher@0.5.7: - resolution: {integrity: sha512-GGFtfHGQVFe/niOZp24Kal5b2i36eE2bNL0xi9Sg/yd0TR8aLjcteApZdHmismP5QQax1cMnZM9yWySUUjJteA==} + /@formatjs/intl-localematcher@0.5.8: + resolution: {integrity: sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==} dependencies: tslib: 2.8.1 dev: false @@ -6927,8 +6927,8 @@ packages: '@opentelemetry/resources': 1.13.0(@opentelemetry/api@1.4.1) dev: true - /@opentelemetry/sdk-metrics@1.27.0(@opentelemetry/api@1.4.1): - resolution: {integrity: sha512-JzWgzlutoXCydhHWIbLg+r76m+m3ncqvkCcsswXAQ4gqKS+LOHKhq+t6fx1zNytvLuaOUBur7EvWxECc4jPQKg==} + /@opentelemetry/sdk-metrics@1.28.0(@opentelemetry/api@1.4.1): + resolution: {integrity: sha512-43tqMK/0BcKTyOvm15/WQ3HLr0Vu/ucAl/D84NO7iSlv6O4eOprxSHa3sUtmYkaZWHqdDJV0AHVz/R6u4JALVQ==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' @@ -7144,7 +7144,7 @@ packages: resolution: {integrity: sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==} engines: {node: '>=8.0.0'} dependencies: - tslib: 2.8.1 + tslib: 2.4.1 dev: false /@peculiar/webcrypto@1.4.1: @@ -7154,7 +7154,7 @@ packages: '@peculiar/asn1-schema': 2.3.13 '@peculiar/json-schema': 1.1.12 pvtsutils: 1.3.5 - tslib: 2.8.1 + tslib: 2.4.1 webcrypto-core: 1.8.1 dev: false @@ -9693,143 +9693,143 @@ packages: picomatch: 4.0.2 dev: true - /@rollup/rollup-android-arm-eabi@4.26.0: - resolution: {integrity: sha512-gJNwtPDGEaOEgejbaseY6xMFu+CPltsc8/T+diUTTbOQLqD+bnrJq9ulH6WD69TqwqWmrfRAtUv30cCFZlbGTQ==} + /@rollup/rollup-android-arm-eabi@4.27.2: + resolution: {integrity: sha512-Tj+j7Pyzd15wAdSJswvs5CJzJNV+qqSUcr/aCD+jpQSBtXvGnV0pnrjoc8zFTe9fcKCatkpFpOO7yAzpO998HA==} cpu: [arm] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-android-arm64@4.26.0: - resolution: {integrity: sha512-YJa5Gy8mEZgz5JquFruhJODMq3lTHWLm1fOy+HIANquLzfIOzE9RA5ie3JjCdVb9r46qfAQY/l947V0zfGJ0OQ==} + /@rollup/rollup-android-arm64@4.27.2: + resolution: {integrity: sha512-xsPeJgh2ThBpUqlLgRfiVYBEf/P1nWlWvReG+aBWfNv3XEBpa6ZCmxSVnxJgLgkNz4IbxpLy64h2gCmAAQLneQ==} cpu: [arm64] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-arm64@4.26.0: - resolution: {integrity: sha512-ErTASs8YKbqTBoPLp/kA1B1Um5YSom8QAc4rKhg7b9tyyVqDBlQxy7Bf2wW7yIlPGPg2UODDQcbkTlruPzDosw==} + /@rollup/rollup-darwin-arm64@4.27.2: + resolution: {integrity: sha512-KnXU4m9MywuZFedL35Z3PuwiTSn/yqRIhrEA9j+7OSkji39NzVkgxuxTYg5F8ryGysq4iFADaU5osSizMXhU2A==} cpu: [arm64] os: [darwin] requiresBuild: true optional: true - /@rollup/rollup-darwin-x64@4.26.0: - resolution: {integrity: sha512-wbgkYDHcdWW+NqP2mnf2NOuEbOLzDblalrOWcPyY6+BRbVhliavon15UploG7PpBRQ2bZJnbmh8o3yLoBvDIHA==} + /@rollup/rollup-darwin-x64@4.27.2: + resolution: {integrity: sha512-Hj77A3yTvUeCIx/Vi+4d4IbYhyTwtHj07lVzUgpUq9YpJSEiGJj4vXMKwzJ3w5zp5v3PFvpJNgc/J31smZey6g==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-freebsd-arm64@4.26.0: - resolution: {integrity: sha512-Y9vpjfp9CDkAG4q/uwuhZk96LP11fBz/bYdyg9oaHYhtGZp7NrbkQrj/66DYMMP2Yo/QPAsVHkV891KyO52fhg==} + /@rollup/rollup-freebsd-arm64@4.27.2: + resolution: {integrity: sha512-RjgKf5C3xbn8gxvCm5VgKZ4nn0pRAIe90J0/fdHUsgztd3+Zesb2lm2+r6uX4prV2eUByuxJNdt647/1KPRq5g==} cpu: [arm64] os: [freebsd] requiresBuild: true dev: true optional: true - /@rollup/rollup-freebsd-x64@4.26.0: - resolution: {integrity: sha512-A/jvfCZ55EYPsqeaAt/yDAG4q5tt1ZboWMHEvKAH9Zl92DWvMIbnZe/f/eOXze65aJaaKbL+YeM0Hz4kLQvdwg==} + /@rollup/rollup-freebsd-x64@4.27.2: + resolution: {integrity: sha512-duq21FoXwQtuws+V9H6UZ+eCBc7fxSpMK1GQINKn3fAyd9DFYKPJNcUhdIKOrMFjLEJgQskoMoiuizMt+dl20g==} cpu: [x64] os: [freebsd] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.26.0: - resolution: {integrity: sha512-paHF1bMXKDuizaMODm2bBTjRiHxESWiIyIdMugKeLnjuS1TCS54MF5+Y5Dx8Ui/1RBPVRE09i5OUlaLnv8OGnA==} + /@rollup/rollup-linux-arm-gnueabihf@4.27.2: + resolution: {integrity: sha512-6npqOKEPRZkLrMcvyC/32OzJ2srdPzCylJjiTJT2c0bwwSGm7nz2F9mNQ1WrAqCBZROcQn91Fno+khFhVijmFA==} cpu: [arm] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm-musleabihf@4.26.0: - resolution: {integrity: sha512-cwxiHZU1GAs+TMxvgPfUDtVZjdBdTsQwVnNlzRXC5QzIJ6nhfB4I1ahKoe9yPmoaA/Vhf7m9dB1chGPpDRdGXg==} + /@rollup/rollup-linux-arm-musleabihf@4.27.2: + resolution: {integrity: sha512-V9Xg6eXtgBtHq2jnuQwM/jr2mwe2EycnopO8cbOvpzFuySCGtKlPCI3Hj9xup/pJK5Q0388qfZZy2DqV2J8ftw==} cpu: [arm] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-gnu@4.26.0: - resolution: {integrity: sha512-4daeEUQutGRCW/9zEo8JtdAgtJ1q2g5oHaoQaZbMSKaIWKDQwQ3Yx0/3jJNmpzrsScIPtx/V+1AfibLisb3AMQ==} + /@rollup/rollup-linux-arm64-gnu@4.27.2: + resolution: {integrity: sha512-uCFX9gtZJoQl2xDTpRdseYuNqyKkuMDtH6zSrBTA28yTfKyjN9hQ2B04N5ynR8ILCoSDOrG/Eg+J2TtJ1e/CSA==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-musl@4.26.0: - resolution: {integrity: sha512-eGkX7zzkNxvvS05ROzJ/cO/AKqNvR/7t1jA3VZDi2vRniLKwAWxUr85fH3NsvtxU5vnUUKFHKh8flIBdlo2b3Q==} + /@rollup/rollup-linux-arm64-musl@4.27.2: + resolution: {integrity: sha512-/PU9P+7Rkz8JFYDHIi+xzHabOu9qEWR07L5nWLIUsvserrxegZExKCi2jhMZRd0ATdboKylu/K5yAXbp7fYFvA==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-powerpc64le-gnu@4.26.0: - resolution: {integrity: sha512-Odp/lgHbW/mAqw/pU21goo5ruWsytP7/HCC/liOt0zcGG0llYWKrd10k9Fj0pdj3prQ63N5yQLCLiE7HTX+MYw==} + /@rollup/rollup-linux-powerpc64le-gnu@4.27.2: + resolution: {integrity: sha512-eCHmol/dT5odMYi/N0R0HC8V8QE40rEpkyje/ZAXJYNNoSfrObOvG/Mn+s1F/FJyB7co7UQZZf6FuWnN6a7f4g==} cpu: [ppc64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-riscv64-gnu@4.26.0: - resolution: {integrity: sha512-MBR2ZhCTzUgVD0OJdTzNeF4+zsVogIR1U/FsyuFerwcqjZGvg2nYe24SAHp8O5sN8ZkRVbHwlYeHqcSQ8tcYew==} + /@rollup/rollup-linux-riscv64-gnu@4.27.2: + resolution: {integrity: sha512-DEP3Njr9/ADDln3kNi76PXonLMSSMiCir0VHXxmGSHxCxDfQ70oWjHcJGfiBugzaqmYdTC7Y+8Int6qbnxPBIQ==} cpu: [riscv64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-s390x-gnu@4.26.0: - resolution: {integrity: sha512-YYcg8MkbN17fMbRMZuxwmxWqsmQufh3ZJFxFGoHjrE7bv0X+T6l3glcdzd7IKLiwhT+PZOJCblpnNlz1/C3kGQ==} + /@rollup/rollup-linux-s390x-gnu@4.27.2: + resolution: {integrity: sha512-NHGo5i6IE/PtEPh5m0yw5OmPMpesFnzMIS/lzvN5vknnC1sXM5Z/id5VgcNPgpD+wHmIcuYYgW+Q53v+9s96lQ==} cpu: [s390x] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-gnu@4.26.0: - resolution: {integrity: sha512-ZuwpfjCwjPkAOxpjAEjabg6LRSfL7cAJb6gSQGZYjGhadlzKKywDkCUnJ+KEfrNY1jH5EEoSIKLCb572jSiglA==} + /@rollup/rollup-linux-x64-gnu@4.27.2: + resolution: {integrity: sha512-PaW2DY5Tan+IFvNJGHDmUrORadbe/Ceh8tQxi8cmdQVCCYsLoQo2cuaSj+AU+YRX8M4ivS2vJ9UGaxfuNN7gmg==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-musl@4.26.0: - resolution: {integrity: sha512-+HJD2lFS86qkeF8kNu0kALtifMpPCZU80HvwztIKnYwym3KnA1os6nsX4BGSTLtS2QVAGG1P3guRgsYyMA0Yhg==} + /@rollup/rollup-linux-x64-musl@4.27.2: + resolution: {integrity: sha512-dOlWEMg2gI91Qx5I/HYqOD6iqlJspxLcS4Zlg3vjk1srE67z5T2Uz91yg/qA8sY0XcwQrFzWWiZhMNERylLrpQ==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-arm64-msvc@4.26.0: - resolution: {integrity: sha512-WUQzVFWPSw2uJzX4j6YEbMAiLbs0BUysgysh8s817doAYhR5ybqTI1wtKARQKo6cGop3pHnrUJPFCsXdoFaimQ==} + /@rollup/rollup-win32-arm64-msvc@4.27.2: + resolution: {integrity: sha512-euMIv/4x5Y2/ImlbGl88mwKNXDsvzbWUlT7DFky76z2keajCtcbAsN9LUdmk31hAoVmJJYSThgdA0EsPeTr1+w==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-ia32-msvc@4.26.0: - resolution: {integrity: sha512-D4CxkazFKBfN1akAIY6ieyOqzoOoBV1OICxgUblWxff/pSjCA2khXlASUx7mK6W1oP4McqhgcCsu6QaLj3WMWg==} + /@rollup/rollup-win32-ia32-msvc@4.27.2: + resolution: {integrity: sha512-RsnE6LQkUHlkC10RKngtHNLxb7scFykEbEwOFDjr3CeCMG+Rr+cKqlkKc2/wJ1u4u990urRHCbjz31x84PBrSQ==} cpu: [ia32] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-x64-msvc@4.26.0: - resolution: {integrity: sha512-2x8MO1rm4PGEP0xWbubJW5RtbNLk3puzAMaLQd3B3JHVw4KcHlmXcO+Wewx9zCoo7EUFiMlu/aZbCJ7VjMzAag==} + /@rollup/rollup-win32-x64-msvc@4.27.2: + resolution: {integrity: sha512-foJM5vv+z2KQmn7emYdDLyTbkoO5bkHZE1oth2tWbQNGW7mX32d46Hz6T0MqXdWS2vBZhaEtHqdy9WYwGfiliA==} cpu: [x64] os: [win32] requiresBuild: true @@ -9852,51 +9852,51 @@ packages: '@types/hast': 3.0.4 dev: false - /@shikijs/core@1.22.2: - resolution: {integrity: sha512-bvIQcd8BEeR1yFvOYv6HDiyta2FFVePbzeowf5pPS1avczrPK+cjmaxxh0nx5QzbON7+Sv0sQfQVciO7bN72sg==} + /@shikijs/core@1.23.1: + resolution: {integrity: sha512-NuOVgwcHgVC6jBVH5V7iblziw6iQbWWHrj5IlZI3Fqu2yx9awH7OIQkXIcsHsUmY19ckwSgUMgrqExEyP5A0TA==} dependencies: - '@shikijs/engine-javascript': 1.22.2 - '@shikijs/engine-oniguruma': 1.22.2 - '@shikijs/types': 1.22.2 + '@shikijs/engine-javascript': 1.23.1 + '@shikijs/engine-oniguruma': 1.23.1 + '@shikijs/types': 1.23.1 '@shikijs/vscode-textmate': 9.3.0 '@types/hast': 3.0.4 hast-util-to-html: 9.0.3 dev: false - /@shikijs/engine-javascript@1.22.2: - resolution: {integrity: sha512-iOvql09ql6m+3d1vtvP8fLCVCK7BQD1pJFmHIECsujB0V32BJ0Ab6hxk1ewVSMFA58FI0pR2Had9BKZdyQrxTw==} + /@shikijs/engine-javascript@1.23.1: + resolution: {integrity: sha512-i/LdEwT5k3FVu07SiApRFwRcSJs5QM9+tod5vYCPig1Ywi8GR30zcujbxGQFJHwYD7A5BUqagi8o5KS+LEVgBg==} dependencies: - '@shikijs/types': 1.22.2 + '@shikijs/types': 1.23.1 '@shikijs/vscode-textmate': 9.3.0 - oniguruma-to-js: 0.4.3 + oniguruma-to-es: 0.4.1 dev: false - /@shikijs/engine-oniguruma@1.22.2: - resolution: {integrity: sha512-GIZPAGzQOy56mGvWMoZRPggn0dTlBf1gutV5TdceLCZlFNqWmuc7u+CzD0Gd9vQUTgLbrt0KLzz6FNprqYAxlA==} + /@shikijs/engine-oniguruma@1.23.1: + resolution: {integrity: sha512-KQ+lgeJJ5m2ISbUZudLR1qHeH3MnSs2mjFg7bnencgs5jDVPeJ2NVDJ3N5ZHbcTsOIh0qIueyAJnwg7lg7kwXQ==} dependencies: - '@shikijs/types': 1.22.2 + '@shikijs/types': 1.23.1 '@shikijs/vscode-textmate': 9.3.0 dev: false - /@shikijs/rehype@1.22.2: - resolution: {integrity: sha512-A0RHgiYR5uiHvddwHehBN9j8PhOvfT6/GebSTWrapur6M+fD/4i3mlfUv7aFK4b+4GQ1R42L8fC5N98whZjNcg==} + /@shikijs/rehype@1.23.1: + resolution: {integrity: sha512-PH5bpMDEc4nBP62Ci3lUqkxBWRTm8cdE+eY9er5QD50jAWQxhXcc1Aeax1AlyrASrtjTwCkI22M6N9iSn5p+bQ==} dependencies: - '@shikijs/types': 1.22.2 + '@shikijs/types': 1.23.1 '@types/hast': 3.0.4 hast-util-to-string: 3.0.1 - shiki: 1.22.2 + shiki: 1.23.1 unified: 11.0.5 unist-util-visit: 5.0.0 dev: false - /@shikijs/transformers@1.22.2: - resolution: {integrity: sha512-8f78OiBa6pZDoZ53lYTmuvpFPlWtevn23bzG+azpPVvZg7ITax57o/K3TC91eYL3OMJOO0onPbgnQyZjRos8XQ==} + /@shikijs/transformers@1.23.1: + resolution: {integrity: sha512-yQ2Cn0M9i46p30KwbyIzLvKDk+dQNU+lj88RGO0XEj54Hn4Cof1bZoDb9xBRWxFE4R8nmK63w7oHnJwvOtt0NQ==} dependencies: - shiki: 1.22.2 + shiki: 1.23.1 dev: false - /@shikijs/types@1.22.2: - resolution: {integrity: sha512-NCWDa6LGZqTuzjsGfXOBWfjS/fDIbDdmVDug+7ykVe1IKT4c1gakrvlfFYp5NhAXH/lyqLM8wsAPo5wNy73Feg==} + /@shikijs/types@1.23.1: + resolution: {integrity: sha512-98A5hGyEhzzAgQh2dAeHKrWW4HfCMeoFER2z16p5eJ+vmPeF6lZ/elEne6/UCU551F/WqkopqRsr1l2Yu6+A0g==} dependencies: '@shikijs/vscode-textmate': 9.3.0 '@types/hast': 3.0.4 @@ -10651,8 +10651,8 @@ packages: '@types/ssh2': 1.15.1 dev: true - /@types/dockerode@3.3.31: - resolution: {integrity: sha512-42R9eoVqJDSvVspV89g7RwRqfNExgievLNWoHkg7NoWIqAmavIbgQBb4oc0qRtHkxE+I3Xxvqv7qVXFABKPBTg==} + /@types/dockerode@3.3.32: + resolution: {integrity: sha512-xxcG0g5AWKtNyh7I7wswLdFvym4Mlqks5ZlKzxEUrGHS0r0PUOfxm2T0mspwu10mHQqu3Ck3MI3V2HqvLWE1fg==} dependencies: '@types/docker-modem': 3.0.6 '@types/node': 20.14.9 @@ -10880,6 +10880,13 @@ packages: '@types/prop-types': 15.7.13 csstype: 3.1.3 + /@types/readable-stream@4.0.18: + resolution: {integrity: sha512-21jK/1j+Wg+7jVw1xnSwy/2Q1VgVjWuFssbYGTREPUBeZ+rqVFl2udq0IkxzPC0ZhOzVceUbyIACFZKLqKEBlA==} + dependencies: + '@types/node': 20.14.9 + safe-buffer: 5.1.2 + dev: true + /@types/resolve@1.20.6: resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} dev: true @@ -11315,78 +11322,78 @@ packages: pretty-format: 29.7.0 dev: true - /@vue/compiler-core@3.5.12: - resolution: {integrity: sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==} + /@vue/compiler-core@3.5.13: + resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} dependencies: '@babel/parser': 7.26.2 - '@vue/shared': 3.5.12 + '@vue/shared': 3.5.13 entities: 4.5.0 estree-walker: 2.0.2 source-map-js: 1.2.1 dev: false - /@vue/compiler-dom@3.5.12: - resolution: {integrity: sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==} + /@vue/compiler-dom@3.5.13: + resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} dependencies: - '@vue/compiler-core': 3.5.12 - '@vue/shared': 3.5.12 + '@vue/compiler-core': 3.5.13 + '@vue/shared': 3.5.13 dev: false - /@vue/compiler-sfc@3.5.12: - resolution: {integrity: sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==} + /@vue/compiler-sfc@3.5.13: + resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==} dependencies: '@babel/parser': 7.26.2 - '@vue/compiler-core': 3.5.12 - '@vue/compiler-dom': 3.5.12 - '@vue/compiler-ssr': 3.5.12 - '@vue/shared': 3.5.12 + '@vue/compiler-core': 3.5.13 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 estree-walker: 2.0.2 magic-string: 0.30.12 postcss: 8.4.49 source-map-js: 1.2.1 dev: false - /@vue/compiler-ssr@3.5.12: - resolution: {integrity: sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==} + /@vue/compiler-ssr@3.5.13: + resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} dependencies: - '@vue/compiler-dom': 3.5.12 - '@vue/shared': 3.5.12 + '@vue/compiler-dom': 3.5.13 + '@vue/shared': 3.5.13 dev: false - /@vue/reactivity@3.5.12: - resolution: {integrity: sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==} + /@vue/reactivity@3.5.13: + resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} dependencies: - '@vue/shared': 3.5.12 + '@vue/shared': 3.5.13 dev: false - /@vue/runtime-core@3.5.12: - resolution: {integrity: sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==} + /@vue/runtime-core@3.5.13: + resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==} dependencies: - '@vue/reactivity': 3.5.12 - '@vue/shared': 3.5.12 + '@vue/reactivity': 3.5.13 + '@vue/shared': 3.5.13 dev: false - /@vue/runtime-dom@3.5.12: - resolution: {integrity: sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==} + /@vue/runtime-dom@3.5.13: + resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==} dependencies: - '@vue/reactivity': 3.5.12 - '@vue/runtime-core': 3.5.12 - '@vue/shared': 3.5.12 + '@vue/reactivity': 3.5.13 + '@vue/runtime-core': 3.5.13 + '@vue/shared': 3.5.13 csstype: 3.1.3 dev: false - /@vue/server-renderer@3.5.12(vue@3.5.12): - resolution: {integrity: sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==} + /@vue/server-renderer@3.5.13(vue@3.5.13): + resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==} peerDependencies: - vue: 3.5.12 + vue: 3.5.13 dependencies: - '@vue/compiler-ssr': 3.5.12 - '@vue/shared': 3.5.12 - vue: 3.5.12(typescript@5.5.3) + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + vue: 3.5.13(typescript@5.5.3) dev: false - /@vue/shared@3.5.12: - resolution: {integrity: sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==} + /@vue/shared@3.5.13: + resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} dev: false /@webassemblyjs/ast@1.14.1: @@ -11619,7 +11626,7 @@ packages: indent-string: 5.0.0 dev: true - /ai@3.4.7(openai@4.52.1)(react@18.3.1)(svelte@5.1.16)(vue@3.5.12)(zod@3.23.8): + /ai@3.4.7(openai@4.52.1)(react@18.3.1)(svelte@5.2.2)(vue@3.5.13)(zod@3.23.8): resolution: {integrity: sha512-SutkVjFE86+xNql7fJERJkSEwpILEuiQvCoogJef6ZX/PGHvu3yepwHwVwedgABXe9SudOIKN48EQESrXX/xCw==} engines: {node: '>=18'} peerDependencies: @@ -11644,9 +11651,9 @@ packages: '@ai-sdk/provider-utils': 1.0.20(zod@3.23.8) '@ai-sdk/react': 0.0.62(react@18.3.1)(zod@3.23.8) '@ai-sdk/solid': 0.0.49(zod@3.23.8) - '@ai-sdk/svelte': 0.0.51(svelte@5.1.16)(zod@3.23.8) + '@ai-sdk/svelte': 0.0.51(svelte@5.2.2)(zod@3.23.8) '@ai-sdk/ui-utils': 0.0.46(zod@3.23.8) - '@ai-sdk/vue': 0.0.53(vue@3.5.12)(zod@3.23.8) + '@ai-sdk/vue': 0.0.53(vue@3.5.13)(zod@3.23.8) '@opentelemetry/api': 1.4.1 eventsource-parser: 1.1.2 json-schema: 0.4.0 @@ -11655,7 +11662,7 @@ packages: openai: 4.52.1 react: 18.3.1 secure-json-parse: 2.7.0 - svelte: 5.1.16 + svelte: 5.2.2 zod: 3.23.8 zod-to-json-schema: 3.23.2(zod@3.23.8) transitivePeerDependencies: @@ -11902,7 +11909,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.4 + es-abstract: 1.23.5 es-shim-unscopables: 1.0.2 dev: true @@ -11913,7 +11920,7 @@ packages: array-buffer-byte-length: 1.0.1 call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.4 + es-abstract: 1.23.5 es-errors: 1.3.0 get-intrinsic: 1.2.4 is-array-buffer: 3.0.4 @@ -11974,16 +11981,6 @@ packages: resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} dev: true - /async-mqtt@2.6.3: - resolution: {integrity: sha512-mFGTtlEpOugOoLOf9H5AJyJaZUNtOVXLGGOnPaPZDPQex6W6iIOgtV+fAgam0GQbgnLfgX+Wn/QzS6d+PYfFAQ==} - dependencies: - mqtt: 4.3.8 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: true - /async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} dev: true @@ -12201,6 +12198,15 @@ packages: readable-stream: 3.6.2 dev: true + /bl@6.0.16: + resolution: {integrity: sha512-V/kz+z2Mx5/6qDfRCilmrukUXcXuCoXKg3/3hDvzKKoSUx8CJKudfIoT29XZc3UE9xBvxs5qictiHdprwtteEg==} + dependencies: + '@types/readable-stream': 4.0.18 + buffer: 6.0.3 + inherits: 2.0.4 + readable-stream: 4.5.2 + dev: true + /blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} dev: true @@ -12258,7 +12264,7 @@ packages: hasBin: true dependencies: caniuse-lite: 1.0.30001680 - electron-to-chromium: 1.5.58 + electron-to-chromium: 1.5.62 node-releases: 2.0.18 update-browserslist-db: 1.1.1(browserslist@4.24.2) @@ -12514,8 +12520,8 @@ packages: get-func-name: 2.0.2 dev: true - /checkly@4.9.1(@types/node@20.14.9)(typescript@5.5.3): - resolution: {integrity: sha512-BLVLb9EB8iOQVNZIRUjoEYVPdzOSiBwPlbNOYXOWDoMZHo6dUxWB4ckqJhqWfrvmVPEnSw6JMQ0v/XAXaGYmqw==} + /checkly@4.10.0(@types/node@20.14.9)(typescript@5.5.3): + resolution: {integrity: sha512-3URsPR3Jv7nm/ONBPj13aj/hFioQZz+b5Vyl8UbG4c3yP7HjJjx6/mGQyxfPNffOzrKcmbQgzZY/r6l0fN0Hzw==} engines: {node: '>=16.0.0'} hasBin: true dependencies: @@ -12527,7 +12533,6 @@ packages: '@typescript-eslint/typescript-estree': 6.19.0(typescript@5.5.3) acorn: 8.8.1 acorn-walk: 8.2.0 - async-mqtt: 2.6.3 axios: 1.7.4 chalk: 4.1.2 ci-info: 3.8.0 @@ -12540,6 +12545,7 @@ packages: jwt-decode: 3.1.2 log-symbols: 4.1.0 luxon: 3.3.0 + mqtt: 5.10.1 open: 8.4.0 p-queue: 6.6.2 prompts: 2.4.2 @@ -12927,11 +12933,8 @@ packages: engines: {node: '>= 12'} dev: true - /commist@1.1.0: - resolution: {integrity: sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==} - dependencies: - leven: 2.1.0 - minimist: 1.2.8 + /commist@3.2.0: + resolution: {integrity: sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==} dev: true /commitizen@4.3.1(@types/node@20.14.9)(typescript@5.5.3): @@ -13907,7 +13910,7 @@ packages: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} dependencies: no-case: 3.0.4 - tslib: 2.8.1 + tslib: 2.4.1 dev: false /dot-prop@6.0.1: @@ -14363,15 +14366,6 @@ packages: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} dev: false - /duplexify@4.1.3: - resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} - dependencies: - end-of-stream: 1.4.4 - inherits: 2.0.4 - readable-stream: 3.6.2 - stream-shift: 1.0.3 - dev: true - /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -14406,8 +14400,12 @@ packages: jake: 10.9.2 dev: true - /electron-to-chromium@1.5.58: - resolution: {integrity: sha512-al2l4r+24ZFL7WzyPTlyD0fC33LLzvxqLCwurtBibVPghRGO9hSTl+tis8t1kD7biPiH/en4U0I7o/nQbYeoVA==} + /electron-to-chromium@1.5.62: + resolution: {integrity: sha512-t8c+zLmJHa9dJy96yBZRXGQYoiCEnHYgFwn1asvSPZSUdVxnB62A4RASd7k41ytG3ErFBA0TpHlKg9D9SQBmLg==} + + /emoji-regex-xs@1.0.0: + resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + dev: false /emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} @@ -14537,8 +14535,8 @@ packages: is-arrayish: 0.2.1 dev: true - /es-abstract@1.23.4: - resolution: {integrity: sha512-HR1gxH5OaiN7XH7uiWH0RLw0RcFySiSoW1ctxmD1ahTw3uGBtkmm/ng0tDU1OtYx5OK6EOL5Y6O21cDflG3Jcg==} + /es-abstract@1.23.5: + resolution: {integrity: sha512-vlmniQ0WNPwXqA0BnmwV3Ng7HxiGlh6r5U6JcTMNx8OilcAGqVJBHJcPjqOMaczU9fRuRK5Px2BdVyPRnKMMVQ==} engines: {node: '>= 0.4'} dependencies: array-buffer-byte-length: 1.0.1 @@ -14894,31 +14892,31 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - /eslint-config-prettier@9.0.0(eslint@9.14.0): + /eslint-config-prettier@9.0.0(eslint@9.15.0): resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} hasBin: true peerDependencies: eslint: '>=7.0.0' dependencies: - eslint: 9.14.0 + eslint: 9.15.0 dev: false - /eslint-config-turbo@1.10.12(eslint@9.14.0): + /eslint-config-turbo@1.10.12(eslint@9.15.0): resolution: {integrity: sha512-z3jfh+D7UGYlzMWGh+Kqz++hf8LOE96q3o5R8X4HTjmxaBWlLAWG+0Ounr38h+JLR2TJno0hU9zfzoPNkR9BdA==} peerDependencies: eslint: '>6.6.0' dependencies: - eslint: 9.14.0 - eslint-plugin-turbo: 1.10.12(eslint@9.14.0) + eslint: 9.15.0 + eslint-plugin-turbo: 1.10.12(eslint@9.15.0) dev: false - /eslint-plugin-turbo@1.10.12(eslint@9.14.0): + /eslint-plugin-turbo@1.10.12(eslint@9.15.0): resolution: {integrity: sha512-uNbdj+ohZaYo4tFJ6dStRXu2FZigwulR1b3URPXe0Q8YaE7thuekKNP+54CHtZPH9Zey9dmDx5btAQl9mfzGOw==} peerDependencies: eslint: '>6.6.0' dependencies: dotenv: 16.0.3 - eslint: 9.14.0 + eslint: 9.15.0 dev: false /eslint-scope@5.1.1: @@ -14946,8 +14944,8 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dev: false - /eslint@9.14.0: - resolution: {integrity: sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==} + /eslint@9.15.0: + resolution: {integrity: sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -14956,13 +14954,13 @@ packages: jiti: optional: true dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.18.0 - '@eslint/core': 0.7.0 - '@eslint/eslintrc': 3.1.0 - '@eslint/js': 9.14.0 - '@eslint/plugin-kit': 0.2.2 + '@eslint/config-array': 0.19.0 + '@eslint/core': 0.9.0 + '@eslint/eslintrc': 3.2.0 + '@eslint/js': 9.15.0 + '@eslint/plugin-kit': 0.2.3 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.1 @@ -14990,7 +14988,6 @@ packages: minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 - text-table: 0.2.0 transitivePeerDependencies: - supports-color dev: false @@ -15207,7 +15204,7 @@ packages: is-plain-obj: 4.1.0 is-stream: 4.0.1 npm-run-path: 5.3.0 - pretty-ms: 9.1.0 + pretty-ms: 9.2.0 signal-exit: 4.1.0 strip-final-newline: 4.0.0 yoctocolors: 2.1.1 @@ -15225,7 +15222,7 @@ packages: is-plain-obj: 4.1.0 is-stream: 4.0.1 npm-run-path: 6.0.0 - pretty-ms: 9.1.0 + pretty-ms: 9.2.0 signal-exit: 4.1.0 strip-final-newline: 4.0.0 yoctocolors: 2.1.1 @@ -15314,7 +15311,7 @@ packages: engines: {node: '>= 10.17.0'} hasBin: true dependencies: - debug: 4.3.7(supports-color@8.1.1) + debug: 4.3.4 get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -15362,6 +15359,14 @@ packages: resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} dev: false + /fast-unique-numbers@8.0.13: + resolution: {integrity: sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==} + engines: {node: '>=16.1.0'} + dependencies: + '@babel/runtime': 7.26.0 + tslib: 2.8.1 + dev: true + /fast-uri@3.0.3: resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==} dev: true @@ -15781,9 +15786,9 @@ packages: react: '>= 18' react-dom: '>= 18' dependencies: - '@formatjs/intl-localematcher': 0.5.7 - '@shikijs/rehype': 1.22.2 - '@shikijs/transformers': 1.22.2 + '@formatjs/intl-localematcher': 0.5.8 + '@shikijs/rehype': 1.23.1 + '@shikijs/transformers': 1.23.1 flexsearch: 0.7.21 github-slugger: 2.0.0 image-size: 1.1.1 @@ -15797,7 +15802,7 @@ packages: remark-gfm: 4.0.0 remark-mdx: 3.1.0 scroll-into-view-if-needed: 3.1.0 - shiki: 1.22.2 + shiki: 1.23.1 swr: 2.2.5(react@18.3.1) unist-util-visit: 5.0.0 transitivePeerDependencies: @@ -15870,7 +15875,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.4 + es-abstract: 1.23.5 functions-have-names: 1.2.3 /functions-have-names@1.2.3: @@ -16267,7 +16272,7 @@ packages: decircular: 0.1.1 is-obj: 3.0.0 sort-keys: 5.1.0 - type-fest: 4.26.1 + type-fest: 4.27.0 dev: false /hasown@2.0.2: @@ -16402,8 +16407,8 @@ packages: zwitch: 2.0.4 dev: true - /hast-util-raw@9.0.4: - resolution: {integrity: sha512-LHE65TD2YiNsHD3YuXcKPHXPLuYh/gjp12mOfU8jxSrm1f/yJpsb0F/KKljS6U9LJoP0Ux+tCe8iJ2AsPzTdgA==} + /hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} dependencies: '@types/hast': 3.0.4 '@types/unist': 3.0.3 @@ -16620,11 +16625,8 @@ packages: resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} dev: true - /help-me@3.0.0: - resolution: {integrity: sha512-hx73jClhyk910sidBB7ERlnhMlFsJJIBqSVMFDwPN8o2v9nmp5KgLq1Xz1Bf1fCMMZ6mPrX159iG0VLy/fPMtQ==} - dependencies: - glob: 7.2.3 - readable-stream: 3.6.2 + /help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} dev: true /hex-rgb@4.3.0: @@ -17639,11 +17641,6 @@ packages: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} dev: false - /leven@2.1.0: - resolution: {integrity: sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==} - engines: {node: '>=0.10.0'} - dev: true - /levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -17921,7 +17918,7 @@ packages: /lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: - tslib: 2.8.1 + tslib: 2.4.1 dev: false /lowercase-keys@3.0.0: @@ -17951,13 +17948,6 @@ packages: dependencies: yallist: 3.1.1 - /lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - dependencies: - yallist: 4.0.0 - dev: true - /lru-cache@7.18.3: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} @@ -19334,7 +19324,6 @@ packages: /minipass@6.0.2: resolution: {integrity: sha512-MzWSV5nYVT7mVyWCwn2o7JH13w2TBRmmSqSRCKzTw+lmft9X4z+3wjvs06Tzijo5z4W/kahUCDpRXTF+ZrmF/w==} engines: {node: '>=16 || 14 >=14.17'} - dev: true /minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} @@ -19460,38 +19449,37 @@ packages: /module-details-from-path@1.0.3: resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==} - /mqtt-packet@6.10.0: - resolution: {integrity: sha512-ja8+mFKIHdB1Tpl6vac+sktqy3gA8t9Mduom1BA75cI+R9AHnZOiaBQwpGiWnaVJLDGRdNhQmFaAqd7tkKSMGA==} + /mqtt-packet@9.0.1: + resolution: {integrity: sha512-koZF1V/X2RZUI6uD9wN5OK1JxxcG1ofAR4H3LjCw1FkeKzruZQ26aAA6v2m1lZyWONZIR5wMMJFrZJDRNzbiQw==} dependencies: - bl: 4.1.0 + bl: 6.0.16 debug: 4.3.7(supports-color@8.1.1) process-nextick-args: 2.0.1 transitivePeerDependencies: - supports-color dev: true - /mqtt@4.3.8: - resolution: {integrity: sha512-2xT75uYa0kiPEF/PE0VPdavmEkoBzMT/UL9moid0rAvlCtV48qBwxD62m7Ld/4j8tSkIO1E/iqRl/S72SEOhOw==} - engines: {node: '>=10.0.0'} + /mqtt@5.10.1: + resolution: {integrity: sha512-hXCOki8sANoQ7w+2OzJzg6qMBxTtrH9RlnVNV8panLZgnl+Gh0J/t4k6r8Az8+C7y3KAcyXtn0mmLixyUom8Sw==} + engines: {node: '>=16.0.0'} hasBin: true dependencies: - commist: 1.1.0 + '@types/readable-stream': 4.0.18 + '@types/ws': 8.5.13 + commist: 3.2.0 concat-stream: 2.0.0 debug: 4.3.7(supports-color@8.1.1) - duplexify: 4.1.3 - help-me: 3.0.0 - inherits: 2.0.4 - lru-cache: 6.0.0 + help-me: 5.0.0 + lru-cache: 10.4.3 minimist: 1.2.8 - mqtt-packet: 6.10.0 + mqtt-packet: 9.0.1 number-allocator: 1.0.14 - pump: 3.0.2 - readable-stream: 3.6.2 + readable-stream: 4.5.2 reinterval: 1.1.0 rfdc: 1.4.1 - split2: 3.2.2 - ws: 7.5.10 - xtend: 4.0.2 + split2: 4.2.0 + worker-timers: 7.1.8 + ws: 8.18.0 transitivePeerDependencies: - bufferutil - supports-color @@ -19543,7 +19531,7 @@ packages: outvariant: 1.4.3 path-to-regexp: 6.3.0 strict-event-emitter: 0.5.1 - type-fest: 4.26.1 + type-fest: 4.27.0 typescript: 5.5.3 yargs: 17.7.2 dev: true @@ -19749,7 +19737,7 @@ packages: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} dependencies: lower-case: 2.0.2 - tslib: 2.8.1 + tslib: 2.4.1 dev: false /node-addon-api@7.1.1: @@ -20082,10 +20070,12 @@ packages: mimic-function: 5.0.1 dev: true - /oniguruma-to-js@0.4.3: - resolution: {integrity: sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==} + /oniguruma-to-es@0.4.1: + resolution: {integrity: sha512-rNcEohFz095QKGRovP/yqPIKc+nP+Sjs4YTHMv33nMePGKrq/r2eu9Yh4646M5XluGJsUnmwoXuiXE69KDs+fQ==} dependencies: - regex: 4.4.0 + emoji-regex-xs: 1.0.0 + regex: 5.0.1 + regex-recursion: 4.2.1 dev: false /open@8.4.0: @@ -20474,7 +20464,7 @@ packages: engines: {node: '>=16 || 14 >=14.18'} dependencies: lru-cache: 10.4.3 - minipass: 7.1.2 + minipass: 6.0.2 /path-to-regexp@0.1.10: resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==} @@ -20783,8 +20773,8 @@ packages: parse-ms: 4.0.0 dev: true - /pretty-ms@9.1.0: - resolution: {integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==} + /pretty-ms@9.2.0: + resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==} engines: {node: '>=18'} dependencies: parse-ms: 4.0.0 @@ -21014,6 +21004,14 @@ packages: engines: {node: '>=0.6'} dependencies: side-channel: 1.0.6 + dev: true + + /qs@6.13.1: + resolution: {integrity: sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.6 + dev: false /querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} @@ -21132,7 +21130,7 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: true - /react-email@2.1.1(@babel/core@7.26.0)(eslint@9.14.0)(ts-node@10.9.2): + /react-email@2.1.1(@babel/core@7.26.0)(eslint@9.15.0)(ts-node@10.9.2): resolution: {integrity: sha512-09oMVl/jN0/Re0bT0sEqYjyyFSCN/Tg0YmzjC9wfYpnMx02Apk40XXitySDfUBMR9EgTdr6T4lYknACqiLK3mg==} engines: {node: '>=18.0.0'} hasBin: true @@ -21158,8 +21156,8 @@ packages: commander: 11.1.0 debounce: 2.0.0 esbuild: 0.19.11 - eslint-config-prettier: 9.0.0(eslint@9.14.0) - eslint-config-turbo: 1.10.12(eslint@9.14.0) + eslint-config-prettier: 9.0.0(eslint@9.15.0) + eslint-config-turbo: 1.10.12(eslint@9.15.0) framer-motion: 10.17.4(react-dom@18.3.1)(react@18.3.1) glob: 10.3.4 log-symbols: 4.1.0 @@ -21649,8 +21647,18 @@ packages: /regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - /regex@4.4.0: - resolution: {integrity: sha512-uCUSuobNVeqUupowbdZub6ggI5/JZkYyJdDogddJr60L764oxC2pMZov1fQ3wM9bdyzUILDG+Sqx6NAKAz9rKQ==} + /regex-recursion@4.2.1: + resolution: {integrity: sha512-QHNZyZAeKdndD1G3bKAbBEKOSSK4KOHQrAJ01N1LJeb0SoH4DJIeFhp0uUpETgONifS4+P3sOgoA1dhzgrQvhA==} + dependencies: + regex-utilities: 2.3.0 + dev: false + + /regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + dev: false + + /regex@5.0.1: + resolution: {integrity: sha512-gIS00E8eHNWONxofNKOhtlkwBQj/K39ZJamnvMEFH3pNKc06Zz2jtFXF/4ldAaJTzQNhMJU7b5+C7tTq2ukV7Q==} dev: false /regexp.prototype.flags@1.5.3: @@ -21724,7 +21732,7 @@ packages: resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} dependencies: '@types/hast': 3.0.4 - hast-util-raw: 9.0.4 + hast-util-raw: 9.1.0 vfile: 6.0.3 dev: false @@ -22135,31 +22143,31 @@ packages: source-map-support: 0.3.3 dev: false - /rollup@4.26.0: - resolution: {integrity: sha512-ilcl12hnWonG8f+NxU6BlgysVA0gvY2l8N0R84S1HcINbW20bvwuCngJkkInV6LXhwRpucsW5k1ovDwEdBVrNg==} + /rollup@4.27.2: + resolution: {integrity: sha512-KreA+PzWmk2yaFmZVwe6GB2uBD86nXl86OsDkt1bJS9p3vqWuEQ6HnJJ+j/mZi/q0920P99/MVRlB4L3crpF5w==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true dependencies: '@types/estree': 1.0.6 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.26.0 - '@rollup/rollup-android-arm64': 4.26.0 - '@rollup/rollup-darwin-arm64': 4.26.0 - '@rollup/rollup-darwin-x64': 4.26.0 - '@rollup/rollup-freebsd-arm64': 4.26.0 - '@rollup/rollup-freebsd-x64': 4.26.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.26.0 - '@rollup/rollup-linux-arm-musleabihf': 4.26.0 - '@rollup/rollup-linux-arm64-gnu': 4.26.0 - '@rollup/rollup-linux-arm64-musl': 4.26.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.26.0 - '@rollup/rollup-linux-riscv64-gnu': 4.26.0 - '@rollup/rollup-linux-s390x-gnu': 4.26.0 - '@rollup/rollup-linux-x64-gnu': 4.26.0 - '@rollup/rollup-linux-x64-musl': 4.26.0 - '@rollup/rollup-win32-arm64-msvc': 4.26.0 - '@rollup/rollup-win32-ia32-msvc': 4.26.0 - '@rollup/rollup-win32-x64-msvc': 4.26.0 + '@rollup/rollup-android-arm-eabi': 4.27.2 + '@rollup/rollup-android-arm64': 4.27.2 + '@rollup/rollup-darwin-arm64': 4.27.2 + '@rollup/rollup-darwin-x64': 4.27.2 + '@rollup/rollup-freebsd-arm64': 4.27.2 + '@rollup/rollup-freebsd-x64': 4.27.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.27.2 + '@rollup/rollup-linux-arm-musleabihf': 4.27.2 + '@rollup/rollup-linux-arm64-gnu': 4.27.2 + '@rollup/rollup-linux-arm64-musl': 4.27.2 + '@rollup/rollup-linux-powerpc64le-gnu': 4.27.2 + '@rollup/rollup-linux-riscv64-gnu': 4.27.2 + '@rollup/rollup-linux-s390x-gnu': 4.27.2 + '@rollup/rollup-linux-x64-gnu': 4.27.2 + '@rollup/rollup-linux-x64-musl': 4.27.2 + '@rollup/rollup-win32-arm64-msvc': 4.27.2 + '@rollup/rollup-win32-ia32-msvc': 4.27.2 + '@rollup/rollup-win32-x64-msvc': 4.27.2 fsevents: 2.3.3 dev: true @@ -22465,13 +22473,13 @@ packages: '@types/hast': 3.0.4 dev: false - /shiki@1.22.2: - resolution: {integrity: sha512-3IZau0NdGKXhH2bBlUk4w1IHNxPh6A5B2sUpyY+8utLu2j/h1QpFkAaUA1bAMxOWWGtTWcAh531vnS4NJKS/lA==} + /shiki@1.23.1: + resolution: {integrity: sha512-8kxV9TH4pXgdKGxNOkrSMydn1Xf6It8lsle0fiqxf7a1149K1WGtdOu3Zb91T5r1JpvRPxqxU3C2XdZZXQnrig==} dependencies: - '@shikijs/core': 1.22.2 - '@shikijs/engine-javascript': 1.22.2 - '@shikijs/engine-oniguruma': 1.22.2 - '@shikijs/types': 1.22.2 + '@shikijs/core': 1.23.1 + '@shikijs/engine-javascript': 1.23.1 + '@shikijs/engine-oniguruma': 1.23.1 + '@shikijs/types': 1.23.1 '@shikijs/vscode-textmate': 9.3.0 '@types/hast': 3.0.4 dev: false @@ -22578,7 +22586,7 @@ packages: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} dependencies: dot-case: 3.0.4 - tslib: 2.8.1 + tslib: 2.4.1 dev: false /snakecase-keys@3.2.1: @@ -22799,10 +22807,9 @@ packages: resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} dev: true - /split2@3.2.2: - resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} - dependencies: - readable-stream: 3.6.2 + /split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} dev: true /sprintf-js@1.0.3: @@ -22831,12 +22838,12 @@ packages: nan: 2.22.0 dev: true - /sswr@2.1.0(svelte@5.1.16): + /sswr@2.1.0(svelte@5.2.2): resolution: {integrity: sha512-Cqc355SYlTAaUt8iDPaC/4DPPXK925PePLMxyBKuWd5kKc5mwsG3nT9+Mq2tyguL5s7b4Jg+IRMpTRsNTAfpSQ==} peerDependencies: svelte: ^4.0.0 || ^5.0.0-next.0 dependencies: - svelte: 5.1.16 + svelte: 5.2.2 swrev: 4.0.0 dev: false @@ -22879,10 +22886,6 @@ packages: engines: {node: '>=4', npm: '>=6'} dev: true - /stream-shift@1.0.3: - resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} - dev: true - /stream-transform@2.1.3: resolution: {integrity: sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==} dependencies: @@ -22951,7 +22954,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.4 + es-abstract: 1.23.5 es-object-atoms: 1.0.0 /string.prototype.trimend@1.0.8: @@ -23059,7 +23062,7 @@ packages: engines: {node: '>=12.*'} dependencies: '@types/node': 20.14.9 - qs: 6.13.0 + qs: 6.13.1 dev: false /style-to-object@0.4.4: @@ -23177,8 +23180,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - /svelte@5.1.16: - resolution: {integrity: sha512-QcY+om9r8+uTcSfeFuv8++ExdfwVCKeT+Y7GPSZ6rQPczvy62BMtvMoi0rScabgv+upGE5jxKjd7M4u23+AjGA==} + /svelte@5.2.2: + resolution: {integrity: sha512-eHIJRcvA6iuXdRGMESTmBtWTQCcCiol4gyH9DA60ybS35W1x27cvtbndNvWDqX72blyf+AYeQ4gzZ0XGg3L8sw==} engines: {node: '>=18'} dependencies: '@ampproject/remapping': 2.3.0 @@ -23240,12 +23243,12 @@ packages: resolution: {integrity: sha512-LqVcOHSB4cPGgitD1riJ1Hh4vdmITOp+BkmfmXRh4hSF/t7EnS4iD+SOTmq7w5pPm/SiPeto4ADbKS6dHUDWFA==} dev: false - /swrv@1.0.4(vue@3.5.12): + /swrv@1.0.4(vue@3.5.13): resolution: {integrity: sha512-zjEkcP8Ywmj+xOJW3lIT65ciY/4AL4e/Or7Gj0MzU3zBJNMdJiT8geVZhINavnlHRMMCcJLHhraLTAiDOTmQ9g==} peerDependencies: vue: '>=3.2.26 < 4' dependencies: - vue: 3.5.12(typescript@5.5.3) + vue: 3.5.13(typescript@5.5.3) dev: false /tailwind-merge@2.2.0: @@ -23494,7 +23497,7 @@ packages: resolution: {integrity: sha512-8fReFeQ4bk17T2vHHzcFavBG8UHuHwsdVj+48TchtsCSklwmSUTkg/b57hVjxZdxN1ed/GfF63WZ39I4syV5tQ==} dependencies: '@balena/dockerignore': 1.0.2 - '@types/dockerode': 3.3.31 + '@types/dockerode': 3.3.32 archiver: 7.0.1 async-lock: 1.4.1 byline: 5.0.0 @@ -23516,10 +23519,6 @@ packages: resolution: {integrity: sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==} dev: true - /text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - dev: false - /thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -23799,7 +23798,7 @@ packages: joycon: 3.1.1 postcss-load-config: 4.0.2(postcss@8.4.49)(ts-node@10.9.2) resolve-from: 5.0.0 - rollup: 4.26.0 + rollup: 4.27.2 source-map: 0.8.0-beta.0 sucrase: 3.35.0 tree-kill: 1.2.2 @@ -23959,8 +23958,8 @@ packages: engines: {node: '>=12.20'} dev: false - /type-fest@4.26.1: - resolution: {integrity: sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==} + /type-fest@4.27.0: + resolution: {integrity: sha512-3IMSWgP7C5KSQqmo1wjhKrwsvXAtF33jO3QY+Uy++ia7hqvgSK6iXbbg5PbDBc1P2ZbNEDgejOrN4YooXvhwCw==} engines: {node: '>=16'} /type-is@1.6.18: @@ -24581,7 +24580,7 @@ packages: '@types/node': 20.14.9 esbuild: 0.21.5 postcss: 8.4.49 - rollup: 4.26.0 + rollup: 4.27.2 optionalDependencies: fsevents: 2.3.3 dev: true @@ -24714,19 +24713,19 @@ packages: resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} dev: false - /vue@3.5.12(typescript@5.5.3): - resolution: {integrity: sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==} + /vue@3.5.13(typescript@5.5.3): + resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==} peerDependencies: typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@vue/compiler-dom': 3.5.12 - '@vue/compiler-sfc': 3.5.12 - '@vue/runtime-dom': 3.5.12 - '@vue/server-renderer': 3.5.12(vue@3.5.12) - '@vue/shared': 3.5.12 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-sfc': 3.5.13 + '@vue/runtime-dom': 3.5.13 + '@vue/server-renderer': 3.5.13(vue@3.5.13) + '@vue/shared': 3.5.13 typescript: 5.5.3 dev: false @@ -24951,6 +24950,31 @@ packages: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} dev: true + /worker-timers-broker@6.1.8: + resolution: {integrity: sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==} + dependencies: + '@babel/runtime': 7.26.0 + fast-unique-numbers: 8.0.13 + tslib: 2.8.1 + worker-timers-worker: 7.0.71 + dev: true + + /worker-timers-worker@7.0.71: + resolution: {integrity: sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==} + dependencies: + '@babel/runtime': 7.26.0 + tslib: 2.8.1 + dev: true + + /worker-timers@7.1.8: + resolution: {integrity: sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==} + dependencies: + '@babel/runtime': 7.26.0 + tslib: 2.8.1 + worker-timers-broker: 6.1.8 + worker-timers-worker: 7.0.71 + dev: true + /workerd@1.20240524.0: resolution: {integrity: sha512-LWLe5D8PVHBcqturmBbwgI71r7YPpIMYZoVEH6S4G35EqIJ55cb0n3FipoSyraoIfpcCxCFxX1K6WsRHbP3pFA==} engines: {node: '>=16'} @@ -25094,6 +25118,7 @@ packages: optional: true utf-8-validate: optional: true + dev: false /ws@8.13.0: resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} @@ -25157,6 +25182,7 @@ packages: /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + dev: false /xterm-for-react@1.0.4(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-DCkLR9ZXeW907YyyaCTk/3Ol34VRHfCnf3MAPOkj3dUNA85sDqHvTXN8efw4g7bx7gWdJQRsEpGt2tJOXKG3EQ==}