diff --git a/apps/web/app/not-found/[domain]/page.tsx b/apps/web/app/notfoundlink/page.tsx similarity index 79% rename from apps/web/app/not-found/[domain]/page.tsx rename to apps/web/app/notfoundlink/page.tsx index bb8f91e5ca..91e3b95d94 100644 --- a/apps/web/app/not-found/[domain]/page.tsx +++ b/apps/web/app/notfoundlink/page.tsx @@ -1,7 +1,5 @@ -import { getDomainViaEdge } from "@/lib/planetscale/get-domain-via-edge"; import { Background, Footer, Nav, NavMobile } from "@dub/ui"; import { constructMetadata } from "@dub/utils"; -import { redirect } from "next/navigation"; export const runtime = "edge"; @@ -12,17 +10,7 @@ export const metadata = constructMetadata({ noIndex: true, }); -export default async function NotFoundLinkPage({ - params, -}: { - params: { domain: string }; -}) { - const domain = await getDomainViaEdge(params.domain); - - if (domain?.notFoundUrl) { - redirect(domain.notFoundUrl); - } - +export default async function NotFoundLinkPage() { return (
diff --git a/apps/web/lib/middleware/link.ts b/apps/web/lib/middleware/link.ts index 9ab4d7d99b..9ef71fc255 100644 --- a/apps/web/lib/middleware/link.ts +++ b/apps/web/lib/middleware/link.ts @@ -23,6 +23,7 @@ import { userAgent, } from "next/server"; import { getLinkViaEdge } from "../planetscale"; +import { getDomainViaEdge } from "../planetscale/get-domain-via-edge"; import { RedisLinkProps } from "../types"; export default async function LinkMiddleware( @@ -56,11 +57,20 @@ export default async function LinkMiddleware( const linkData = await getLinkViaEdge(domain, key); if (!linkData) { - // short link not found, rewrite to not-found page - // TODO: log 404s (https://github.com/dubinc/dub/issues/559) - return NextResponse.rewrite(new URL(`/not-found/${domain}`, req.url), { - headers: DUB_HEADERS, - }); + // check if domain has notFoundUrl configured + const domainData = await getDomainViaEdge(domain); + if (domainData?.notFoundUrl) { + return NextResponse.redirect(domainData.notFoundUrl, { + headers: { + ...DUB_HEADERS, + "X-Robots-Tag": "googlebot: noindex", + }, + }); + } else { + return NextResponse.rewrite(new URL("/notfoundlink", req.url), { + headers: DUB_HEADERS, + }); + } } // format link to fit the RedisLinkProps interface diff --git a/apps/web/scripts/update-not-found.ts b/apps/web/scripts/update-not-found.ts new file mode 100644 index 0000000000..63006b1b4d --- /dev/null +++ b/apps/web/scripts/update-not-found.ts @@ -0,0 +1,71 @@ +import { prisma } from "@/lib/prisma"; +import "dotenv-flow/config"; + +async function main() { + const domains = await prisma.domain.findMany({ + where: { + notFoundUrl: null, + project: { + plan: { + not: "free", + }, + }, + links: { + some: { + key: "_root", + url: { + not: "", + }, + }, + }, + }, + select: { + id: true, + slug: true, + notFoundUrl: true, + project: { + select: { + slug: true, + plan: true, + }, + }, + links: { + select: { + key: true, + url: true, + }, + where: { + key: "_root", + }, + }, + }, + take: 10, + orderBy: { + createdAt: "asc", + }, + }); + + const updatedDomains = await Promise.all( + domains.map((domain) => { + return prisma.domain.update({ + where: { id: domain.id }, + data: { notFoundUrl: domain.links[0].url }, + select: { + id: true, + slug: true, + notFoundUrl: true, + project: { + select: { + slug: true, + plan: true, + }, + }, + }, + }); + }), + ); + + console.table(updatedDomains); +} + +main(); diff --git a/packages/cli/README.md b/packages/cli/README.md index 4fdf39e616..85713d465c 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -8,7 +8,7 @@ https://github.com/user-attachments/assets/2ce9fe51-68ab-4e6d-b08d-4da09c17f90e | Command | Description | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `dub login [key]` | Configure your workspace API key | +| `dub login` | Log into the DUB platform | | `dub config` | See your configured workspace credentials | | `dub domains` | Configure your workspace domain | | `dub shorten [url] [key]` | Create a short link. You can preemptively pass the URL and the generated short link key, or go through the CLI prompts. | diff --git a/packages/cli/package.json b/packages/cli/package.json index 12012ab773..76ab39ba79 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -40,6 +40,7 @@ "start": "node dist/index.js " }, "dependencies": { + "@badgateway/oauth2-client": "^2.4.2", "chalk": "^5.3.0", "commander": "^11.1.0", "configstore": "^6.0.0", @@ -48,6 +49,7 @@ "json-colorizer": "^2.2.2", "nanoid": "^5.0.7", "node-fetch": "^3.3.2", + "open": "^10.1.0", "ora": "^7.0.1", "package-json": "^8.1.1", "prompts": "^2.4.2", diff --git a/packages/cli/src/api/callback.ts b/packages/cli/src/api/callback.ts new file mode 100644 index 0000000000..ea2735de40 --- /dev/null +++ b/packages/cli/src/api/callback.ts @@ -0,0 +1,86 @@ +import { DubConfig } from "@/types"; +import { setConfig } from "@/utils/config"; +import { logger } from "@/utils/logger"; +import { OAuth2Client } from "@badgateway/oauth2-client"; +import chalk from "chalk"; +import * as http from "http"; +import { Ora } from "ora"; +import * as url from "url"; + +interface OAuthCallbackServerProps { + oauthClient: OAuth2Client; + redirectUri: string; + spinner: Ora; + codeVerifier: string; +} + +export function oauthCallbackServer({ + oauthClient, + redirectUri, + codeVerifier, + spinner, +}: OAuthCallbackServerProps) { + const server = http.createServer(async (req, res) => { + const reqUrl = url.parse(req.url || "", true); + + if (reqUrl.pathname !== "/callback" || req.method !== "GET") { + res.writeHead(404); + res.end("Not found"); + return; + } + + const code = reqUrl.query.code as string; + + if (!code) { + res.writeHead(400); + res.end( + "Authorization code not found. Please start the login process again.", + ); + + return; + } + + try { + spinner.text = "Verifying"; + + const { accessToken, refreshToken, expiresAt } = + await oauthClient.authorizationCode.getToken({ + code, + redirectUri, + codeVerifier, + }); + + spinner.text = "Configuring"; + + const configInfo: DubConfig = { + access_token: accessToken.trim(), + refresh_token: refreshToken, + expires_at: expiresAt ? Date.now() + expiresAt * 1000 : null, + domain: "dub.sh", + }; + + await setConfig(configInfo); + spinner.succeed("Configuration completed"); + + logger.info(""); + logger.info(chalk.green("Logged in successfully!")); + logger.info(""); + + res.writeHead(200, { "Content-Type": "text/html" }); + res.end("Authentication successful! You can close this window."); + } catch (error) { + res.writeHead(500, { "Content-Type": "text/html" }); + res.end("An error occurred during authentication. Please try again."); + } finally { + server.close(); + process.exit(0); + } + }); + + setTimeout(() => { + server.close(); + process.exit(0); + }, 300000); + + server.listen(4587); +} diff --git a/packages/cli/src/api/domains.ts b/packages/cli/src/api/domains.ts index 68a0656323..a8efd372ba 100644 --- a/packages/cli/src/api/domains.ts +++ b/packages/cli/src/api/domains.ts @@ -7,7 +7,7 @@ export async function getDomains() { const config = await getConfig(); const dub = new Dub({ - token: config.key, + token: config.access_token, }); const [{ result: domainsResponse }, defaultDomainsResponse] = @@ -16,7 +16,7 @@ export async function getDomains() { fetch("https://api.dub.co/domains/default", { method: "GET", headers: { - Authorization: `Bearer ${config.key}`, + Authorization: `Bearer ${config.access_token}`, "Content-Type": "application/json", }, }), diff --git a/packages/cli/src/api/links.ts b/packages/cli/src/api/links.ts index 3f60d9dc01..6efd717375 100644 --- a/packages/cli/src/api/links.ts +++ b/packages/cli/src/api/links.ts @@ -5,7 +5,7 @@ export async function createLink({ url, key }: { url: string; key: string }) { const config = await getConfig(); const dub = new Dub({ - token: config.key, + token: config.access_token, }); return await dub.links.create({ diff --git a/packages/cli/src/commands/config.ts b/packages/cli/src/commands/config.ts index e0b4b33946..2d8a90ee04 100644 --- a/packages/cli/src/commands/config.ts +++ b/packages/cli/src/commands/config.ts @@ -7,13 +7,12 @@ import ora from "ora"; export const config = new Command() .name("config") - .description("See your configured workspace credentials") + .description("See your configured credentials") .action(async () => { - const spinner = ora("Getting config file").start(); + const spinner = ora("Getting config").start(); try { const configInfo = await getConfig(); - spinner.succeed("Configuration file successfully retrieved"); logger.info(""); diff --git a/packages/cli/src/commands/domains.ts b/packages/cli/src/commands/domains.ts index da01531c13..f6f376f46a 100644 --- a/packages/cli/src/commands/domains.ts +++ b/packages/cli/src/commands/domains.ts @@ -1,9 +1,9 @@ import { getDomains } from "@/api/domains"; +import { setConfig } from "@/utils/config"; import { handleError } from "@/utils/handle-error"; import { logger } from "@/utils/logger"; import chalk from "chalk"; import { Command } from "commander"; -import Configstore from "configstore"; import ora from "ora"; import prompts from "prompts"; import { z } from "zod"; @@ -56,10 +56,9 @@ export const domains = new Command() }, ); - const getconfig = new Configstore("dub-cli"); - getconfig.set("domain", options.domain); - + setConfig({ domain: options.domain }); spinner.succeed("Done"); + logger.info(""); logger.info(`${chalk.green("Success!")} Configuration updated.`); logger.info(""); diff --git a/packages/cli/src/commands/links.ts b/packages/cli/src/commands/links.ts index c9bf90f64c..c857a4417b 100644 --- a/packages/cli/src/commands/links.ts +++ b/packages/cli/src/commands/links.ts @@ -6,7 +6,7 @@ import ora from "ora"; export const links = new Command() .command("links") - .description("Search for links in your Dub workspace.") + .description("Search for links in your Dub workspace") .option("-s, --search [search]", "Search term to filter links by") .option("-l, --limit [limit]", "Number of links to fetch") .action(async ({ search, limit }) => { @@ -16,13 +16,14 @@ export const links = new Command() const spinner = ora("Fetching links").start(); const dub = new Dub({ - token: config.key, + token: config.access_token, }); const links = await dub.links.list({ search, pageSize: limit ? parseInt(limit) : 10, }); + spinner.stop(); const formattedLinks = links.result.map((link) => ({ diff --git a/packages/cli/src/commands/login.ts b/packages/cli/src/commands/login.ts index 09dbfc6891..bd3fe0f214 100644 --- a/packages/cli/src/commands/login.ts +++ b/packages/cli/src/commands/login.ts @@ -1,72 +1,37 @@ -import type { DubConfig } from "@/types"; +import { oauthCallbackServer } from "@/api/callback"; +import { getNanoid } from "@/utils/get-nanoid"; import { handleError } from "@/utils/handle-error"; -import { logger } from "@/utils/logger"; -import chalk from "chalk"; +import { oauthClient } from "@/utils/oauth"; import { Command } from "commander"; -import Configstore from "configstore"; +import open from "open"; import ora from "ora"; -import prompts from "prompts"; -import { z } from "zod"; - -const loginOptionsSchema = z.object({ - key: z.string().min(8, { message: "Please use a valid workspace API key" }), -}); export const login = new Command() .name("login") - .description("Configure your workspace key") - .argument("[key]", "Workspace API key for authentication") - .action(async (key) => { + .description("Log into the Dub platform") + .action(async () => { try { - let credentials = { key }; - - if (!credentials.key) { - credentials = await prompts( - [ - { - type: "password", - name: "key", - message: "Enter your workspace API key:", - validate: (value) => { - const result = loginOptionsSchema.shape.key.safeParse(value); - return result.success || result.error.errors[0].message; - }, - }, - ], - { - onCancel: () => { - logger.info(""); - logger.warn("You cancelled the prompt."); - logger.info(""); - process.exit(0); - }, - }, - ); - } - - const validatedData = loginOptionsSchema.parse(credentials); - - const spinner = ora("configuring workspace key").start(); + const codeVerifier = getNanoid(64); + const redirectUri = "http://localhost:4587/callback"; - const configInfo: DubConfig = { - key: validatedData.key.trim(), - domain: "dub.sh", - }; + const authUrl = await oauthClient.authorizationCode.getAuthorizeUri({ + redirectUri, + codeVerifier, + scope: ["links.read", "links.write", "domains.read"], + }); - const config = new Configstore("dub-cli"); - config.set(configInfo); + const spinner = ora("Opening browser for authentication").start(); - if (!config.path) { - handleError(new Error("Failed to create config file")); - } + await open(authUrl); - spinner.succeed("Done"); + spinner.text = "Waiting for authentication"; - logger.info(""); - logger.info( - `${chalk.green("Success!")} Workspace API key configured successfully.`, - ); - logger.info(""); + oauthCallbackServer({ + oauthClient, + redirectUri, + codeVerifier, + spinner, + }); } catch (error) { handleError(error); } diff --git a/packages/cli/src/types/index.ts b/packages/cli/src/types/index.ts index 7931476b92..b1ccb80e99 100644 --- a/packages/cli/src/types/index.ts +++ b/packages/cli/src/types/index.ts @@ -1,24 +1,10 @@ export interface DubConfig { - key: string; + access_token: string; + refresh_token: string | null; + expires_at: number | null; domain?: string; } -export type Domain = { - id: string; - slug: string; - verified: boolean; - primary: boolean; - archived: boolean; - placeholder: string; - expiredUrl: string; - createdAt: string; - updatedAt: string; - registeredDomain: { - id: string; - createdAt: string; - expiresAt: string; - }; -}; export interface APIError { error: { code: string; diff --git a/packages/cli/src/utils/config.ts b/packages/cli/src/utils/config.ts index 502108d4d6..d49d4ecec1 100644 --- a/packages/cli/src/utils/config.ts +++ b/packages/cli/src/utils/config.ts @@ -1,14 +1,53 @@ import type { DubConfig } from "@/types"; +import { oauthClient } from "@/utils/oauth"; import Configstore from "configstore"; export async function getConfig(): Promise { - const getConfig = new Configstore("dub-cli"); + const configStore = new Configstore("dub-cli"); - if (!getConfig.size) { + if (!configStore.size) { throw new Error( - "Workspace API key not found. Run `dub login` to configure your API key.", + "Access token not found. Please run `dub login` to authenticate with Dub.", ); } - return await getConfig.all; + const config = configStore.all as DubConfig; + + if (config.expires_at && Date.now() >= config.expires_at) { + const response = await oauthClient.refreshToken({ + accessToken: config.access_token, + refreshToken: config.refresh_token, + expiresAt: config.expires_at, + }); + + const { accessToken, refreshToken, expiresAt } = response; + + return await setConfig({ + access_token: accessToken, + refresh_token: refreshToken, + expires_at: expiresAt, + }); + } + + return await configStore.all; +} + +export async function setConfig( + newConfig: Partial, +): Promise { + const configStore = new Configstore("dub-cli"); + const existingConfig: DubConfig = configStore.all; + + const updatedConfig: DubConfig = { + ...existingConfig, + ...newConfig, + }; + + configStore.set(updatedConfig); + + if (!configStore.path) { + throw new Error("Failed to create or update config file"); + } + + return updatedConfig; } diff --git a/packages/cli/src/utils/get-nanoid.ts b/packages/cli/src/utils/get-nanoid.ts index 910f5aa1e4..31a3976ce8 100644 --- a/packages/cli/src/utils/get-nanoid.ts +++ b/packages/cli/src/utils/get-nanoid.ts @@ -1,7 +1,10 @@ import { customAlphabet } from "nanoid"; -export function getNanoid() { - const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"; - const nanoid = customAlphabet(alphabet, 10); - return nanoid(7); +const alphabet = + "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + +export function getNanoid(length: number = 7) { + const nanoid = customAlphabet(alphabet, length); + + return nanoid(); } diff --git a/packages/cli/src/utils/oauth.ts b/packages/cli/src/utils/oauth.ts new file mode 100644 index 0000000000..8fc16158ba --- /dev/null +++ b/packages/cli/src/utils/oauth.ts @@ -0,0 +1,8 @@ +import { OAuth2Client } from "@badgateway/oauth2-client"; + +export const oauthClient = new OAuth2Client({ + // TODO: add client id here + clientId: "dub_app_39527dcc11b452f38bb54a3a1664fd044d7158dfea8abcde", + authorizationEndpoint: "https://app.dub.co/oauth/authorize", + tokenEndpoint: "https://api.dub.co/oauth/token", +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed09ed46d4..3c2a154ab5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -481,6 +481,9 @@ importers: packages/cli: dependencies: + '@badgateway/oauth2-client': + specifier: ^2.4.2 + version: 2.4.2 chalk: specifier: ^5.3.0 version: 5.3.0 @@ -505,6 +508,9 @@ importers: node-fetch: specifier: ^3.3.2 version: 3.3.2 + open: + specifier: ^10.1.0 + version: 10.1.0 ora: specifier: ^7.0.1 version: 7.0.1 @@ -3386,6 +3392,11 @@ packages: '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 + /@badgateway/oauth2-client@2.4.2: + resolution: {integrity: sha512-70Fmzlmn8EfCjjssls8N6E94quBUWnLhu4inPZU2pkwpc6ZvbErkLRvtkYl81KFCvVcuVC0X10QPZVNwjXo2KA==} + engines: {node: '>= 14'} + dev: false + /@bcoe/v8-coverage@0.2.3: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true @@ -11265,6 +11276,13 @@ packages: ieee754: 1.2.1 dev: false + /bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + dependencies: + run-applescript: 7.0.0 + dev: false + /bundle-require@3.1.2(esbuild@0.14.54): resolution: {integrity: sha512-Of6l6JBAxiyQ5axFxUM6dYeP/W7X2Sozeo/4EYB9sJhL+dqL7TKjg+shwxp6jlu/6ZSERfsYtIpSJ1/x3XkAEA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -12199,6 +12217,19 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + /default-browser-id@5.0.0: + resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} + engines: {node: '>=18'} + dev: false + + /default-browser@5.2.1: + resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} + engines: {node: '>=18'} + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.0 + dev: false + /defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} dependencies: @@ -12232,6 +12263,11 @@ packages: engines: {node: '>=8'} dev: false + /define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + dev: false + /define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} @@ -14516,6 +14552,12 @@ packages: hasBin: true dev: false + /is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + dev: false + /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -14557,6 +14599,14 @@ packages: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} dev: false + /is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + dependencies: + is-docker: 3.0.0 + dev: false + /is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} @@ -14735,6 +14785,13 @@ packages: is-docker: 2.2.1 dev: false + /is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + dependencies: + is-inside-container: 1.0.0 + dev: false + /isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -17735,6 +17792,16 @@ packages: dependencies: mimic-fn: 4.0.0 + /open@10.1.0: + resolution: {integrity: sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==} + engines: {node: '>=18'} + dependencies: + default-browser: 5.2.1 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + is-wsl: 3.1.0 + dev: false + /open@6.4.0: resolution: {integrity: sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==} engines: {node: '>=8'} @@ -19551,6 +19618,11 @@ packages: '@rollup/rollup-win32-x64-msvc': 4.14.0 fsevents: 2.3.3 + /run-applescript@7.0.0: + resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} + engines: {node: '>=18'} + dev: false + /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: