From b0dae1b7909148ffc284f9b3934e471212cf28cf Mon Sep 17 00:00:00 2001 From: Aleksy Date: Sun, 1 Dec 2024 15:55:31 +0000 Subject: [PATCH] feat: api keys support for CLI/backend/auth (#1387) * feat: api keys support for CLI/backend/auth * fix(backend): test execution fix * fix: changed api key id to uuid * chore: Add changeset --------- Co-authored-by: Mohamad Mohebifar --- .changeset/eight-bees-exist.md | 5 + apps/auth-service/.env.example | 3 + apps/auth-service/package.json | 1 + apps/auth-service/src/server.ts | 45 +++++++ apps/backend/.env.example | 5 +- apps/backend/package.json | 3 + .../src/handlers/createAPIKeyHandler.ts | 35 +++++ .../src/handlers/deleteAPIKeysHandler.ts | 40 ++++++ .../src/handlers/listAPIKeysHandler.ts | 25 ++++ apps/backend/src/server.ts | 26 +++- apps/backend/src/services/UnkeyService.ts | 56 ++++++++ apps/cli/src/api.ts | 86 +++++++++++-- apps/cli/src/commands/api-keys.ts | 83 ++++++++++++ apps/cli/src/commands/publish.ts | 64 +++++++--- apps/cli/src/main.ts | 88 ++++++++++++- packages/api-types/package.json | 3 +- packages/api-types/src/api-keys.ts | 38 ++++++ packages/api-types/src/index.ts | 1 + packages/auth/src/plugin.ts | 42 +++--- .../20241125130309_api_keys/migration.sql | 8 ++ packages/database/prisma/schema.prisma | 7 + pnpm-lock.yaml | 120 +++++++++++++----- pnpm-workspace.yaml | 1 + 23 files changed, 707 insertions(+), 78 deletions(-) create mode 100644 .changeset/eight-bees-exist.md create mode 100644 apps/backend/src/handlers/createAPIKeyHandler.ts create mode 100644 apps/backend/src/handlers/deleteAPIKeysHandler.ts create mode 100644 apps/backend/src/handlers/listAPIKeysHandler.ts create mode 100644 apps/backend/src/services/UnkeyService.ts create mode 100644 apps/cli/src/commands/api-keys.ts create mode 100644 packages/api-types/src/api-keys.ts create mode 100644 packages/database/prisma/migrations/20241125130309_api_keys/migration.sql diff --git a/.changeset/eight-bees-exist.md b/.changeset/eight-bees-exist.md new file mode 100644 index 000000000..42c3f27bb --- /dev/null +++ b/.changeset/eight-bees-exist.md @@ -0,0 +1,5 @@ +--- +"codemod": minor +--- + +Add API keys functionality diff --git a/apps/auth-service/.env.example b/apps/auth-service/.env.example index 186c404cf..bff5b220f 100644 --- a/apps/auth-service/.env.example +++ b/apps/auth-service/.env.example @@ -4,3 +4,6 @@ CLERK_SECRET_KEY= CLERK_JWT_KEY= CLERK_PUBLISH_KEY= CLI_TOKEN_TEMPLATE= + +UNKEY_ROOT_KEY= +UNKEY_API_ID= diff --git a/apps/auth-service/package.json b/apps/auth-service/package.json index a72aef5b2..89f6de717 100644 --- a/apps/auth-service/package.json +++ b/apps/auth-service/package.json @@ -25,6 +25,7 @@ "@clerk/fastify": "catalog:", "@codemod-com/auth": "workspace:*", "@codemod-com/database": "workspace:*", + "@unkey/api": "catalog:", "@codemod-com/utilities": "workspace:*", "@fastify/busboy": "catalog:", "@fastify/cors": "catalog:", diff --git a/apps/auth-service/src/server.ts b/apps/auth-service/src/server.ts index 1aa78164c..b37e9e9ea 100644 --- a/apps/auth-service/src/server.ts +++ b/apps/auth-service/src/server.ts @@ -12,11 +12,25 @@ import type { } from "@codemod-com/api-types"; import { isNeitherNullNorUndefined } from "@codemod-com/utilities"; +import { Unkey } from "@unkey/api"; import { createLoginIntent } from "./handlers/intents/create.js"; import { getLoginIntent } from "./handlers/intents/get.js"; import { populateLoginIntent } from "./handlers/intents/populate.js"; import { environment } from "./util.js"; +const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY as string }); +const apiId = process.env.UNKEY_API_ID as string; + +const getUserId = async (key: string) => { + const response = await unkey.keys.verify({ apiId, key }); + + if (response.error) { + throw new Error(response.error.message); + } + + return response.result.identity?.externalId ?? response.result.ownerId; +}; + export const initApp = async (toRegister: FastifyPluginCallback[]) => { const { PORT: port } = environment; if (Number.isNaN(port)) { @@ -168,6 +182,37 @@ const routes: FastifyPluginCallback = (instance, _opts, done) => { }); }); + instance.get("/apiUserData", async (request, reply) => { + const apiKey = request.headers["x-api-key"] as string; + const userId = await getUserId(apiKey); + + if (!userId) { + return reply.status(200).send({}); + } + + const user = await clerkClient.users.getUser(userId); + const organizations = ( + await clerkClient.users.getOrganizationMembershipList({ userId }) + ).data.map((organization) => organization); + const allowedNamespaces = [ + ...organizations.map(({ organization }) => organization.slug), + ].filter(isNeitherNullNorUndefined); + + if (user.username) { + allowedNamespaces.unshift(user.username); + + if (environment.VERIFIED_PUBLISHERS.includes(user.username)) { + allowedNamespaces.push("codemod-com"); + } + } + + return reply.status(200).send({ + user, + organizations, + allowedNamespaces, + }); + }); + instance.delete<{ Reply: RevokeScopedTokenResponse }>( "/revokeToken", async (request, reply) => { diff --git a/apps/backend/.env.example b/apps/backend/.env.example index 4588a1fa0..f28ae5b7c 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -13,4 +13,7 @@ TASK_MANAGER_QUEUE_NAME= POSTHOG_API_KEY= POSTHOG_PROJECT_ID= -ZAPIER_PUBLISH_HOOK= \ No newline at end of file +ZAPIER_PUBLISH_HOOK= + +UNKEY_ROOT_KEY= +UNKEY_API_ID= diff --git a/apps/backend/package.json b/apps/backend/package.json index 9fcc38cea..711ac2992 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -43,6 +43,7 @@ "@fastify/rate-limit": "catalog:", "@slack/web-api": "catalog:", "@types/tar": "catalog:", + "@unkey/api": "catalog:", "ai": "2.2.29", "axios": "catalog:", "bullmq": "catalog:", @@ -54,6 +55,7 @@ "fuse.js": "catalog:", "ioredis": "catalog:", "langchain": "catalog:", + "lodash-es": "catalog:", "openai": "catalog:", "openai-edge": "catalog:", "parse-github-url": "catalog:", @@ -61,6 +63,7 @@ "replicate": "catalog:", "semver": "7.6.0", "tar": "^6.2.0", + "uuid": "^11.0.3", "valibot": "catalog:", "ws": "^8.18.0", "zod": "catalog:" diff --git a/apps/backend/src/handlers/createAPIKeyHandler.ts b/apps/backend/src/handlers/createAPIKeyHandler.ts new file mode 100644 index 000000000..0126c3a20 --- /dev/null +++ b/apps/backend/src/handlers/createAPIKeyHandler.ts @@ -0,0 +1,35 @@ +import { + type CreateAPIKeyResponse, + createAPIKeyRequestSchema, +} from "@codemod-com/api-types"; +import type { UserDataPopulatedRequest } from "@codemod-com/auth"; +import { prisma } from "@codemod-com/database"; +import type { RouteHandler } from "fastify"; +import { v4 as uuidv4 } from "uuid"; +import { parse } from "valibot"; +import { createApiKey } from "../services/UnkeyService.js"; + +export const createAPIKeyHandler: RouteHandler<{ + Reply: CreateAPIKeyResponse; +}> = async (request: UserDataPopulatedRequest) => { + const user = request.user!; + + const uuid = uuidv4(); + + const apiKey = await createApiKey({ + externalId: user.id, + apiKeyData: parse(createAPIKeyRequestSchema, request.body), + uuid, + }); + + await prisma.apiKey.create({ + data: { + externalId: user.id, + uuid, + keyId: apiKey.keyId, + }, + }); + + const reply: CreateAPIKeyResponse = { key: apiKey.key, uuid }; + return reply; +}; diff --git a/apps/backend/src/handlers/deleteAPIKeysHandler.ts b/apps/backend/src/handlers/deleteAPIKeysHandler.ts new file mode 100644 index 000000000..1202608ee --- /dev/null +++ b/apps/backend/src/handlers/deleteAPIKeysHandler.ts @@ -0,0 +1,40 @@ +import { + type DeleteAPIKeysResponse, + deleteAPIKeysRequestSchema, +} from "@codemod-com/api-types"; +import type { UserDataPopulatedRequest } from "@codemod-com/auth"; +import { prisma } from "@codemod-com/database"; +import type { RouteHandler } from "fastify"; +import { parse } from "valibot"; +import { deleteApiKeys, listApiKeys } from "../services/UnkeyService.js"; + +export const deleteAPIKeysHandler: RouteHandler<{ + Reply: DeleteAPIKeysResponse; +}> = async (request: UserDataPopulatedRequest) => { + const user = request.user!; + + const { uuid } = parse(deleteAPIKeysRequestSchema, request.params); + + const keysToDelete = await prisma.apiKey.findMany({ + where: { externalId: user.id, uuid }, + }); + + const keysInfo = await listApiKeys({ externalId: user.id }); + + await deleteApiKeys({ keyIds: keysToDelete.map((key) => key.keyId) }); + + await prisma.apiKey.deleteMany({ + where: { + externalId: user.id, + keyId: { in: keysToDelete.map((key) => key.keyId) }, + }, + }); + + const reply: DeleteAPIKeysResponse = { + keys: keysToDelete + .map(({ uuid }) => keysInfo.keys.find((k) => k.meta?.uuid === uuid)) + .filter((key) => !!key) + .map(({ start, name }) => ({ start, name })), + }; + return reply; +}; diff --git a/apps/backend/src/handlers/listAPIKeysHandler.ts b/apps/backend/src/handlers/listAPIKeysHandler.ts new file mode 100644 index 000000000..4cdab9b48 --- /dev/null +++ b/apps/backend/src/handlers/listAPIKeysHandler.ts @@ -0,0 +1,25 @@ +import type { ListAPIKeysResponse } from "@codemod-com/api-types"; +import type { UserDataPopulatedRequest } from "@codemod-com/auth"; +import type { RouteHandler } from "fastify"; +import { listApiKeys } from "../services/UnkeyService.js"; + +export const listAPIKeysHandler: RouteHandler<{ + Reply: ListAPIKeysResponse; +}> = async (request: UserDataPopulatedRequest) => { + const user = request.user!; + + const apiKeys = await listApiKeys({ externalId: user.id }).then( + ({ keys }) => keys, + ); + + const reply: ListAPIKeysResponse = { + keys: apiKeys.map(({ start, name, createdAt, expires, meta }) => ({ + start, + name, + createdAt, + expiresAt: expires, + uuid: meta?.uuid as string | undefined, + })), + }; + return reply; +}; diff --git a/apps/backend/src/server.ts b/apps/backend/src/server.ts index 33137d4d4..94c389b29 100644 --- a/apps/backend/src/server.ts +++ b/apps/backend/src/server.ts @@ -1,5 +1,10 @@ import { randomBytes } from "node:crypto"; -import type { CodemodListResponse } from "@codemod-com/api-types"; +import type { + CodemodListResponse, + CreateAPIKeyResponse, + DeleteAPIKeysResponse, + ListAPIKeysResponse, +} from "@codemod-com/api-types"; import { getAuthPlugin } from "@codemod-com/auth"; import { prisma } from "@codemod-com/database"; import { decryptWithIv, encryptWithIv } from "@codemod-com/utilities"; @@ -10,6 +15,8 @@ import Fastify, { type FastifyPluginCallback, type FastifyRequest, } from "fastify"; +import { createAPIKeyHandler } from "./handlers/createAPIKeyHandler.js"; +import { deleteAPIKeysHandler } from "./handlers/deleteAPIKeysHandler.js"; import { type GetCodemodDownloadLinkResponse, getCodemodDownloadLink, @@ -20,6 +27,7 @@ import { getCodemodsHandler, } from "./handlers/getCodemodsHandler.js"; import { getCodemodsListHandler } from "./handlers/getCodemodsListHandler.js"; +import { listAPIKeysHandler } from "./handlers/listAPIKeysHandler.js"; import { type PublishHandlerResponse, publishHandler, @@ -169,6 +177,22 @@ const routes: FastifyPluginCallback = (instance, _opts, done) => { }, ); + instance.post<{ + Reply: CreateAPIKeyResponse; + }>("/api-keys", { preHandler: instance.getUserData }, createAPIKeyHandler); + + instance.get<{ + Reply: ListAPIKeysResponse; + }>("/api-keys", { preHandler: instance.getUserData }, listAPIKeysHandler); + + instance.delete<{ + Reply: DeleteAPIKeysResponse; + }>( + "/api-keys/:uuid", + { preHandler: instance.getUserData }, + deleteAPIKeysHandler, + ); + instance.get("/codemods/:criteria", getCodemodHandler); instance.get<{ Reply: GetCodemodsResponse }>( diff --git a/apps/backend/src/services/UnkeyService.ts b/apps/backend/src/services/UnkeyService.ts new file mode 100644 index 000000000..2c1b79647 --- /dev/null +++ b/apps/backend/src/services/UnkeyService.ts @@ -0,0 +1,56 @@ +import type { CreateAPIKeyRequest } from "@codemod-com/api-types"; +import { Unkey } from "@unkey/api"; +import { memoize } from "lodash-es"; + +const UNKEY_API_ID = process.env.UNKEY_API_ID as string; +const UNKEY_ROOT_KEY = process.env.UNKEY_ROOT_KEY as string; + +const getUnkey = memoize(() => new Unkey({ rootKey: UNKEY_ROOT_KEY })); + +export const createApiKey = async ({ + apiKeyData, + externalId, + uuid, +}: { apiKeyData: CreateAPIKeyRequest; externalId: string; uuid: string }) => { + const response = await getUnkey().keys.create({ + apiId: UNKEY_API_ID, + prefix: "codemod.com", + externalId, + name: apiKeyData.name, + expires: apiKeyData.expiresAt + ? Date.parse(apiKeyData.expiresAt) + : undefined, + meta: { + uuid, + }, + }); + + if (response.error) { + throw new Error(response.error.message); + } + + return response.result; +}; + +export const listApiKeys = async ({ externalId }: { externalId: string }) => { + const response = await getUnkey().apis.listKeys({ + apiId: UNKEY_API_ID, + externalId, + }); + + if (response.error) { + throw new Error(response.error.message); + } + + return response.result; +}; + +export const deleteApiKeys = async ({ keyIds }: { keyIds: string[] }) => { + await Promise.all( + keyIds.map(async (keyId) => + getUnkey().keys.delete({ + keyId, + }), + ), + ); +}; diff --git a/apps/cli/src/api.ts b/apps/cli/src/api.ts index b711e6931..8119efdbd 100644 --- a/apps/cli/src/api.ts +++ b/apps/cli/src/api.ts @@ -4,9 +4,14 @@ import Axios, { AxiosError, type RawAxiosRequestHeaders } from "axios"; import type { CodemodDownloadLinkResponse, CodemodListResponse, + CreateAPIKeyRequest, + CreateAPIKeyResponse, + DeleteAPIKeysRequest, + DeleteAPIKeysResponse, GetCodemodResponse, GetScopedTokenResponse, GetUserDataResponse, + ListAPIKeysResponse, VerifyTokenResponse, } from "@codemod-com/api-types"; @@ -18,6 +23,50 @@ export const extractPrintableApiError = (err: unknown): string => { return err instanceof AxiosError ? err.response?.data.errorText : err.message; }; +export const createAPIKey = async ( + accessToken: string, + data: CreateAPIKeyRequest, +): Promise => { + const url = new URL(`${process.env.BACKEND_URL}/api-keys`); + + const res = await Axios.post(url.toString(), data, { + headers: { Authorization: `Bearer ${accessToken}` }, + timeout: 10000, + }); + + return res.data; +}; + +export const listAPIKeys = async ( + accessToken: string, +): Promise => { + const url = new URL(`${process.env.BACKEND_URL}/api-keys`); + + const res = await Axios.get(url.toString(), { + headers: { Authorization: `Bearer ${accessToken}` }, + timeout: 10000, + }); + + return res.data; +}; + +export const deleteAPIKeys = async ( + accessToken: string, + data: DeleteAPIKeysRequest, +): Promise => { + const url = new URL(`${process.env.BACKEND_URL}/api-keys`); + + const res = await Axios.delete( + `${url.toString()}/${data.uuid}`, + { + headers: { Authorization: `Bearer ${accessToken}` }, + timeout: 10000, + }, + ); + + return res.data; +}; + export const getCLIAccessToken = async ( accessToken: string, ): Promise => { @@ -68,14 +117,25 @@ export const getUserData = async ( }; export const publish = async ( - accessToken: string, + credentials: + | { + apiKey: string; + accessToken?: undefined; + } + | { + accessToken: string; + apiKey?: undefined; + }, formData: FormData, ): Promise => { + const headers: RawAxiosRequestHeaders = {}; + if (credentials.accessToken) { + headers.Authorization = `Bearer ${credentials.accessToken}`; + } else if (credentials.apiKey) { + headers["X-API-Key"] = credentials.apiKey; + } await Axios.post(`${process.env.BACKEND_URL}/publish`, formData, { - headers: { - Authorization: `Bearer ${accessToken}`, - "Content-Type": "multipart/form-data", - }, + headers, timeout: 10000, }); }; @@ -103,13 +163,23 @@ export const revokeCLIToken = async (accessToken: string): Promise => { export const getCodemod = async ( name: string, - accessToken?: string, + credentials: + | { + apiKey: string; + accessToken?: undefined; + } + | { + accessToken: string; + apiKey?: undefined; + }, ): Promise => { const url = new URL(`${process.env.BACKEND_URL}/codemods/${name}`); const headers: RawAxiosRequestHeaders = {}; - if (accessToken) { - headers.Authorization = `Bearer ${accessToken}`; + if (credentials.accessToken) { + headers.Authorization = `Bearer ${credentials.accessToken}`; + } else if (credentials.apiKey) { + headers["X-API-Key"] = credentials.apiKey; } const res = await Axios.get(url.toString(), { diff --git a/apps/cli/src/commands/api-keys.ts b/apps/cli/src/commands/api-keys.ts new file mode 100644 index 000000000..95563a970 --- /dev/null +++ b/apps/cli/src/commands/api-keys.ts @@ -0,0 +1,83 @@ +import type { + CreateAPIKeyRequest, + DeleteAPIKeysRequest, +} from "@codemod-com/api-types"; +import type { Printer } from "@codemod-com/printer"; +import { createAPIKey, deleteAPIKeys, listAPIKeys } from "#api.js"; +import { getCurrentUserOrLogin } from "#auth-utils.js"; + +export const handleCreateAPIKeyCommand = async (options: { + printer: Printer; + data: CreateAPIKeyRequest; +}) => { + const { printer } = options; + + const { token } = await getCurrentUserOrLogin({ + printer, + message: "You need to log in to be able create API keys", + }); + + const apiKey = await createAPIKey(token, options.data); + + printer.printConsoleMessage( + "info", + ` +${options.data.name ?? ""} +API key with id ${apiKey.uuid} created successfully: +${apiKey.key} +Please store this key in a safe place, as it will not be shown again.`, + ); +}; + +export const handleListAPIKeysCommand = async (options: { + printer: Printer; +}) => { + const { printer } = options; + + const { token } = await getCurrentUserOrLogin({ + printer, + message: "You need to log in to be able create API keys", + }); + + const { keys } = await listAPIKeys(token); + + if (keys.length === 0) { + printer.printConsoleMessage("info", "No API keys found"); + return; + } + + printer.printConsoleMessage( + "info", + ` +API keys: +${keys.map(({ name, start, createdAt, expiresAt, uuid }) => ` - ${[name, `${start}...`, `created at ${new Date(createdAt).toISOString()}`, expiresAt ? `expires at ${new Date(expiresAt).toISOString()}` : undefined, uuid ? `id: ${uuid}` : undefined].filter((info) => !!info).join("\n ")}`).join("\n")} +`, + ); +}; + +export const handleDeleteAPIKeysCommand = async (options: { + printer: Printer; + data: DeleteAPIKeysRequest; +}) => { + const { printer, data } = options; + + const { token } = await getCurrentUserOrLogin({ + printer, + message: "You need to log in to be able create API keys", + }); + + const { keys } = await deleteAPIKeys(token, data); + + if (keys.length === 0) { + printer.printConsoleMessage("info", "No API keys were deleted"); + return; + } + + printer.printConsoleMessage( + "info", + ` +Next API keys were deleted: +${keys.map(({ name, start }) => ` - ${[name, `${start}...`].filter((info) => !!info).join("\n ")}`).join("\n")} +`, + ); +}; diff --git a/apps/cli/src/commands/publish.ts b/apps/cli/src/commands/publish.ts index 4f6aa527e..cff9d4c69 100644 --- a/apps/cli/src/commands/publish.ts +++ b/apps/cli/src/commands/publish.ts @@ -26,20 +26,59 @@ import { handleInitCliCommand } from "#commands/init.js"; import type { TelemetryEvent } from "#telemetry.js"; import { isFile } from "#utils/general.js"; +const API_KEY = process.env.CODEMOD_API_KEY; + export const handlePublishCliCommand = async (options: { printer: Printer; source: string; telemetry: TelemetrySender; esm?: boolean; + namespace?: string; }) => { let { source, printer, telemetry, esm } = options; - const { token, allowedNamespaces, organizations } = - await getCurrentUserOrLogin({ + const requestCredentials = await (async () => { + if (API_KEY) { + return { apiKey: API_KEY }; + } + + const { token } = await getCurrentUserOrLogin({ message: "Authentication is required to publish codemods. Proceed?", printer, }); + return { accessToken: token }; + })(); + + const getNamespace = async ( + codemodRc: NonNullable>["config"]>, + ) => { + if (API_KEY) { + return options.namespace; + } + + const { allowedNamespaces, organizations } = await getCurrentUserOrLogin({ + message: "Authentication is required to publish codemods. Proceed?", + printer, + }); + + if (allowedNamespaces.length > 1 && !codemodRc.name.startsWith("@")) { + const { namespace } = await inquirer.prompt<{ namespace: string }>({ + type: "list", + name: "namespace", + choices: allowedNamespaces, + default: allowedNamespaces.find( + (ns) => + !organizations.map((org) => org.organization.slug).includes(ns), + ), + message: + "You have access to multiple namespaces. Please choose which one you would like to publish the codemod under.", + }); + + return namespace; + } + }; + const formData = new FormData(); const excludedPaths = [ "node_modules/**", @@ -118,7 +157,7 @@ export const handlePublishCliCommand = async (options: { let bumpedVersion = false; const existingCodemod = await getCodemod( buildCodemodSlug(codemodRc.name), - token, + requestCredentials, ).catch(() => null); if (existingCodemod !== null) { @@ -131,19 +170,12 @@ export const handlePublishCliCommand = async (options: { await updateCodemodRC(codemodRc); bumpedVersion = true; } - } else if (allowedNamespaces.length > 1 && !codemodRc.name.startsWith("@")) { - const { namespace } = await inquirer.prompt<{ namespace: string }>({ - type: "list", - name: "namespace", - choices: allowedNamespaces, - default: allowedNamespaces.find( - (ns) => !organizations.map((org) => org.organization.slug).includes(ns), - ), - message: - "You have access to multiple namespaces. Please choose which one you would like to publish the codemod under.", - }); + } else { + const namespace = await getNamespace(codemodRc); - formData.append("namespace", namespace); + if (namespace) { + formData.append("namespace", namespace); + } } if (codemodRc.engine !== "recipe") { @@ -305,7 +337,7 @@ export const handlePublishCliCommand = async (options: { ); try { - await publish(token, formData); + await publish(requestCredentials, formData); publishSpinner.succeed(); } catch (error) { publishSpinner.fail(); diff --git a/apps/cli/src/main.ts b/apps/cli/src/main.ts index 891edfd7b..c1cbf273a 100644 --- a/apps/cli/src/main.ts +++ b/apps/cli/src/main.ts @@ -11,6 +11,11 @@ import { } from "@codemod-com/telemetry"; import { doubleQuotify, execPromise } from "@codemod-com/utilities"; import { version } from "#/../package.json"; +import { + handleCreateAPIKeyCommand, + handleDeleteAPIKeysCommand, + handleListAPIKeysCommand, +} from "#commands/api-keys.js"; import { handleFeedbackCommand } from "#commands/feedback.js"; import { handleInitCliCommand } from "#commands/init.js"; import { handleLearnCliCommand } from "#commands/learn.js"; @@ -247,13 +252,87 @@ export const main = async () => { }, ) .command( - "publish", - "publish the codemod to Codemod Registry", + "api-keys:create", + "create a new API key", (y) => - y.option("source", { + y + .option("name", { + type: "string", + description: + "A way to easily identify the key by giving it a name.", + demandOption: false, + }) + .option("expiresAt", { + type: "string", + description: + "The date and time when the key will expire. Format: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format", + demandOption: false, + }), + async (args) => { + const { executeCliCommand, printer } = + await initializeDependencies(args); + + return executeCliCommand(async () => { + await handleCreateAPIKeyCommand({ + printer, + data: { + name: args.name, + expiresAt: args.expiresAt, + }, + }); + }); + }, + ) + .command( + "api-keys:list", + "list API keys", + (y) => y, + async (args) => { + const { executeCliCommand, printer } = + await initializeDependencies(args); + + return executeCliCommand(async () => { + await handleListAPIKeysCommand({ + printer, + }); + }); + }, + ) + .command( + "api-keys:delete", + "list API keys", + (y) => + y.option("id", { type: "string", - description: "path to the codemod to be published", + description: "Key id", + demandOption: true, }), + async (args) => { + const { executeCliCommand, printer } = + await initializeDependencies(args); + + return executeCliCommand(async () => { + await handleDeleteAPIKeysCommand({ + printer, + data: { uuid: args.id }, + }); + }); + }, + ) + .command( + "publish", + "publish the codemod to Codemod Registry", + (y) => + y + .option("source", { + type: "string", + description: "path to the codemod to be published", + }) + .option("namespace", { + type: "string", + description: "namespace to publish the codemod under", + demandOption: false, + }), async (args) => { const { executeCliCommand, printer, telemetryService } = await initializeDependencies(args); @@ -264,6 +343,7 @@ export const main = async () => { source: args.source ?? process.cwd(), telemetry: telemetryService, esm: args.esm, + namespace: args.namespace, }); }); }, diff --git a/packages/api-types/package.json b/packages/api-types/package.json index 934227bbd..19c16dc45 100644 --- a/packages/api-types/package.json +++ b/packages/api-types/package.json @@ -31,6 +31,7 @@ "dependencies": { "@clerk/backend": "catalog:", "@codemod-com/database": "workspace:*", - "@codemod-com/filemod": "workspace:*" + "@codemod-com/filemod": "workspace:*", + "valibot": "catalog:" } } diff --git a/packages/api-types/src/api-keys.ts b/packages/api-types/src/api-keys.ts new file mode 100644 index 000000000..7589e667a --- /dev/null +++ b/packages/api-types/src/api-keys.ts @@ -0,0 +1,38 @@ +import { type InferInput, object, optional, string } from "valibot"; + +export const createAPIKeyRequestSchema = object({ + name: optional(string()), + /** + * Format: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format + */ + expiresAt: optional(string()), +}); + +export const deleteAPIKeysRequestSchema = object({ + uuid: string(), +}); + +export type DeleteAPIKeysRequest = InferInput< + typeof deleteAPIKeysRequestSchema +>; + +export type CreateAPIKeyRequest = InferInput; + +export type CreateAPIKeyResponse = { key: string; uuid: string }; + +export type ListAPIKeysResponse = { + keys: { + start: string; + name?: string; + createdAt: number; + expiresAt?: number; + uuid?: string; + }[]; +}; + +export type DeleteAPIKeysResponse = { + keys: { + start: string; + name?: string; + }[]; +}; diff --git a/packages/api-types/src/index.ts b/packages/api-types/src/index.ts index dc9c72495..4ceea6843 100644 --- a/packages/api-types/src/index.ts +++ b/packages/api-types/src/index.ts @@ -2,3 +2,4 @@ export * from "./errors.js"; export * from "./responses.js"; export * from "./github.js"; export * from "./clerk.js"; +export * from "./api-keys.js"; diff --git a/packages/auth/src/plugin.ts b/packages/auth/src/plugin.ts index 09f12e450..80e7deae1 100644 --- a/packages/auth/src/plugin.ts +++ b/packages/auth/src/plugin.ts @@ -64,25 +64,37 @@ export async function getAuthPlugin(authBackendUrl: string) { ) => { try { const authHeader = request.headers.authorization; - - if (!authHeader) { + const apiKey = request.headers["x-api-key"] as string; + + if (authHeader) { + const { data } = await axios.get(`${authBackendUrl}/userData`, { + headers: { + Authorization: authHeader, + }, + }); + + const { user, organizations, allowedNamespaces } = data; + + request.user = user; + request.organizations = organizations; + request.allowedNamespaces = allowedNamespaces; + } else if (apiKey) { + const { data } = await axios.get(`${authBackendUrl}/apiUserData`, { + headers: { + "x-api-key": apiKey, + }, + }); + + const { user, organizations, allowedNamespaces } = data; + + request.user = user; + request.organizations = organizations; + request.allowedNamespaces = allowedNamespaces; + } else { request.user = undefined; request.organizations = undefined; request.allowedNamespaces = undefined; - return; } - - const { data } = await axios.get(`${authBackendUrl}/userData`, { - headers: { - Authorization: authHeader, - }, - }); - - const { user, organizations, allowedNamespaces } = data; - - request.user = user; - request.organizations = organizations; - request.allowedNamespaces = allowedNamespaces; } catch (error) { console.error(error); reply.code(401).send({ error: "Unauthorized" }); diff --git a/packages/database/prisma/migrations/20241125130309_api_keys/migration.sql b/packages/database/prisma/migrations/20241125130309_api_keys/migration.sql new file mode 100644 index 000000000..b6de4389b --- /dev/null +++ b/packages/database/prisma/migrations/20241125130309_api_keys/migration.sql @@ -0,0 +1,8 @@ +-- CreateTable +CREATE TABLE "ApiKey" ( + "externalId" VARCHAR(32) NOT NULL, + "uuid" VARCHAR(36) NOT NULL, + "keyId" VARCHAR(32) NOT NULL, + + CONSTRAINT "ApiKey_pkey" PRIMARY KEY ("uuid","keyId") +); diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma index c41f22ae5..2994f1430 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -91,3 +91,10 @@ model CodeDiff { createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt } + +model ApiKey { + externalId String @db.VarChar(32) + uuid String @db.VarChar(36) + keyId String @db.VarChar(32) + @@id([uuid, keyId]) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a9f57e54..8fa7747e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -303,6 +303,9 @@ catalogs: '@types/yargs': specifier: ^17.0.13 version: 17.0.32 + '@unkey/api': + specifier: ^0.26.2 + version: 0.26.2 '@vercel/analytics': specifier: ^1.2.2 version: 1.3.1 @@ -927,6 +930,9 @@ importers: '@fastify/rate-limit': specifier: 'catalog:' version: 9.0.1 + '@unkey/api': + specifier: 'catalog:' + version: 0.26.2 axios: specifier: 'catalog:' version: 1.7.2 @@ -1006,6 +1012,9 @@ importers: '@types/tar': specifier: 'catalog:' version: 6.1.13 + '@unkey/api': + specifier: 'catalog:' + version: 0.26.2 ai: specifier: 2.2.29 version: 2.2.29(react@18.2.0)(solid-js@1.8.17)(svelte@4.2.18)(vue@3.4.30(typescript@5.5.4)) @@ -1039,6 +1048,9 @@ importers: langchain: specifier: 'catalog:' version: 0.0.209(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/client-s3@3.600.0)(@aws-sdk/credential-provider-node@3.600.0(@aws-sdk/client-sso-oidc@3.600.0)(@aws-sdk/client-sts@3.600.0(@aws-sdk/client-sso-oidc@3.600.0)))(@smithy/util-utf8@2.3.0)(axios@1.7.2)(cheerio@1.0.0-rc.12)(chromadb@1.7.2(openai@4.23.0))(ignore@5.3.1)(ioredis@5.4.1)(jsdom@23.2.0)(jsonwebtoken@9.0.2)(lodash@4.17.21)(openai@4.23.0)(pg@8.12.0)(replicate@0.25.2)(ws@8.18.0) + lodash-es: + specifier: 'catalog:' + version: 4.17.21 openai: specifier: 'catalog:' version: 4.23.0 @@ -1060,6 +1072,9 @@ importers: tar: specifier: ^6.2.0 version: 6.2.1 + uuid: + specifier: ^11.0.3 + version: 11.0.3 valibot: specifier: 'catalog:' version: 0.34.0 @@ -2123,6 +2138,9 @@ importers: '@codemod-com/filemod': specifier: workspace:* version: link:../filemod + valibot: + specifier: 'catalog:' + version: 0.34.0 devDependencies: prettier: specifier: ^3.2.5 @@ -2172,7 +2190,7 @@ importers: version: 0.11.11 jscodeshift: specifier: ^0.16.1 - version: 0.16.1(@babel/preset-env@7.24.7) + version: 0.16.1(@babel/preset-env@7.24.7(@babel/core@7.24.7)) devDependencies: '@types/node': specifier: 20.10.3 @@ -2988,7 +3006,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -3009,7 +3027,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -3030,7 +3048,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -3051,7 +3069,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -3074,7 +3092,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -3095,7 +3113,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -3116,7 +3134,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -3137,7 +3155,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -3599,7 +3617,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -3622,7 +3640,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -3667,7 +3685,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -3688,7 +3706,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -3709,7 +3727,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -3730,7 +3748,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -3751,7 +3769,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -7018,7 +7036,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -7039,7 +7057,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -7060,7 +7078,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -7081,7 +7099,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -7102,7 +7120,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -7123,7 +7141,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -7146,7 +7164,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -7167,7 +7185,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -7260,7 +7278,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -7281,7 +7299,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -7304,7 +7322,7 @@ importers: devDependencies: '@codemod.com/codemod-utils': specifier: '*' - version: 1.0.0(@babel/preset-env@7.24.7) + version: 1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)) '@types/jscodeshift': specifier: ^0.11.10 version: 0.11.11 @@ -7472,7 +7490,7 @@ importers: version: 4.1.0 jscodeshift: specifier: ^0.16.1 - version: 0.16.1(@babel/preset-env@7.24.7) + version: 0.16.1(@babel/preset-env@7.24.7(@babel/core@7.24.7)) mdast-util-from-markdown: specifier: 'catalog:' version: 2.0.1 @@ -8814,6 +8832,10 @@ packages: resolution: {integrity: sha512-3wRZSs7jiFaB8AjxiiD+VqN5DTG2iRvJGQ+qYFrs/654lg6kGTQWIOFjlBo5RaXuAZjBmP3+OQH4dmhqiiyYxw==} engines: {node: '>=6.9.0'} + '@babel/types@7.22.5': + resolution: {integrity: sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==} + engines: {node: '>=6.9.0'} + '@babel/types@7.24.7': resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} engines: {node: '>=6.9.0'} @@ -13324,6 +13346,15 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + '@unkey/api@0.26.2': + resolution: {integrity: sha512-huHF5AbX1NL2MJHSfsO1mB9CSA1CgTlgdPUGZgYCll3BOCKRneYiZ2GgYGiWFbtYaUimjaZgHyGnH9fpQlDFjA==} + + '@unkey/error@0.2.0': + resolution: {integrity: sha512-DFGb4A7SrusZPP0FYuRIF0CO+Gi4etLUAEJ6EKc+TKYmscL0nEJ2Pr38FyX9MvjI4Wx5l35Wc9KsBjMm9Ybh7w==} + + '@unkey/rbac@0.3.1': + resolution: {integrity: sha512-Hj+52XRIlBBl3/qOUq9K71Fwy3PWExBQOpOClVYHdrcmbgqNL6L4EdW/BzliLhqPCdwZTPVSJTnZ3Hw4ZYixsQ==} + '@vercel/analytics@1.3.1': resolution: {integrity: sha512-xhSlYgAuJ6Q4WQGkzYTLmXwhYl39sWjoMA3nHxfkvG+WdBT25c563a7QhwwKivEOZtPJXifYHR1m2ihoisbWyA==} peerDependencies: @@ -21425,6 +21456,10 @@ packages: util@0.12.5: resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + uuid@11.0.3: + resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -22901,7 +22936,7 @@ snapshots: '@babel/helper-split-export-declaration@7.24.7': dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.22.5 '@babel/helper-string-parser@7.24.7': {} @@ -23654,6 +23689,12 @@ snapshots: '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 + '@babel/types@7.22.5': + dependencies: + '@babel/helper-string-parser': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + '@babel/types@7.24.7': dependencies: '@babel/helper-string-parser': 7.24.7 @@ -24159,11 +24200,11 @@ snapshots: style-mod: 4.1.2 w3c-keyname: 2.2.8 - '@codemod.com/codemod-utils@1.0.0(@babel/preset-env@7.24.7)': + '@codemod.com/codemod-utils@1.0.0(@babel/preset-env@7.24.7(@babel/core@7.24.7))': dependencies: '@babel/parser': 7.24.7 '@types/jscodeshift': 0.11.11 - jscodeshift: 0.16.1(@babel/preset-env@7.24.7) + jscodeshift: 0.16.1(@babel/preset-env@7.24.7(@babel/core@7.24.7)) transitivePeerDependencies: - '@babel/preset-env' - supports-color @@ -28440,6 +28481,19 @@ snapshots: '@ungap/structured-clone@1.2.0': {} + '@unkey/api@0.26.2': + dependencies: + '@unkey/rbac': 0.3.1 + + '@unkey/error@0.2.0': + dependencies: + zod: 3.23.8 + + '@unkey/rbac@0.3.1': + dependencies: + '@unkey/error': 0.2.0 + zod: 3.23.8 + '@vercel/analytics@1.3.1(next@14.2.4(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)': dependencies: server-only: 0.0.1 @@ -32792,7 +32846,7 @@ snapshots: transitivePeerDependencies: - supports-color - jscodeshift@0.16.1(@babel/preset-env@7.24.7): + jscodeshift@0.16.1(@babel/preset-env@7.24.7(@babel/core@7.24.7)): dependencies: '@babel/core': 7.24.7 '@babel/parser': 7.24.7 @@ -38224,6 +38278,8 @@ snapshots: is-typed-array: 1.1.13 which-typed-array: 1.1.15 + uuid@11.0.3: {} + uuid@8.3.2: {} uuid@9.0.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a7cc81b72..61e820c9c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -103,6 +103,7 @@ catalog: "@types/vscode": ^1.74.0 "@types/vscode-webview": ^1.57.1 "@types/yargs": ^17.0.13 + "@unkey/api": "^0.26.2" "@vercel/analytics": ^1.2.2 "@vercel/stega": ^0.1.0 "@vitejs/plugin-react": ^4.0.0