From 6d5265dfe0ff1690fd191deb9a89f8c0548fcf3a Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 9 Nov 2025 18:02:15 +0000 Subject: [PATCH] Initial Commit --- .gitignore | 37 + package.json | 28 + src/index.js | 67 + src/objects/blogs.js | 45 + src/objects/companies.js | 62 + src/objects/cv.js | 43 + src/objects/files.js | 27 + src/objects/images.js | 26 + src/objects/navigation.js | 22 + src/objects/pages.js | 53 + src/objects/positions.js | 50 + src/objects/projects.js | 54 + src/objects/settings.js | 34 + src/objects/videos.js | 24 + src/routes/blurHash.js | 24 + src/routes/contact.js | 85 + src/routes/content.js | 92 ++ src/routes/hooks.js | 277 ++++ src/utils/api.js | 6 + src/utils/blogs.js | 203 +++ src/utils/blurHash.js | 110 ++ src/utils/companies.js | 237 +++ src/utils/contentCache.js | 44 + src/utils/cv.js | 154 ++ src/utils/fileCache.js | 369 +++++ src/utils/icon.js | 72 + src/utils/imageCache.js | 349 ++++ src/utils/navigation.js | 90 ++ src/utils/notion.js | 627 ++++++++ src/utils/pages.js | 214 +++ src/utils/positions.js | 164 ++ src/utils/projects.js | 225 +++ src/utils/settings.js | 177 ++ src/utils/videoCache.js | 340 ++++ tb-api.paw | Bin 0 -> 18134 bytes vitest.config.js | 11 + wrangler.jsonc | 140 ++ yarn.lock | 3217 +++++++++++++++++++++++++++++++++++++ 38 files changed, 7799 insertions(+) create mode 100644 .gitignore create mode 100644 package.json create mode 100644 src/index.js create mode 100644 src/objects/blogs.js create mode 100644 src/objects/companies.js create mode 100644 src/objects/cv.js create mode 100644 src/objects/files.js create mode 100644 src/objects/images.js create mode 100644 src/objects/navigation.js create mode 100644 src/objects/pages.js create mode 100644 src/objects/positions.js create mode 100644 src/objects/projects.js create mode 100644 src/objects/settings.js create mode 100644 src/objects/videos.js create mode 100644 src/routes/blurHash.js create mode 100644 src/routes/contact.js create mode 100644 src/routes/content.js create mode 100644 src/routes/hooks.js create mode 100644 src/utils/api.js create mode 100644 src/utils/blogs.js create mode 100644 src/utils/blurHash.js create mode 100644 src/utils/companies.js create mode 100644 src/utils/contentCache.js create mode 100644 src/utils/cv.js create mode 100644 src/utils/fileCache.js create mode 100644 src/utils/icon.js create mode 100644 src/utils/imageCache.js create mode 100644 src/utils/navigation.js create mode 100644 src/utils/notion.js create mode 100644 src/utils/pages.js create mode 100644 src/utils/positions.js create mode 100644 src/utils/projects.js create mode 100644 src/utils/settings.js create mode 100644 src/utils/videoCache.js create mode 100644 tb-api.paw create mode 100644 vitest.config.js create mode 100644 wrangler.jsonc create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0509aab --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +.dev.vars + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +.wrangler/ \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..cb29dc9 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "tombutcher-api", + "version": "0.0.0", + "private": true, + "scripts": { + "deploy": "wrangler deploy --env production", + "dev": "wrangler dev --test-scheduled --host 0.0.0.0", + "test": "vitest" + }, + "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.6.4", + "vitest": "~2.1.9", + "wrangler": "^4.38.0" + }, + "dependencies": { + "@napi-rs/canvas": "^0.1.80", + "@notionhq/client": "^5.1.0", + "blurhash": "^2.0.5", + "dayjs": "^1.11.18", + "jpeg-js": "^0.4.4", + "lodash": "^4.17.21", + "microdiff": "^1.5.0", + "remove-svg-properties": "^0.3.4", + "sharp": "^0.34.3", + "svgson": "^5.3.1", + "upng-js": "^2.1.0" + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..a63e2ed --- /dev/null +++ b/src/index.js @@ -0,0 +1,67 @@ +import { handleContactRequest } from "./routes/contact.js"; +import { handleContentRequest } from "./routes/content.js"; +import { handleNotionHook } from "./routes/hooks.js"; +import { globalHeaders } from "./utils/api.js"; + +async function handleRequest(request, env) { + if ( + request.method === "OPTIONS" && + request.url.split("?")[0].endsWith("/contact") + ) { + console.log("Handling contact OPTIONS request..."); + return new Response(null, { + status: 204, // No Content + headers: { + "Access-Control-Allow-Origin": env.CORS_ORIGIN, + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + }, + }); + } + if ( + request.method === "POST" && + request.url.split("?")[0].endsWith("/contact") + ) { + return await handleContactRequest(request, env); + } + + if ( + request.method === "GET" && + request.url.split("?")[0].endsWith("/content") + ) { + return await handleContentRequest(request, env); + } + + if ( + request.method === "POST" && + request.url.split("?")[0].endsWith("/notionHook") + ) { + return await handleNotionHook(request, env); + } + + // Return 404 if the route is not found + return new Response("Not Found", { status: 404, headers: globalHeaders }); +} + +async function handleScheduledEvent(event, env) { + console.log("Scheduled event:", event.cron); + switch (event.cron) { + case "*/5 * * * *": + await updateAllSmoobuData(env); + break; + case "* * * * *": + await refreshBookingCache(env); + break; + default: + break; + } +} + +export default { + async fetch(request, env) { + return await handleRequest(request, env); + }, + async scheduled(event, env) { + return await handleScheduledEvent(event, env); + }, +}; diff --git a/src/objects/blogs.js b/src/objects/blogs.js new file mode 100644 index 0000000..bc02c62 --- /dev/null +++ b/src/objects/blogs.js @@ -0,0 +1,45 @@ +import { + buildListCache, + queryNotionDataSource, + getNotionPage, +} from "../utils/notion.js"; +import { getBlogs, storeBlogs, transformNotionBlog } from "../utils/blogs.js"; +import { unionBy } from "lodash"; + +export async function importNotionBlogs(env, notionId = null) { + console.log("Importing Blogs from Notion..."); + + let BlogsData = []; + if (notionId !== null) { + BlogsData = [await getNotionPage(notionId)]; + } else { + BlogsData = await queryNotionDataSource(env.BLOGS_DB); + } + + const Blogs = ( + await Promise.all( + BlogsData.map(async (blog) => transformNotionBlog(env, blog)) + ) + ).filter(Boolean); + + // If notionId is not null, use lodash unionBy to replace existing Blogs + if (notionId !== null) { + const cachedBlogs = await getBlogs(env); + const mergedBlogs = unionBy(Blogs, cachedBlogs, "notionId"); + await storeBlogs(env, mergedBlogs); + console.log("Imported Blogs from Notion and merged with cache."); + return mergedBlogs; + } + + await storeBlogs(env, Blogs); + console.log("Imported Blogs from Notion."); + return Blogs; +} + +export async function deleteNotionBlogFromCache(env, notionId) { + const cachedBlogs = await getBlogs(env); + const newCache = cachedBlogs.filter((b) => b.notionId !== notionId); + await storeBlogs(env, newCache); + console.log("Deleted blog from cache."); + return newCache; +} diff --git a/src/objects/companies.js b/src/objects/companies.js new file mode 100644 index 0000000..3712f92 --- /dev/null +++ b/src/objects/companies.js @@ -0,0 +1,62 @@ +import { + queryNotionDataSource, + getNotionPage, + sortDuration, +} from "../utils/notion.js"; +import { + getCompanies, + storeCompanies, + transformNotionCompany, +} from "../utils/companies.js"; +import { unionBy } from "lodash"; +import { getSettings } from "../utils/settings.js"; +import { getPositions } from "../utils/positions.js"; + +export async function importNotionCompanies(env, notionId = null) { + console.log("Importing Companies from Notion..."); + + let CompaniesData = []; + if (notionId !== null) { + CompaniesData = [await getNotionPage(notionId)]; + } else { + CompaniesData = await queryNotionDataSource(env.COMPANIES_DB); + } + + const settingsData = await getSettings(env); + const positionsData = await getPositions(env); + + const Companies = ( + await Promise.all( + CompaniesData.map(async (company) => + transformNotionCompany(env, company, settingsData, positionsData) + ) + ) + ) + .filter(Boolean) + .sort(sortDuration); + + // If notionId is not null, use lodash unionBy to replace existing Companies + if (notionId !== null) { + const cachedCompanies = await getCompanies(env); + const mergedCompanies = unionBy( + Companies, + cachedCompanies, + "notionId" + ).sort(sortDuration); + await storeCompanies(env, mergedCompanies); + console.log("Imported Companies from Notion and merged with cache."); + return mergedCompanies; + } + + await storeCompanies(env, Companies); + console.log("Imported Companies from Notion."); + return Companies; +} + +export async function deleteNotionCompanyFromCache(env, notionId) { + const cachedCompanies = await getCompanies(env); + const newCache = cachedCompanies.filter((c) => c.notionId !== notionId); + await storeCompanies(env, newCache); + console.log("Deleted company from cache."); + return newCache; +} diff --git a/src/objects/cv.js b/src/objects/cv.js new file mode 100644 index 0000000..559f5ce --- /dev/null +++ b/src/objects/cv.js @@ -0,0 +1,43 @@ +import { + buildListCache, + queryNotionDataSource, + getNotionPage, +} from "../utils/notion.js"; +import { getCvs, storeCvs, transformNotionCv } from "../utils/cv.js"; +import { unionBy } from "lodash"; + +export async function importNotionCvs(env, notionId = null) { + console.log("Importing CVs from Notion..."); + + let CvsData = []; + if (notionId !== null) { + CvsData = [await getNotionPage(notionId)]; + } else { + CvsData = await queryNotionDataSource(env.CV_DB); + } + + const Cvs = ( + await Promise.all(CvsData.map(async (cv) => transformNotionCv(env, cv))) + ).filter(Boolean); + + // If notionId is not null, use lodash unionBy to replace existing CVs + if (notionId !== null) { + const cachedCvs = await getCvs(env); + const mergedCvs = unionBy(Cvs, cachedCvs, "notionId"); + await storeCvs(env, mergedCvs); + console.log("Imported CVs from Notion and merged with cache."); + return mergedCvs; + } + + await storeCvs(env, Cvs); + console.log("Imported CVs from Notion."); + return Cvs; +} + +export async function deleteNotionCvFromCache(env, notionId) { + const cachedCvs = await getCvs(env); + const newCache = cachedCvs.filter((c) => c.notionId !== notionId); + await storeCvs(env, newCache); + console.log("Deleted CV from cache."); + return newCache; +} diff --git a/src/objects/files.js b/src/objects/files.js new file mode 100644 index 0000000..51b6c7f --- /dev/null +++ b/src/objects/files.js @@ -0,0 +1,27 @@ +import { getPages } from "../utils/pages.js"; +import { getBlogs } from "../utils/blogs.js"; +import { getProjects } from "../utils/projects.js"; +import { getCompanies } from "../utils/companies.js"; +import { collectFileUrls, updateFiles } from "../utils/fileCache.js"; +import { getCvs } from "../utils/cv.js"; + +export async function importFiles(env) { + console.log("Importing files from Notion..."); + const pages = (await getPages(env)) || []; + const blogs = (await getBlogs(env)) || []; + const projects = (await getProjects(env)) || []; + const companies = (await getCompanies(env)) || []; + const cvs = (await getCvs(env)) || []; + + const fileUrls = collectFileUrls([ + ...pages, + ...blogs, + ...projects, + ...companies, + ...cvs, + ]); + + const updatedFiles = await updateFiles(env, fileUrls); + console.log("Imported files from Notion."); + return updatedFiles; +} diff --git a/src/objects/images.js b/src/objects/images.js new file mode 100644 index 0000000..a0f1955 --- /dev/null +++ b/src/objects/images.js @@ -0,0 +1,26 @@ +import { getPages } from "../utils/pages.js"; +import { getBlogs } from "../utils/blogs.js"; +import { getProjects } from "../utils/projects.js"; +import { getCompanies } from "../utils/companies.js"; +import { collectImageUrls, updateImages } from "../utils/imageCache.js"; + +export async function importImages(env) { + // Fetch caches + console.log("Importing images from Notion..."); + const pages = (await getPages(env)) || []; + const blogs = (await getBlogs(env)) || []; + const projects = (await getProjects(env)) || []; + const companies = (await getCompanies(env)) || []; + + // Collect all image URLs + const imageUrls = collectImageUrls([ + ...pages, + ...blogs, + ...projects, + ...companies, + ]); + + const updatedImages = await updateImages(env, imageUrls); + console.log("Imported images from Notion."); + return updatedImages; +} diff --git a/src/objects/navigation.js b/src/objects/navigation.js new file mode 100644 index 0000000..3b874bb --- /dev/null +++ b/src/objects/navigation.js @@ -0,0 +1,22 @@ +import { queryNotionDataSource } from "../utils/notion.js"; +import { transformNavigation, storeNavigation } from "../utils/navigation.js"; + +export async function importNotionNavigation(env) { + console.log("Importing navigation from Notion..."); + const notionPages = await queryNotionDataSource(env.PAGES_DB); + const notionBlogs = await queryNotionDataSource(env.BLOGS_DB); + // Transform the incoming pages for the given type + const navigationItems = ( + await Promise.all( + notionPages.map(async (item) => transformNavigation(item, "page")), + notionBlogs.map(async (item) => transformNavigation(item, "blog")) + ) + ).filter(Boolean); + + // Store the updated cache + await storeNavigation(env, navigationItems); + + console.log("Navigation imported from Notion."); + + return navigationItems; +} diff --git a/src/objects/pages.js b/src/objects/pages.js new file mode 100644 index 0000000..7b7129d --- /dev/null +++ b/src/objects/pages.js @@ -0,0 +1,53 @@ +import { queryNotionDataSource, getNotionPage } from "../utils/notion.js"; +import { transformPageData, storePages, getPages } from "../utils/pages.js"; +import { getSettings } from "../utils/settings.js"; +import { unionBy } from "lodash"; + +// Fetch Notion pages (raw data) +export async function importNotionPages(env, notionId = null) { + console.log("Importing pages from Notion..."); + const settingsData = await getSettings(env); + let pagesData = []; + if (notionId !== null) { + pagesData = [await getNotionPage(notionId)]; + } else { + pagesData = await queryNotionDataSource(env.PAGES_DB, { + sorts: [{ property: "Order", direction: "ascending" }], + }); + } + + const pages = ( + await Promise.all( + pagesData.map(async (page) => transformPageData(env, page, settingsData)) + ) + ) + .filter(Boolean) + .sort((a, b) => a.order - b.order) + .filter((page) => page.published == true); + + if (notionId !== null) { + const cachedPages = await getPages(env); + console.log("Cached pages:", cachedPages); + console.log("Pages:", pages); + + // Merge: preserve cached order, but replace with new data if available + const mergedPages = unionBy(pages, cachedPages, "notionId") + .sort((a, b) => a.order - b.order) + .filter((page) => page.published == true); + await storePages(env, mergedPages); + console.log("Pages imported from Notion and merged with cache."); + return mergedPages; + } + + await storePages(env, pages); + console.log("Pages imported from Notion."); + return pages; +} + +export async function deleteNotionPageFromCache(env, notionId) { + const cachedPages = await getPages(env); + const newCache = cachedPages.filter((b) => b.notionId !== notionId); + await storePages(env, newCache); + console.log("Deleted page from cache."); + return newCache; +} diff --git a/src/objects/positions.js b/src/objects/positions.js new file mode 100644 index 0000000..941585a --- /dev/null +++ b/src/objects/positions.js @@ -0,0 +1,50 @@ +import { queryNotionDataSource, getNotionPage } from "../utils/notion.js"; +import { + getPositions, + storePositions, + transformNotionPosition, +} from "../utils/positions.js"; +import { unionBy } from "lodash"; +import { getCompanies } from "../utils/companies.js"; + +export async function importNotionPositions(env, notionId = null) { + console.log("Importing Positions from Notion..."); + + let PositionsData = []; + if (notionId !== null) { + PositionsData = [await getNotionPage(notionId)]; + } else { + PositionsData = await queryNotionDataSource(env.POSITIONS_DB); + } + + const companiesData = await getCompanies(env); + + const Positions = ( + await Promise.all( + PositionsData.map(async (position) => + transformNotionPosition(env, position, companiesData) + ) + ) + ).filter(Boolean); + + // If notionId is not null, use lodash unionBy to replace existing Positions + if (notionId !== null) { + const cachedPositions = await getPositions(env); + const mergedPositions = unionBy(Positions, cachedPositions, "notionId"); + await storePositions(env, mergedPositions); + console.log("Imported Positions from Notion and merged with cache."); + return mergedPositions; + } + + await storePositions(env, Positions); + console.log("Imported Positions from Notion."); + return Positions; +} + +export async function deleteNotionPositionFromCache(env, notionId) { + const cachedPositions = await getPositions(env); + const newCache = cachedPositions.filter((p) => p.notionId !== notionId); + await storePositions(env, newCache); + console.log("Deleted position from cache."); + return newCache; +} diff --git a/src/objects/projects.js b/src/objects/projects.js new file mode 100644 index 0000000..5c9cbd2 --- /dev/null +++ b/src/objects/projects.js @@ -0,0 +1,54 @@ +import { + buildListCache, + queryNotionDataSource, + getNotionPage, +} from "../utils/notion.js"; +import { + getProjects, + storeProjects, + transformNotionProject, +} from "../utils/projects.js"; +import { unionBy } from "lodash"; +import { getSettings } from "../utils/settings.js"; + +export async function importNotionProjects(env, notionId = null) { + console.log("Importing Projects from Notion..."); + + let ProjectsData = []; + if (notionId !== null) { + ProjectsData = [await getNotionPage(notionId)]; + } else { + ProjectsData = await queryNotionDataSource(env.PROJECTS_DB); + } + + const settingsData = await getSettings(env); + + const Projects = ( + await Promise.all( + ProjectsData.map(async (project) => + transformNotionProject(env, project, settingsData) + ) + ) + ).filter(Boolean); + + // If notionId is not null, use lodash unionBy to replace existing Projects + if (notionId !== null) { + const cachedProjects = await getProjects(env); + const mergedProjects = unionBy(Projects, cachedProjects, "notionId"); + await storeProjects(env, mergedProjects); + console.log("Imported Projects from Notion and merged with cache."); + return mergedProjects; + } + + await storeProjects(env, Projects); + console.log("Imported Projects from Notion."); + return Projects; +} + +export async function deleteNotionProjectFromCache(env, notionId) { + const cachedProjects = await getProjects(env); + const newCache = cachedProjects.filter((p) => p.notionId !== notionId); + await storeProjects(env, newCache); + console.log("Deleted project from cache."); + return newCache; +} diff --git a/src/objects/settings.js b/src/objects/settings.js new file mode 100644 index 0000000..51dd3dc --- /dev/null +++ b/src/objects/settings.js @@ -0,0 +1,34 @@ +import { queryNotionDataSource } from "../utils/notion.js"; +import { + transformThemes, + transformRedirectsData, + transformGlobalThemesData, + storeSettings, +} from "../utils/settings.js"; + +export async function importNotionSettings(env) { + console.log("Importing settings from Notion..."); + const [globalThemesData, themesData, redirectsData] = await Promise.all([ + queryNotionDataSource(env.GLOBAL_THEMES_DB), + queryNotionDataSource(env.THEMES_DB), + queryNotionDataSource(env.REDIRECTS_DB), + ]); + + console.log("Fetched settings from Notion."); + + const themes = await transformThemes(themesData); + const globalThemes = transformGlobalThemesData(env, globalThemesData, themes); + const redirects = await transformRedirectsData(env, redirectsData); + + await storeSettings(env, { + globalThemes, + themes, + redirects, + }); + + return { + globalThemes, + themes, + redirects, + }; +} diff --git a/src/objects/videos.js b/src/objects/videos.js new file mode 100644 index 0000000..bc87922 --- /dev/null +++ b/src/objects/videos.js @@ -0,0 +1,24 @@ +import { getPages } from "../utils/pages.js"; +import { getBlogs } from "../utils/blogs.js"; +import { getProjects } from "../utils/projects.js"; +import { getCompanies } from "../utils/companies.js"; +import { collectVideoUrls, updateVideos } from "../utils/videoCache.js"; + +export async function importVideos(env) { + console.log("Importing videos from Notion..."); + const pages = (await getPages(env)) || []; + const blogs = (await getBlogs(env)) || []; + const projects = (await getProjects(env)) || []; + const companies = (await getCompanies(env)) || []; + + const videoUrls = collectVideoUrls([ + ...pages, + ...blogs, + ...projects, + ...companies, + ]); + + const updatedVideos = await updateVideos(env, videoUrls); + console.log("Imported videos from Notion."); + return updatedVideos; +} diff --git a/src/routes/blurHash.js b/src/routes/blurHash.js new file mode 100644 index 0000000..c1a803e --- /dev/null +++ b/src/routes/blurHash.js @@ -0,0 +1,24 @@ +export async function handleProcessBlurhash(env) { + try { + const imagesData = await getImages(env); + + if (imagesData) { + console.log("Found cached content!"); + await processBlurhashes(imagesData); + } else { + console.log("No images found in cache!"); + } + } catch (error) { + console.error("Error in handleProcessBlurhash:", error); + return new Response( + JSON.stringify({ + error: "Failed to process blurhash!", + message: error.message, + }), + { + status: 500, + headers: globalHeaders, + } + ); + } +} diff --git a/src/routes/contact.js b/src/routes/contact.js new file mode 100644 index 0000000..a92dede --- /dev/null +++ b/src/routes/contact.js @@ -0,0 +1,85 @@ +import { globalHeaders } from "../utils/api.js"; +import { addToNotionDataSource } from "../utils/notion.js"; +import { env } from "cloudflare:workers"; + +const TURNSTILE_ENABLED = true; + +export async function handleContactRequest(request, env) { + const { name, email, message, token } = await request.json(); + if (!email || !token || !message || !name) { + return new Response( + JSON.stringify({ + message: "Email, message, name and token are required", + code: "missing-fields", + }), + { + status: 400, + headers: globalHeaders, + }, + ); + } + + // Extract the IP address from the request headers + const ip = request.headers.get("CF-Connecting-IP"); + + if (TURNSTILE_ENABLED) { + // Verify the Turnstile token + const verificationResponse = await fetch( + "https://challenges.cloudflare.com/turnstile/v0/siteverify", + { + method: "POST", + headers: globalHeaders, + body: JSON.stringify({ + secret: env.TURNSTILE_AUTH, + response: token, + }), + }, + ); + + const verificationData = await verificationResponse.json(); + if (!verificationData.success) { + const code = verificationData["error-codes"][0]; + var errorMessage = "Captcha error."; + if (code == "timeout-or-duplicate") { + errorMessage = "Captcha expired."; + } + return new Response( + JSON.stringify({ message: errorMessage, code: `captcha-${code}` }), + { + status: 400, + headers: globalHeaders, + }, + ); + } + } + + // Add the email and location to Notion + try { + await addToNotionDataSource( + { + Name: name, + Email: email, + Message: message, + ["IP Address"]: ip, + }, + env.MESSAGES_DB, + ); + return new Response( + JSON.stringify({ + success: true, + message: "Email processed and added to Notion", + }), + { + headers: globalHeaders, + }, + ); + } catch (error) { + return new Response( + JSON.stringify({ message: "Error storing data.", code: "storage-error" }), + { + status: 500, + headers: globalHeaders, + }, + ); + } +} diff --git a/src/routes/content.js b/src/routes/content.js new file mode 100644 index 0000000..d35c03e --- /dev/null +++ b/src/routes/content.js @@ -0,0 +1,92 @@ +import { globalHeaders } from "../utils/api.js"; +import { getCombinedCachedContent } from "../utils/contentCache.js"; +import { importNotionBlogs } from "../objects/blogs.js"; +import { importNotionPages } from "../objects/pages.js"; +import { importNotionSettings } from "../objects/settings.js"; +import { importNotionNavigation } from "../objects/navigation.js"; +import { importNotionPositions } from "../objects/positions.js"; +import { importNotionCompanies } from "../objects/companies.js"; +import { importImages } from "../objects/images.js"; +import { importNotionCvs } from "../objects/cv.js"; +import { importFiles } from "../objects/files.js"; +import { importVideos } from "../objects/videos.js"; + +// Fetch or return cached content +export async function getCachedContent(env) { + // Try to get combined cached content + var cachedContent = await getCombinedCachedContent(env); + + const noBlogs = cachedContent.blogs?.length === 0; + const noPages = cachedContent.pages?.length === 0; + const noSettings = cachedContent.settings == {}; + const noCompanies = cachedContent.companies?.length === 0; + const noCvs = cachedContent.cvs?.length === 0; + const noPositions = cachedContent.positions?.length === 0; + + if (noBlogs || noPages || noSettings || noCompanies || noCvs || noPositions) { + await importNotionNavigation(env); + } + + if (noBlogs) { + cachedContent.blogs = await importNotionBlogs(env); + cachedContent.images = await importImages(env); + cachedContent.files = await importFiles(env); + cachedContent.videos = await importVideos(env); + } + + if (noPages) { + cachedContent.pages = await importNotionPages(env); + cachedContent.images = await importImages(env); + cachedContent.files = await importFiles(env); + cachedContent.videos = await importVideos(env); + } + + if (noSettings) { + cachedContent.settings = await importNotionSettings(env); + } + + if (noCvs) { + cachedContent.cvs = await importNotionCvs(env); + cachedContent.files = await importFiles(env); + } + + if (noCompanies) { + await importNotionPositions(env); + cachedContent.companies = await importNotionCompanies(env); + cachedContent.images = await importImages(env); + cachedContent.files = await importFiles(env); + cachedContent.videos = await importVideos(env); + } + + if (cachedContent) { + return new Response( + JSON.stringify({ + ...cachedContent, + blogs: cachedContent.blogs.filter((blog) => blog.published == true), + }), + { + headers: globalHeaders, + } + ); + } + + return new Response(JSON.stringify({}), { headers: globalHeaders }); +} + +export async function handleContentRequest(request, env) { + try { + return await getCachedContent(env); + } catch (error) { + console.error("Error handling content request:", error); + return new Response( + JSON.stringify({ + error: "Failed to fetch content", + message: error.message, + }), + { + status: 500, + headers: globalHeaders, + } + ); + } +} diff --git a/src/routes/hooks.js b/src/routes/hooks.js new file mode 100644 index 0000000..10908b3 --- /dev/null +++ b/src/routes/hooks.js @@ -0,0 +1,277 @@ +import { importNotionNavigation } from "../objects/navigation"; +import { importNotionSettings } from "../objects/settings"; +import { importNotionPages, deleteNotionPageFromCache } from "../objects/pages"; +import { importNotionBlogs, deleteNotionBlogFromCache } from "../objects/blogs"; +import { globalHeaders } from "../utils/api"; +import { importImages } from "../objects/images"; +import { importFiles } from "../objects/files"; +import { importVideos } from "../objects/videos"; +import { handleBlurhashUpdate } from "../utils/imageCache"; +import { importNotionProjects } from "../objects/projects"; +import { importNotionCompanies } from "../objects/companies"; +import { importNotionPositions } from "../objects/positions"; +import { importNotionCvs } from "../objects/cv"; + +async function updateAllNotionData(env) { + await importNotionNavigation(env); + const settings = await importNotionSettings(env); + const pages = await importNotionPages(env); + const blogs = await importNotionBlogs(env); + const bookings = await importNotionBookings(env); + const projects = await importNotionProjects(env); + + const cvs = await importNotionCvs(env); + return { settings, pages, blogs, bookings, projects, cvs }; +} + +export async function handleNotionHook(request, env) { + try { + const body = await request.json(); + if (body?.verification_token) { + console.log("Verification token received:", body.verification_token); + return new Response( + JSON.stringify({ + status: "gotVerificationToken", + }), + { headers: globalHeaders } + ); + } + console.log("Notion hook received:", body.type); + if ( + body.type == "page.properties_updated" || + body.type == "page.content_updated" || + (body.type == "page.created" && body.data?.parent?.data_source_id) + ) { + const dataSourceId = body.data.parent.data_source_id; + const entityId = body?.entity?.id || null; + console.log("Data source ID:", dataSourceId); + + if (dataSourceId === env.PAGES_DB) { + console.log("Importing pages from Notion..."); + await importNotionNavigation(env); + const pages = await importNotionPages(env, entityId); + const images = await importImages(env); + await handleBlurhashUpdate(request, env); + return new Response( + JSON.stringify({ + status: "OK", + content: { pages, images }, + }), + { + headers: globalHeaders, + } + ); + } + if (dataSourceId === env.BLOGS_DB) { + await importNotionNavigation(env); + const blogs = await importNotionBlogs(env, entityId); + const images = await importImages(env); + const files = await importFiles(env); + const videos = await importVideos(env); + await handleBlurhashUpdate(request, env); + return new Response( + JSON.stringify({ + status: "OK", + content: { blogs, images, files, videos }, + }), + { + headers: globalHeaders, + } + ); + } + if (dataSourceId === env.PROJECTS_DB) { + await importNotionNavigation(env); + const projects = await importNotionProjects(env, entityId); + const images = await importImages(env); + const files = await importFiles(env); + const videos = await importVideos(env); + await handleBlurhashUpdate(request, env); + return new Response( + JSON.stringify({ + status: "OK", + content: { projects, images, files, videos }, + }), + { + headers: globalHeaders, + } + ); + } + if (dataSourceId === env.COMPANIES_DB) { + const positions = await importNotionPositions(env); + const companies = await importNotionCompanies(env, entityId); + const images = await importImages(env); + const files = await importFiles(env); + const videos = await importVideos(env); + await handleBlurhashUpdate(request, env); + return new Response( + JSON.stringify({ + status: "OK", + content: { companies, images, positions, files, videos }, + }), + { + headers: globalHeaders, + } + ); + } + if (dataSourceId === env.POSITIONS_DB) { + const positions = await importNotionPositions(env, entityId); + const companies = await importNotionCompanies(env); + const images = await importImages(env); + await handleBlurhashUpdate(request, env); + const files = await importFiles(env); + const videos = await importVideos(env); + return new Response( + JSON.stringify({ + status: "OK", + content: { positions, companies, images, files, videos }, + }), + { + headers: globalHeaders, + } + ); + } + if (dataSourceId === env.CV_DB) { + const cvs = await importNotionCvs(env, entityId); + const files = await importFiles(env); + return new Response( + JSON.stringify({ status: "OK", content: { cvs, files } }), + { + headers: globalHeaders, + } + ); + } + if (dataSourceId === env.THEMES_DB) { + const settings = await importNotionSettings(env); + return new Response( + JSON.stringify({ status: "OK", content: { settings } }), + { + headers: globalHeaders, + } + ); + } + if (dataSourceId === env.REDIRECTS_DB) { + const redirects = await importNotionSettings(env); + return new Response( + JSON.stringify({ status: "OK", content: { redirects } }), + { + headers: globalHeaders, + } + ); + } + if (dataSourceId === env.BRANDING_DB) { + const branding = await importNotionSettings(env); + return new Response( + JSON.stringify({ status: "OK", content: { branding } }), + { + headers: globalHeaders, + } + ); + } + console.log("Page Blogs with data source:", dataSourceId); + } + if ( + body.type == "page.properties_updated" || + (body.type == "page.deleted" && body.data?.parent?.data_source_id) + ) { + const dataSourceId = body.data.parent.data_source_id; + const entityId = body?.entity?.id || null; + console.log("Data source ID:", dataSourceId); + console.log("Entity ID:", entityId); + + switch (dataSourceId) { + case env.PAGES_DB: + const pages = await deleteNotionPageFromCache(env, entityId); + await importNotionNavigation(env); + return new Response( + JSON.stringify({ status: "OK", content: { pages } }), + { + headers: globalHeaders, + } + ); + case env.BLOGS_DB: + const blogs = await deleteNotionBlogFromCache(env, entityId); + await importNotionNavigation(env); + return new Response( + JSON.stringify({ status: "OK", content: { blogs } }), + { + headers: globalHeaders, + } + ); + case env.COMPANIES_DB: + const companies = await deleteNotionCompanyFromCache(env, entityId); + return new Response( + JSON.stringify({ status: "OK", content: { companies } }), + { + headers: globalHeaders, + } + ); + case env.POSITIONS_DB: + const positions = await deleteNotionPositionFromCache(env, entityId); + await importNotionCompanies(env); + return new Response( + JSON.stringify({ status: "OK", content: { positions } }), + { + headers: globalHeaders, + } + ); + case env.PROJECTS_DB: + const projects = await deleteNotionProjectFromCache(env, entityId); + return new Response( + JSON.stringify({ status: "OK", content: { projects } }), + { + headers: globalHeaders, + } + ); + case env.CV_DB: + const cvs = await deleteNotionCvFromCache(env, entityId); + return new Response( + JSON.stringify({ status: "OK", content: { cvs } }), + { + headers: globalHeaders, + } + ); + case env.THEMES_DB: + const settings = await importNotionSettings(env); + return new Response( + JSON.stringify({ status: "OK", content: { settings } }), + { + headers: globalHeaders, + } + ); + case env.REDIRECTS_DB: + const redirects = await importNotionSettings(env); + return new Response( + JSON.stringify({ status: "OK", content: { redirects } }), + { + headers: globalHeaders, + } + ); + case env.BRANDING_DB: + const branding = await importNotionSettings(env); + return new Response( + JSON.stringify({ status: "OK", content: { branding } }), + { + headers: globalHeaders, + } + ); + } + console.log("Page Blogs with data source:", dataSourceId); + } + + console.log("Unknown hook:", body.type); + + return new Response( + JSON.stringify({ status: "Unknown hook", type: body.type }), + { + headers: globalHeaders, + } + ); + } catch (error) { + throw error; + console.log("No body to parse. Calling both fetch requests..."); + const content = await updateAllNotionData(env); + return new Response(JSON.stringify({ status: "OK", content }), { + headers: globalHeaders, + }); + } +} diff --git a/src/utils/api.js b/src/utils/api.js new file mode 100644 index 0000000..a1cf5a6 --- /dev/null +++ b/src/utils/api.js @@ -0,0 +1,6 @@ +import { env } from "cloudflare:workers"; + +export const globalHeaders = { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": env.CORS_ORIGIN, +}; diff --git a/src/utils/blogs.js b/src/utils/blogs.js new file mode 100644 index 0000000..6976975 --- /dev/null +++ b/src/utils/blogs.js @@ -0,0 +1,203 @@ +import { extractRichText, getPlainText, transformContent } from "./notion.js"; +import { addToNotionDataSource, updateNotionPage } from "./notion.js"; +import _ from "lodash"; +import diff from "microdiff"; +import { getNavigation } from "../utils/navigation.js"; +import dayjs from "dayjs"; + +export async function getBlogs(env, cached = false) { + try { + // If cached=true, try to get from Cloudflare Cache API first + if (cached) { + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/Blogs` + ); + const cachedResponse = await cache.match(cacheKey); + + if (cachedResponse) { + const cachedData = await cachedResponse.json(); + console.log("Blogs retrieved from Cloudflare cache"); + return cachedData; + } + } catch (cacheError) { + console.log("Cache miss or error, falling back to KV:", cacheError); + } + } + + // Fall back to KV storage + const kvData = await env.CONTENT_KV.get(env.BLOGS_KEY, { + type: "json", + }); + + // If we were trying to use cache but it failed, store the KV data in cache + if (cached && kvData) { + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/Blogs` + ); + + const response = new Response(JSON.stringify(kvData), { + headers: { + "Content-Type": "application/json", + "Cache-Control": "max-age=60", // 1 minute TTL + ETag: `"Blogs-${Date.now()}"`, // Add ETag for cache validation + }, + }); + + await cache.put(cacheKey, response); + console.log("Blogs stored in Cloudflare cache after KV fallback"); + } catch (cacheError) { + console.warn("Error storing in cache after KV fallback:", cacheError); + } + } + + return kvData || []; + } catch (error) { + console.log("Error fetching Blogs cache:", error); + return null; + } +} + +export async function storeBlogs(env, Blogs) { + try { + // Always store in KV first + await env.CONTENT_KV.put(env.BLOGS_KEY, JSON.stringify(Blogs)); + console.log("Blogs stored in KV."); + + // Purge the Cloudflare Cache API + try { + const cache = caches.default; + const cacheKey = new Request(`https://${env.CACHE_URL || "cache"}/Blogs`); + + await cache.delete(cacheKey); + console.log("Blogs cache purged successfully."); + } catch (cacheError) { + console.warn( + "Error purging Cloudflare cache, but KV was updated successfully:", + cacheError + ); + } + } catch (error) { + console.error("Error storing Blogs cache:", error); + throw error; + } +} + +export function diffBlogs(newList, oldList) { + // Keys to check for changes (set to null to check all keys) + const keysToCheck = [ + "name", + "maxOccupancy", + "bedrooms", + "bathrooms", + "address", + "features", + "minPrice", + "maxPrice", + "timezone", + "doubleBeds", + "singleBeds", + "sofaBeds", + "couches", + "childBeds", + "queenSizeBeds", + "kingSizeBeds", + ]; + + // Helper: index by smoobuId using lodash + const oldById = _.keyBy(oldList || [], "smoobuId"); + const newById = _.keyBy(newList || [], "smoobuId"); + + // toAdd: in newList but not in oldList + const toAdd = newList.filter((p) => !oldById[p.smoobuId]); + + // toDelete: in oldList but not in newList + const toDelete = oldList.filter((p) => !newById[p.smoobuId]); + + // toUpdate: in both, but with different content (using microdiff) + const toUpdate = newList.filter((p) => { + const old = oldById[p.smoobuId]; + if (!old) return false; + + // If syncName is false, exclude name from comparison + const { smoobuId, notionId, name, ...restNew } = p; + const { smoobuId: _, notionId: __, name: oldName, ...restOld } = old; + + // Prepare objects for comparison + let newObj, oldObj; + + if (old.syncName === false) { + // Don't compare names when syncName is false + newObj = restNew; + oldObj = restOld; + } else { + // Include name in comparison when syncName is true or undefined + newObj = { name, ...restNew }; + oldObj = { name: oldName, ...restOld }; + } + + // Use microdiff to get differences + const differences = diff(oldObj, newObj); + + // If no keysToCheck specified, return true if any differences found + if (!keysToCheck) { + return differences.length > 0; + } + + // Filter differences to only include keys we care about + const relevantDifferences = differences.filter((change) => { + // Check if the changed path starts with any of our keysToCheck + const path = change.path.join("."); + return keysToCheck.some((key) => path.startsWith(key)); + }); + + return relevantDifferences.length > 0; + }); + + return { toAdd, toUpdate, toDelete }; +} + +// Transform Notion blog data to desired format +export async function transformNotionBlog(env, notionBlog) { + const navigationItems = await getNavigation(env); + + const properties = notionBlog.properties; + console.log("Properties:", properties); + + const published = properties["Published"].checkbox || false; + + const dateRaw = properties["Date"].date; + const date = dateRaw?.start + ? dayjs(dateRaw.start).format("DD/MM/YY h:mma") + : null; + + // Extract image URLs (handle both external and file images) + const images = (properties["Images"]?.files || []) + .map((f) => f?.external?.url || f?.file?.url) + .filter(Boolean); + + // Extract slug from formula + const slug = properties.Slug?.formula?.string || "unknown"; + + const subTitle = getPlainText(properties["Subtitle"]); + + // Extract name from title + const name = properties.Name?.title?.[0]?.plain_text || "Untitled"; + + // Fetch and transform the actual page content from Notion blocks + const content = await transformContent(env, notionBlog.id, navigationItems); + + return { + notionId: notionBlog.id, + name: name, + subTitle: subTitle, + images: images, + slug: slug, + date: date, + content, + published, + }; +} diff --git a/src/utils/blurHash.js b/src/utils/blurHash.js new file mode 100644 index 0000000..d556661 --- /dev/null +++ b/src/utils/blurHash.js @@ -0,0 +1,110 @@ +import { encode } from "blurhash"; +import JPEG from "jpeg-js"; +import { env } from "cloudflare:workers"; + +// Function to decode JPEG image data using jpeg-js (Cloudflare Workers compatible) +export function decodeJPEG(arrayBuffer) { + try { + const buffer = new Uint8Array(arrayBuffer); + + console.log("Decoding JPG..."); + const jpeg = JPEG.decode(buffer, { useTArray: true }); + console.log("Decoded JPG."); + + if (!jpeg) { + throw new Error("Failed to decode JPEG"); + } + + return { + data: jpeg.data, + width: jpeg.width, + height: jpeg.height, + }; + } catch (error) { + throw new Error(`JPEG decode error: ${error.message}`); + } +} + +// Function to detect image type from URL or content (only JPEG) +export function detectImageType(url) { + const urlLower = url.toLowerCase(); + if (urlLower.includes(".jpg") || urlLower.includes(".jpeg")) { + return "jpeg"; + } + + // Default to JPEG for backward compatibility + return "jpeg"; +} + +// Function to fetch an image and generate a blur hash (no caching, JPEG only) +export async function fetchBlurHash(url) { + try { + if (env.BLUR_HASH === "false") { + console.log("Blur hash disabled"); + return "LDMZ?kMaD$?w.9R:NIIU=V?bw[RP"; + } + + console.log("Fetching URL for blur hash:", url); + const options = { + cf: { + image: { + compression: "fast", + width: 100, + height: 100, + fit: "cover", + quality: 80, + format: "jpeg", + }, + }, + }; + + const response = await fetch(url, options); + console.log("Fetch complete."); + + if (!response.ok) { + console.log("Failed to fetch image:", response.status); + return null; + } + + let arrayBuffer = await response.arrayBuffer(); + + const transformed = await env.IMAGES.input(arrayBuffer) + .transform({ width: 128, height: 128 }) // can combine width/height + .output({ format: "image/jpeg" }); + + const compressedArrayBuffer = + (await transformed.arrayBuffer?.()) || + (await transformed.response().arrayBuffer()); + + const fileSizeMB = ( + compressedArrayBuffer.byteLength / + (1024 * 1024) + ).toFixed(2); + console.log( + "File size:", + compressedArrayBuffer.byteLength, + "bytes (", + fileSizeMB, + "MB)", + ); + + // Decode JPEG + const decodedImage = decodeJPEG(compressedArrayBuffer); + const { data, width, height } = decodedImage; + + // Generate blur hash + console.log("Encoding blur hash..."); + const blurHash = encode(data, width, height, 4, 3); + console.log("Encoded blur hash."); + + // Clear memory + console.log("Clearing memory..."); + data.fill(0); + arrayBuffer = null; + + return blurHash; + } catch (error) { + console.error(`Error generating blur hash for ${url}:`, error); + return null; + } +} diff --git a/src/utils/companies.js b/src/utils/companies.js new file mode 100644 index 0000000..be8dd6d --- /dev/null +++ b/src/utils/companies.js @@ -0,0 +1,237 @@ +import { getItemByNotionId, transformContent, sortDuration } from "./notion.js"; +import _ from "lodash"; +import diff from "microdiff"; +import { getNavigation } from "../utils/navigation.js"; +import { processNotionIcon } from "./icon.js"; + +export async function getCompanies(env, cached = false) { + try { + // If cached=true, try to get from Cloudflare Cache API first + if (cached) { + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/Companies` + ); + const cachedResponse = await cache.match(cacheKey); + + if (cachedResponse) { + const cachedData = await cachedResponse.json(); + console.log("Companies retrieved from Cloudflare cache"); + return cachedData; + } + } catch (cacheError) { + console.log("Cache miss or error, falling back to KV:", cacheError); + } + } + + // Fall back to KV storage + const kvData = await env.CONTENT_KV.get(env.COMPANIES_KEY, { + type: "json", + }); + + // If we were trying to use cache but it failed, store the KV data in cache + if (cached && kvData) { + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/Companies` + ); + + const response = new Response(JSON.stringify(kvData), { + headers: { + "Content-Type": "application/json", + "Cache-Control": "max-age=60", // 1 minute TTL + ETag: `"Companies-${Date.now()}"`, // Add ETag for cache validation + }, + }); + + await cache.put(cacheKey, response); + console.log("Companies stored in Cloudflare cache after KV fallback"); + } catch (cacheError) { + console.warn("Error storing in cache after KV fallback:", cacheError); + } + } + + return kvData || []; + } catch (error) { + console.log("Error fetching Companies cache:", error); + return null; + } +} + +export async function storeCompanies(env, Companies) { + try { + // Always store in KV first + await env.CONTENT_KV.put(env.COMPANIES_KEY, JSON.stringify(Companies)); + console.log("Companies stored in KV."); + + // Purge the Cloudflare Cache API + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/Companies` + ); + + await cache.delete(cacheKey); + console.log("Companies cache purged successfully."); + } catch (cacheError) { + console.warn( + "Error purging Cloudflare cache, but KV was updated successfully:", + cacheError + ); + } + } catch (error) { + console.error("Error storing Companies cache:", error); + throw error; + } +} + +export function diffCompanies(newList, oldList) { + // Keys to check for changes + const keysToCheck = [ + "name", + "theme", + "published", + "logo", + "image", + "content", + ]; + + // Helper: index by notionId using lodash + const oldById = _.keyBy(oldList || [], "notionId"); + const newById = _.keyBy(newList || [], "notionId"); + + // toAdd: in newList but not in oldList + const toAdd = newList.filter((p) => !oldById[p.notionId]); + + // toDelete: in oldList but not in newList + const toDelete = oldList.filter((p) => !newById[p.notionId]); + + // toUpdate: in both, but with different content (using microdiff) + const toUpdate = newList.filter((p) => { + const old = oldById[p.notionId]; + if (!old) return false; + + // Prepare objects for comparison + const { notionId, ...restNew } = p; + const { notionId: _, ...restOld } = old; + + // Use microdiff to get differences + const differences = diff(restOld, restNew); + + // Filter differences to only include keys we care about + const relevantDifferences = differences.filter((change) => { + // Check if the changed path starts with any of our keysToCheck + const path = change.path.join("."); + return keysToCheck.some((key) => path.startsWith(key)); + }); + + return relevantDifferences.length > 0; + }); + + return { toAdd, toUpdate, toDelete }; +} + +// Transform Notion company data to desired format +export async function transformNotionCompany( + env, + notionCompany, + settingsData, + positionsData +) { + const navigationItems = await getNavigation(env); + + const properties = notionCompany.properties; + + // Extract theme information + let theme = undefined; // default + if (properties.Theme?.relation?.[0]?.id) { + const themeId = properties.Theme.relation[0].id; + const themeData = getItemByNotionId(settingsData.themes, themeId); + if (themeData?.name) { + theme = themeData.name; + } + } + + const published = properties["Published"]?.checkbox || false; + + // Extract external link + const externalLink = properties["External Link"]?.url || null; + + // Extract image URL (handle both external and file images) + const imageFile = notionCompany?.cover; + const image = imageFile?.external?.url || imageFile?.file?.url || null; + + // Extract logo URL (handle both external and file images) + const logoFiles = properties["Logo"]?.files || []; + const logo = logoFiles[0]?.file + ? await processNotionIcon(logoFiles[0].file, "tb-company-logo") + : undefined; + + // Extract name from title + const name = properties.Name?.title?.[0]?.plain_text || "Untitled"; + + // Extract slug from formula + const slug = properties.Slug?.formula?.string || "unknown"; + + // Fetch and transform the actual page content from Notion blocks + const content = [{ type: "image", url: image }]; + const companyContent = await transformContent( + env, + notionCompany.id, + navigationItems + ); + content.push(...companyContent); + + const filteredPositions = positionsData + .filter((position) => position.company === notionCompany.id) + .sort(sortDuration); + + // Add positions timeline at the end if positions are provided + if (filteredPositions && filteredPositions.length > 0) { + // Filter positions for this company based on company name + + const timelinePositions = filteredPositions.map((position) => ({ + type: "positionTimelineItem", + name: position.name, + duration: position.duration, + content: position?.content || [], + })); + + if (timelinePositions.length > 0) { + content.push({ + type: "positionsTimeline", + children: timelinePositions, + }); + } + } + + if (published == false) { + return null; + } + + const positionsList = filteredPositions.map((position) => ({ + name: position.name, + notionId: position.notionId, + })); + + const duration = { + start: filteredPositions[filteredPositions.length - 1]?.duration?.start, + end: filteredPositions[0]?.duration?.end, + }; + + return { + notionId: notionCompany.id, + name: name, + theme: theme, + published, + logo: logo, + image: image, + slug: slug, + positions: positionsList, + externalLink: externalLink, + duration: duration, + content, + }; +} diff --git a/src/utils/contentCache.js b/src/utils/contentCache.js new file mode 100644 index 0000000..f810691 --- /dev/null +++ b/src/utils/contentCache.js @@ -0,0 +1,44 @@ +import { getImages } from "./imageCache.js"; +import { getPages } from "./pages.js"; +import { getBlogs } from "./blogs.js"; +import { getSettings } from "./settings.js"; +import { getProjects } from "./projects.js"; +import { getCompanies } from "./companies.js"; +import { getCvs } from "./cv.js"; +import { getFiles } from "./fileCache.js"; +import { getVideos } from "./videoCache.js"; + +export async function getCombinedCachedContent(env) { + try { + // Get current images from image cache + const cachedImages = await getImages(env, true); + const cachedFiles = await getFiles(env, true); + const cachedVideos = await getVideos(env, true); + const cachedPages = await getPages(env, true); + const cachedBlogs = await getBlogs(env, true); + const cachedSettings = await getSettings(env, true); + const cachedProjects = await getProjects(env, true); + const cachedCompanies = await getCompanies(env, true); + const cachedCvs = await getCvs(env, true); + // Combine content with current images + const content = { + pages: cachedPages, + blogs: cachedBlogs, + settings: cachedSettings, + images: cachedImages, + files: cachedFiles, + videos: cachedVideos, + projects: cachedProjects, + companies: cachedCompanies, + cvs: cachedCvs, + }; + + console.log("Serving content..."); + return content; + + return null; + } catch (error) { + console.log("Error getting combined cached content:", error); + return null; + } +} diff --git a/src/utils/cv.js b/src/utils/cv.js new file mode 100644 index 0000000..435efd2 --- /dev/null +++ b/src/utils/cv.js @@ -0,0 +1,154 @@ +import { getPlainText, toCamelCase } from "./notion.js"; +import _ from "lodash"; +import diff from "microdiff"; + +export async function getCvs(env, cached = false) { + try { + // If cached=true, try to get from Cloudflare Cache API first + if (cached) { + try { + const cache = caches.default; + const cacheKey = new Request(`https://${env.CACHE_URL || "cache"}/CVs`); + const cachedResponse = await cache.match(cacheKey); + + if (cachedResponse) { + const cachedData = await cachedResponse.json(); + console.log("CVs retrieved from Cloudflare cache"); + return cachedData; + } + } catch (cacheError) { + console.log("Cache miss or error, falling back to KV:", cacheError); + } + } + + // Fall back to KV storage + const kvData = await env.CONTENT_KV.get(env.CV_KEY, { + type: "json", + }); + + // If we were trying to use cache but it failed, store the KV data in cache + if (cached && kvData) { + try { + const cache = caches.default; + const cacheKey = new Request(`https://${env.CACHE_URL || "cache"}/CVs`); + + const response = new Response(JSON.stringify(kvData), { + headers: { + "Content-Type": "application/json", + "Cache-Control": "max-age=60", // 1 minute TTL + ETag: `"CVs-${Date.now()}"`, // Add ETag for cache validation + }, + }); + + await cache.put(cacheKey, response); + console.log("CVs stored in Cloudflare cache after KV fallback"); + } catch (cacheError) { + console.warn("Error storing in cache after KV fallback:", cacheError); + } + } + + return kvData || []; + } catch (error) { + console.log("Error fetching CVs cache:", error); + return null; + } +} + +export async function storeCvs(env, Cvs) { + try { + // Always store in KV first + await env.CONTENT_KV.put(env.CV_KEY, JSON.stringify(Cvs)); + console.log("CVs stored in KV."); + + // Purge the Cloudflare Cache API + try { + const cache = caches.default; + const cacheKey = new Request(`https://${env.CACHE_URL || "cache"}/CVs`); + + await cache.delete(cacheKey); + console.log("CVs cache purged successfully."); + } catch (cacheError) { + console.warn( + "Error purging Cloudflare cache, but KV was updated successfully:", + cacheError + ); + } + } catch (error) { + console.error("Error storing CVs cache:", error); + throw error; + } +} + +export function diffCvs(newList, oldList) { + // Keys to check for changes + const keysToCheck = ["version", "type", "published", "files"]; + + // Helper: index by notionId using lodash + const oldById = _.keyBy(oldList || [], "notionId"); + const newById = _.keyBy(newList || [], "notionId"); + + // toAdd: in newList but not in oldList + const toAdd = newList.filter((p) => !oldById[p.notionId]); + + // toDelete: in oldList but not in newList + const toDelete = oldList.filter((p) => !newById[p.notionId]); + + // toUpdate: in both, but with different content (using microdiff) + const toUpdate = newList.filter((p) => { + const old = oldById[p.notionId]; + if (!old) return false; + + // Prepare objects for comparison + const { notionId, ...restNew } = p; + const { notionId: _, ...restOld } = old; + + // Use microdiff to get differences + const differences = diff(restOld, restNew); + + // Filter differences to only include keys we care about + const relevantDifferences = differences.filter((change) => { + // Check if the changed path starts with any of our keysToCheck + const path = change.path.join("."); + return keysToCheck.some((key) => path.startsWith(key)); + }); + + return relevantDifferences.length > 0; + }); + + return { toAdd, toUpdate, toDelete }; +} + +// Transform Notion CV data to desired format +export async function transformNotionCv(env, notionCv) { + const properties = notionCv.properties; + + console.log("Notion CV:", notionCv); + + const published = properties["Published"]?.checkbox || false; + + // Extract version (text) + const version = getPlainText(properties["Version"]); + + // Extract type (select) + const type = toCamelCase(properties["Type"]?.select?.name || null); + + const date = properties["Date"]?.date?.start || undefined; + + // Extract files (files array) + const files = (properties["Files"]?.files || []) + .map((f) => f?.external?.url || f?.file?.url) + .filter(Boolean); + + if (published == false) { + return null; + } + + return { + notionId: notionCv.id, + version: version, + type: type, + published: published, + files: files, + date: date, + }; +} diff --git a/src/utils/fileCache.js b/src/utils/fileCache.js new file mode 100644 index 0000000..a82dae5 --- /dev/null +++ b/src/utils/fileCache.js @@ -0,0 +1,369 @@ +function generateFileId(url) { + const urlObj = new URL(url); + const origin = urlObj.origin; + const pathname = urlObj.pathname; + + const combined = `${origin}${pathname}`; + + let hash = 0; + for (let i = 0; i < combined.length; i++) { + const char = combined.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; + } + + const positiveHash = Math.abs(hash); + const base36Hash = positiveHash.toString(36); + + return base36Hash.padStart(8, "0").substring(0, 12); +} + +function generateR2KeyFromFileId(fileId, url) { + const urlObj = new URL(url); + const pathname = urlObj.pathname; + const filename = pathname.split("/").pop() || "file"; + const extension = filename.includes(".") ? filename.split(".").pop() : ""; + const baseFilename = filename.replace(/\.[^/.]+$/, ""); + + return extension + ? `files/${fileId}-${baseFilename}.${extension}` + : `files/${fileId}-${baseFilename}`; +} + +function extractR2KeyFromUrl(mirrorUrl) { + const urlObj = new URL(mirrorUrl); + return urlObj.pathname.substring(1); +} + +export async function getFiles(env, cached = false) { + try { + if (cached) { + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/files` + ); + const cachedResponse = await cache.match(cacheKey); + + if (cachedResponse) { + const cachedData = await cachedResponse.json(); + console.log("Files retrieved from Cloudflare cache"); + return cachedData; + } + } catch (cacheError) { + console.log( + "File cache miss or error, falling back to KV:", + cacheError + ); + } + } + + const kvData = await env.CONTENT_KV.get(env.FILES_KEY, { + type: "json", + }); + + if (cached && kvData) { + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/files` + ); + + const response = new Response(JSON.stringify(kvData), { + headers: { + "Content-Type": "application/json", + "Cache-Control": "max-age=60", + ETag: `"files-${Date.now()}"`, + }, + }); + + await cache.put(cacheKey, response); + console.log("Files stored in Cloudflare cache after KV fallback"); + } catch (cacheError) { + console.warn( + "Error storing files in cache after KV fallback:", + cacheError + ); + } + } + + return kvData || []; + } catch (error) { + console.log("Error fetching files cache:", error); + return null; + } +} + +export async function updateFiles(env, currentFileUrls) { + console.log("Updating file cache..."); + + const currentFiles = currentFileUrls.map((url) => ({ + id: generateFileId(url), + url, + })); + + const existingFiles = await getFiles(env); + const existingFileMap = new Map(existingFiles.map((file) => [file.id, file])); + + const filesToAdd = currentFiles.filter( + (file) => !existingFileMap.has(file.id) + ); + + const currentFileMap = new Map(currentFiles.map((file) => [file.id, file])); + const filesToKeep = existingFiles + .filter((file) => currentFileMap.has(file.id)) + .map((file) => ({ + ...file, + url: currentFileMap.get(file.id).url, + })); + + const filesToRemove = existingFiles.filter( + (file) => !currentFileMap.has(file.id) + ); + + console.log( + `Files to add: ${filesToAdd.length}, Files to keep: ${filesToKeep.length}, Files to remove: ${filesToRemove.length}` + ); + + for (const file of filesToRemove) { + if (file.mirrorUrl) { + try { + const key = extractR2KeyFromUrl(file.mirrorUrl); + await env.TB_STORAGE.delete(key); + console.log(`Deleted ${key} from R2 for file ID ${file.id}`); + } catch (error) { + console.error( + `Failed to delete ${file.mirrorUrl} from R2 for file ID ${file.id}:`, + error + ); + } + } + } + + const newFiles = []; + for (let i = 0; i < filesToAdd.length; i++) { + const fileData = filesToAdd[i]; + const { id, url } = fileData; + + console.log( + `Uploading file to R2 (${i + 1}/${filesToAdd.length}): ${url} (ID: ${id})` + ); + + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch file: ${response.status}`); + } + + const contentType = response.headers.get("content-type") || null; + const contentLengthHeader = response.headers.get("content-length"); + const contentLength = contentLengthHeader + ? Number.parseInt(contentLengthHeader, 10) + : null; + + const urlObj = new URL(url); + const pathname = urlObj.pathname; + const filename = pathname.split("/").pop() || null; + + const key = generateR2KeyFromFileId(id, url); + + const putOptions = { + httpMetadata: {}, + }; + if (contentType) { + putOptions.httpMetadata.contentType = contentType; + } + if (Object.keys(putOptions.httpMetadata).length === 0) { + delete putOptions.httpMetadata; + } + + const result = await env.TB_STORAGE.put( + key, + response.body, + Object.keys(putOptions).length ? putOptions : undefined + ); + + if (result == null) { + throw new Error(`Failed to upload file to R2: ${key}`); + } + + const mirrorUrl = `${env.R2_PUBLIC_URL}/${key}`; + + const metadata = + contentType || contentLength || filename + ? { + contentType, + contentLength, + filename, + } + : null; + + newFiles.push({ + id, + url, + metadata, + mirrorUrl, + }); + + console.log( + `Successfully uploaded and mirrored file: ${url} -> ${mirrorUrl} (ID: ${id})` + ); + } catch (error) { + console.error(`Failed to upload ${url} to R2 (ID: ${id}):`, error); + newFiles.push({ + id, + url, + metadata: null, + mirrorUrl: null, + }); + } + + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + const updatedFiles = [...filesToKeep, ...newFiles]; + + await storeFiles(env, updatedFiles); + + return updatedFiles; +} + +export async function storeFiles(env, files) { + try { + await env.CONTENT_KV.put(env.FILES_KEY, JSON.stringify(files)); + console.log("Files stored in KV."); + + try { + const cache = caches.default; + const cacheKey = new Request(`https://${env.CACHE_URL || "cache"}/files`); + + await cache.delete(cacheKey); + console.log("Files cache purged successfully."); + } catch (cacheError) { + console.warn( + "Error purging Cloudflare file cache, but KV was updated successfully:", + cacheError + ); + } + } catch (error) { + console.error("Error storing files cache:", error); + throw error; + } +} + +function findFileChildren(obj, fileUrls = []) { + if (!obj || typeof obj !== "object") { + return fileUrls; + } + + if (obj.type === "file") { + if (obj.url) { + fileUrls.push(obj.url); + } else if (obj.href) { + fileUrls.push(obj.href); + } else if (obj.file) { + fileUrls.push(obj.file); + } + } + + if (obj.children && Array.isArray(obj.children)) { + for (const child of obj.children) { + findFileChildren(child, fileUrls); + } + } + + for (const [key, value] of Object.entries(obj)) { + if ( + key !== "children" && + key !== "content" && + key !== "blocks" && + typeof value === "object" + ) { + findFileChildren(value, fileUrls); + } + } + + return fileUrls; +} + +export function collectFileUrls(contentObjects) { + const fileUrls = []; + + for (const object of contentObjects) { + if (object.file) { + fileUrls.push(object.file); + } + if (object.files) { + object.files.forEach((url) => fileUrls.push(url)); + } + if (object.content) { + findFileChildren(object.content, fileUrls); + } + } + + return fileUrls; +} + +export async function handleFileMetadataUpdate(request, env) { + const cachedFiles = await env.CONTENT_KV.get(env.FILES_KEY, { + type: "json", + }); + if (!cachedFiles) return; + + let updated = false; + + for (const file of cachedFiles) { + if ( + !file.metadata || + !file.metadata.contentType || + !file.metadata.contentLength + ) { + const targetUrl = file.mirrorUrl || file.url; + try { + const response = await fetch(targetUrl, { method: "HEAD" }); + if (response.ok) { + const contentType = response.headers.get("content-type") || null; + const contentLengthHeader = response.headers.get("content-length"); + const contentLength = contentLengthHeader + ? Number.parseInt(contentLengthHeader, 10) + : null; + + const urlObj = new URL(file.url); + const pathname = urlObj.pathname; + const filename = pathname.split("/").pop() || null; + + if (contentType || contentLength || filename) { + file.metadata = { + contentType, + contentLength, + filename, + }; + updated = true; + console.log( + `Updated metadata for file ID ${file.id} (${file.url})` + ); + } + } + } catch (error) { + console.warn( + `Failed to update metadata for file ID ${file.id} (${file.url}):`, + error + ); + } + } + } + + if (updated) { + await env.CONTENT_KV.put(env.FILES_KEY, JSON.stringify(cachedFiles)); + } + + return cachedFiles; +} + +export function getFileById(cachedFiles, fileId) { + return cachedFiles.find((file) => file.id === fileId); +} + +export function getFileIdFromUrl(url) { + return generateFileId(url); +} diff --git a/src/utils/icon.js b/src/utils/icon.js new file mode 100644 index 0000000..4a88402 --- /dev/null +++ b/src/utils/icon.js @@ -0,0 +1,72 @@ +import { parse, stringify } from "svgson"; + +async function processSVG(svgText, cssClass) { + try { + // Parse SVG to JSON + const json = await parse(svgText); + + // Remove all fill attributes and fill styles recursively + const removeFill = (node) => { + if (node.attributes?.fill) delete node.attributes.fill; + + // Remove fill from inline styles + if (node.attributes?.style) { + const styles = node.attributes.style + .split(";") + .map((s) => s.trim()) + .filter((s) => s && !s.toLowerCase().startsWith("fill:")) + .join("; "); + + if (styles) { + node.attributes.style = styles; + } else { + delete node.attributes.style; + } + } + + if (node.children) node.children.forEach(removeFill); + }; + removeFill(json); + + // Add class to root + json.attributes.class = cssClass; + + // Convert back to SVG string + return stringify(json); + } catch (err) { + throw new Error(`Error processing SVG: ${err.message}`); + } +} + +// processNotionIcon fetches and validates the SVG +export async function processNotionIcon(iconObj, cssClass = "") { + if (iconObj?.external?.url && iconObj?.url) { + return undefined; + } + + const url = iconObj?.external?.url || iconObj?.file?.url || iconObj.url; + + const urlObject = new URL(url); + + // Fetch the file headers first + console.log("Fetching icon:", urlObject.origin + urlObject.pathname); + const svgResponse = await fetch(url, { method: "GET" }); + const contentType = svgResponse.headers.get("content-type") || ""; + + if (!contentType.includes("image/svg+xml")) { + console.log(`Not an SVG file (content-type was ${contentType}).`); + return `Page Icon`; + } + + console.log("Fetched SVG:", urlObject.origin + urlObject.pathname); + if (!svgResponse.ok) { + throw new Error( + `Failed to fetch SVG: ${svgResponse.status} ${svgResponse.statusText}` + ); + } + + const svgText = await svgResponse.text(); + const processedSVG = processSVG(svgText, cssClass); + + return processedSVG; +} diff --git a/src/utils/imageCache.js b/src/utils/imageCache.js new file mode 100644 index 0000000..2203915 --- /dev/null +++ b/src/utils/imageCache.js @@ -0,0 +1,349 @@ +import { fetchBlurHash } from "./blurHash"; + +function generateImageId(url) { + const urlObj = new URL(url); + const origin = urlObj.origin; + const pathname = urlObj.pathname; + + // Create a consistent hash from origin + pathname + const combined = `${origin}${pathname}`; + + // Simple hash function for better distribution + let hash = 0; + for (let i = 0; i < combined.length; i++) { + const char = combined.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; // Convert to 32-bit integer + } + + // Convert to positive number and create readable ID + const positiveHash = Math.abs(hash); + const base36Hash = positiveHash.toString(36); + + // Pad with zeros if needed and limit length + return base36Hash.padStart(8, "0").substring(0, 12); +} + +// Generate R2 key from image ID and original URL +function generateR2KeyFromImageId(imageId, url) { + const urlObj = new URL(url); + const pathname = urlObj.pathname; + const filename = pathname.split("/").pop() || "image"; + const extension = filename.includes(".") ? filename.split(".").pop() : "jpg"; + const baseFilename = filename.replace(/\.[^/.]+$/, ""); + + return `images/${imageId}-${baseFilename}.${extension}`; +} + +function extractR2KeyFromUrl(mirrorUrl) { + // Extract the key from a mirror URL like "https://r2-domain.com/images/abc123-image.jpg" + const urlObj = new URL(mirrorUrl); + return urlObj.pathname.substring(1); // Remove leading slash +} + +export async function getImages(env, cached = false) { + try { + // If cached=true, try to get from Cloudflare Cache API first + if (cached) { + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/images` + ); + const cachedResponse = await cache.match(cacheKey); + + if (cachedResponse) { + const cachedData = await cachedResponse.json(); + console.log("Images retrieved from Cloudflare cache"); + return cachedData; + } + } catch (cacheError) { + console.log("Cache miss or error, falling back to KV:", cacheError); + } + } + + // Fall back to KV storage + const kvData = await env.CONTENT_KV.get(env.IMAGES_KEY, { + type: "json", + }); + + // If we were trying to use cache but it failed, store the KV data in cache + if (cached && kvData) { + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/images` + ); + + const response = new Response(JSON.stringify(kvData), { + headers: { + "Content-Type": "application/json", + "Cache-Control": "max-age=60", // 1 minute TTL + ETag: `"images-${Date.now()}"`, // Add ETag for cache validation + }, + }); + + await cache.put(cacheKey, response); + console.log("Images stored in Cloudflare cache after KV fallback"); + } catch (cacheError) { + console.warn("Error storing in cache after KV fallback:", cacheError); + } + } + + return kvData || []; + } catch (error) { + console.log("Error fetching images cache:", error); + return null; + } +} + +export async function updateImages(env, currentImageUrls) { + console.log("Updating image cache..."); + + // Convert URLs to image objects with IDs + const currentImages = currentImageUrls.map((url) => ({ + id: generateImageId(url), + url, + })); + + // Get existing image cache + const existingImages = await getImages(env); + const existingImageMap = new Map(existingImages.map((img) => [img.id, img])); + + // Find images to add (current images with IDs not in cache) + const imagesToAdd = currentImages.filter( + (img) => !existingImageMap.has(img.id) + ); + + // Find images to keep (existing images with IDs in current set) + const currentImageMap = new Map(currentImages.map((img) => [img.id, img])); + const imagesToKeep = existingImages + .filter((img) => currentImageMap.has(img.id)) + .map((img) => ({ + ...img, + // Update URL in case it has changed (different query params, etc.) + url: currentImageMap.get(img.id).url, + })); + + // Find images to remove (existing images with IDs not in current set) + const imagesToRemove = existingImages.filter( + (img) => !currentImageMap.has(img.id) + ); + + console.log( + `Images to add: ${imagesToAdd.length}, Images to keep: ${imagesToKeep.length}, Images to remove: ${imagesToRemove.length}` + ); + + // Delete removed images from R2 + for (const image of imagesToRemove) { + if (image.mirrorUrl) { + try { + const key = extractR2KeyFromUrl(image.mirrorUrl); + await env.TB_STORAGE.delete(key); + console.log(`Deleted ${key} from R2 for image ID ${image.id}`); + } catch (error) { + console.error( + `Failed to delete ${image.mirrorUrl} from R2 for image ID ${image.id}:`, + error + ); + } + } + } + + // Upload new images to R2 and create mirror URLs + const newImages = []; + for (let i = 0; i < imagesToAdd.length; i++) { + const imageData = imagesToAdd[i]; + const { id, url } = imageData; + + console.log( + `Uploading image to R2 (${i + 1}/${ + imagesToAdd.length + }): ${url} (ID: ${id})` + ); + + try { + // Fetch the image + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch image: ${response.status}`); + } + + // Generate R2 key from image ID + const key = generateR2KeyFromImageId(id, url); + + const result = await env.TB_STORAGE.put(key, response.body); + + if (result == null) { + throw new Error(`Failed to upload image to R2: ${key}`); + } + + // Create mirror URL + const mirrorUrl = `${env.R2_PUBLIC_URL}/${key}`; + + newImages.push({ + id, + url, + blurHash: null, + mirrorUrl, + }); + + console.log( + `Successfully uploaded and mirrored: ${url} -> ${mirrorUrl} (ID: ${id})` + ); + } catch (error) { + console.error(`Failed to upload ${url} to R2 (ID: ${id}):`, error); + // Add image without mirror URL if upload fails + newImages.push({ + id, + url, + blurHash: null, + mirrorUrl: null, + }); + } + + // Small delay to avoid overwhelming the system + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + // Combine kept images with new images + const updatedImages = [...imagesToKeep, ...newImages]; + + // Store updated image cache + await storeImages(env, updatedImages); + + return updatedImages; +} + +export async function storeImages(env, images) { + try { + // Always store in KV first + await env.CONTENT_KV.put(env.IMAGES_KEY, JSON.stringify(images)); + console.log("Images stored in KV."); + + // Purge the Cloudflare Cache API + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/images` + ); + + await cache.delete(cacheKey); + console.log("Images cache purged successfully."); + } catch (cacheError) { + console.warn( + "Error purging Cloudflare cache, but KV was updated successfully:", + cacheError + ); + } + } catch (error) { + console.error("Error storing images cache:", error); + throw error; + } +} + +// Recursively traverse objects to find children with type: "image" +function findImageChildren(obj, imageUrls = []) { + if (!obj || typeof obj !== "object") { + return imageUrls; + } + + // Check if this object has type: "image" + if (obj.type === "image") { + // Extract image URL from various possible properties + if (obj.url) { + imageUrls.push(obj.url); + } else if (obj.src) { + imageUrls.push(obj.src); + } else if (obj.image) { + imageUrls.push(obj.image); + } + } + + // Recursively check children array + if (obj.children && Array.isArray(obj.children)) { + for (const child of obj.children) { + findImageChildren(child, imageUrls); + } + } + + // Recursively check all other object properties + for (const [key, value] of Object.entries(obj)) { + if ( + key !== "children" && + key !== "content" && + key !== "blocks" && + typeof value === "object" + ) { + findImageChildren(value, imageUrls); + } + } + + return imageUrls; +} + +export function collectImageUrls(contentObjects) { + const imageUrls = []; + + // Collect from pages + for (const object of contentObjects) { + if (object.image) { + imageUrls.push(object.image); + } + if (object.images) { + object.images.forEach((url) => imageUrls.push(url)); + } + if (object.content) { + // Use recursive function to find all image children + findImageChildren(object.content, imageUrls); + } + } + + return imageUrls; +} + +export async function handleBlurhashUpdate(request, env) { + // Read the image cache + const cachedImages = await env.CONTENT_KV.get(env.IMAGES_KEY, { + type: "json", + }); + if (!cachedImages) return; + + let updated = false; + + for (const image of cachedImages) { + if (image.blurHash === null) { + // Try mirrorUrl first + let blurHash = image.mirrorUrl + ? await fetchBlurHash(image.mirrorUrl) + : null; + // Fallback to original URL if mirrorUrl fails + if (!blurHash) { + blurHash = await fetchBlurHash(image.url); + } + + if (blurHash) { + image.blurHash = blurHash; + updated = true; + console.log(`Updated blurHash for image ID ${image.id} (${image.url})`); + } + } + } + + // Store updated cache if anything changed + if (updated) { + await env.CONTENT_KV.put(env.IMAGES_KEY, JSON.stringify(cachedImages)); + } + + return cachedImages; +} + +// Helper function to get image by ID +export function getImageById(cachedImages, imageId) { + return cachedImages.find((img) => img.id === imageId); +} + +// Helper function to get image ID from URL +export function getImageIdFromUrl(url) { + return generateImageId(url); +} diff --git a/src/utils/navigation.js b/src/utils/navigation.js new file mode 100644 index 0000000..172a740 --- /dev/null +++ b/src/utils/navigation.js @@ -0,0 +1,90 @@ +import { processNotionIcon } from "./icon"; + +export async function getNavigation(env, cached = false) { + try { + // If cached=true, try to get from Cloudflare Cache API first + if (cached) { + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.NAVIGATION_CACHE_URL || "cache"}/navigation` + ); + const cachedResponse = await cache.match(cacheKey); + + if (cachedResponse) { + const cachedData = await cachedResponse.json(); + console.log("Navigation retrieved from Cloudflare cache"); + return cachedData; + } + } catch (cacheError) { + console.log("Cache miss or error, falling back to KV:", cacheError); + } + } + + // Fall back to KV storage + const kvData = await env.CONTENT_KV.get(env.NAVIGATION_KEY, { + type: "json", + }); + return kvData || null; + } catch (error) { + console.log("Error fetching navigation cache:", error); + return null; + } +} + +export async function storeNavigation(env, navigation) { + try { + // Always store in KV first + await env.CONTENT_KV.put(env.NAVIGATION_KEY, JSON.stringify(navigation)); + console.log("Navigation stored in KV."); + + // Also update the Cloudflare Cache API + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.NAVIGATION_CACHE_URL || "cache"}/navigation` + ); + + // Create a response with appropriate cache headers + const response = new Response(JSON.stringify(navigation), { + headers: { + "Content-Type": "application/json", + "Cache-Control": "max-age=60", // 1 minute TTL + ETag: `"navigation-${Date.now()}"`, // Add ETag for cache validation + }, + }); + + await cache.put(cacheKey, response); + console.log("Navigation stored in Cloudflare cache."); + } catch (cacheError) { + console.warn( + "Error updating Cloudflare cache, but KV was updated successfully:", + cacheError + ); + } + } catch (error) { + console.error("Error storing navigation cache:", error); + throw error; + } +} + +// Transform basic Notion page data to desired format +export async function transformNavigation(notionPage, type) { + var icon = undefined; + if (notionPage.icon) { + icon = await processNotionIcon(notionPage.icon, "tb-icon"); + } + const properties = notionPage.properties; + const id = notionPage.id; + const slug = properties.Slug?.formula?.string || "unknown"; + // Extract name from title + const name = properties.Name?.title?.[0]?.plain_text || "Untitled"; + + return { + slug: type == "blog" ? `properties/${slug}` : slug, + name: name, + icon: icon, + notionId: id, + type: type, + }; +} diff --git a/src/utils/notion.js b/src/utils/notion.js new file mode 100644 index 0000000..c499e1c --- /dev/null +++ b/src/utils/notion.js @@ -0,0 +1,627 @@ +import { processNotionIcon } from "./icon"; +import { env } from "cloudflare:workers"; +import { getNavigation } from "./navigation"; + +const NOTION_API_BASE = "https://api.notion.com/v1"; +const NOTION_VERSION = "2025-09-03"; // or your preferred version + +const iconColorSubstitutions = [ + ["#D44C47", "#FF453A"], + ["#55534E", "#FFFFFF"], + ["#448361", "#32D74B"], + ["#337ea9", "#0A84FF"], + ["#9065B0", "#BF5AF2"], + ["#CB912F", "#FFD60A"], + ["#C14C8A", "#FF375F"], + ["#d9730d", "#FF9F0A"], +]; + +async function notionFetch(endpoint, options = {}) { + const res = await fetch(`${NOTION_API_BASE}${endpoint}`, { + headers: { + Authorization: `Bearer ${env.NOTION_AUTH}`, + "Notion-Version": NOTION_VERSION, + "Content-Type": "application/json", + }, + ...options, + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error( + `Notion API error: ${res.status} ${res.statusText} - ${text}` + ); + } + return res.json(); +} + +export async function addToNotionDataSource( + properties, + dataSourceId, + icon = null +) { + console.log("Adding to Notion data source..."); + + const pageParams = {}; + const notionProperties = {}; + + if (icon != null) { + pageParams["icon"] = { + type: "external", + external: { + url: icon, + }, + }; + } + + for (const [key, value] of Object.entries(properties)) { + if (key === "Email" && value != "") { + notionProperties[key] = { email: value }; + } else if (key === "Name" && value != "") { + notionProperties[key] = { title: [{ text: { content: value } }] }; + } else if (key === "Features") { + notionProperties[key] = { + multi_select: value.map((feature) => ({ + name: feature, + })), + }; + } else if (key === "Message" && value != "") { + pageParams["children"] = [ + { + object: "block", + type: "paragraph", + paragraph: { + rich_text: [ + { + type: "text", + text: { + content: value, + }, + }, + ], + }, + }, + ]; + } else if (typeof value === "string" && value != "") { + notionProperties[key] = { + rich_text: [{ text: { content: value } }], + }; + } else if (typeof value === "number" && value != undefined) { + notionProperties[key] = { number: value }; + } else if (value && typeof value === "object" && !Array.isArray(value)) { + notionProperties[key] = value; + } + } + try { + const response = await notionFetch("/pages", { + method: "POST", + body: JSON.stringify({ + parent: { data_source_id: dataSourceId }, + ...pageParams, + properties: notionProperties, + }), + }); + + console.log("Added to Notion DB!"); + return response; + } catch (error) { + console.error("Failed to add to Notion:", error); + throw new Error("Failed to add to Notion"); + } +} + +export async function queryNotionDataSource(dataSourceId, queryParams = {}) { + console.log(`Fetching data source: ${dataSourceId} from Notion API...`); + + try { + const body = JSON.stringify(queryParams); + + const result = await notionFetch(`/data_sources/${dataSourceId}/query`, { + method: "POST", + body, + }); + + console.log("Fetched data source:", dataSourceId); + return result.results; + } catch (error) { + console.error(`Error fetching Notion data source ${dataSourceId}:`, error); + throw error; + } +} + +export async function getNotionDatabase(databaseId) { + console.log(`Fetching database: ${databaseId} from Notion API...`); + + try { + const result = await notionFetch(`/databases/${databaseId}`, { + method: "GET", + }); + return result; + } catch (error) { + console.error(`Error fetching Notion database ${databaseId}:`, error); + throw error; + } +} + +export async function getNotionDataSource(dataSourceId) { + console.log(`Fetching data source: ${dataSourceId} from Notion API...`); + + try { + const result = await notionFetch(`/data_sources/${dataSourceId}`, { + method: "GET", + }); + return result; + } catch (error) { + console.error(`Error fetching Notion data source ${dataSourceId}:`, error); + throw error; + } +} + +export async function getNotionPage(pageId) { + console.log(`Fetching page: ${pageId} from Notion API...`); + try { + const result = await notionFetch(`/pages/${pageId}`, { + method: "GET", + }); + return result; + } catch (error) { + console.error(`Error fetching Notion page ${pageId}:`, error); + throw error; + } +} + +export async function updateNotionPage( + pageId, + properties, + archive = false, + trash = false +) { + console.log(`Updating page: ${pageId} in Notion API...`); + const notionProperties = {}; + + for (const [key, value] of Object.entries(properties)) { + if (key === "Email" && value != "") { + notionProperties[key] = { email: value }; + } else if (key === "Name") { + notionProperties[key] = { title: [{ text: { content: value } }] }; + } else if (key === "Features") { + notionProperties[key] = { + multi_select: value.map((feature) => ({ + name: feature, + })), + }; + } else if (typeof value === "string" && value != "") { + notionProperties[key] = { + rich_text: [{ text: { content: value } }], + }; + } else if (typeof value === "number" && value != undefined) { + notionProperties[key] = { number: value }; + } else if (value && typeof value === "object" && !Array.isArray(value)) { + notionProperties[key] = value; + } + } + + try { + const result = await notionFetch(`/pages/${pageId}`, { + method: "PATCH", + body: JSON.stringify({ + properties: notionProperties, + archived: archive, + in_trash: trash, + }), + }); + return result; + } catch (error) { + console.error(`Error updating Notion page ${pageId}:`, error); + throw error; + } +} + +export async function getNotionBlocks(pageId, queryParams = {}) { + console.log(`Fetching blocks for page: ${pageId} from Notion API...`); + + try { + const result = await notionFetch( + `/blocks/${pageId}/children?${new URLSearchParams(queryParams)}` + ); + return result.results; + } catch (error) { + console.error(`Error fetching Notion page ${pageId}:`, error); + throw error; + } +} + +export async function getNotionSVGIcon(url) { + try { + console.log(`Fetching SVG icon from: ${url}`); + const response = await fetch(url); + if (!response.ok) + throw new Error(`Failed to fetch SVG: ${response.statusText}`); + + let svgText = await response.text(); + iconColorSubstitutions.forEach(([searchColor, replaceColor]) => { + svgText = svgText.replaceAll(searchColor, replaceColor); + }); + + return svgText; + } catch (error) { + console.error(error); + return null; + } +} + +// Transform Notion blocks to content format +export async function transformContent(env, pageId, navigationItems) { + try { + console.log(`Transforming content for page: ${pageId}`); + // Fetch blocks from Notion + const blocks = await getNotionBlocks(pageId); + + const content = []; + + for (let i = 0; i < blocks.length; i++) { + const block = blocks[i]; + const blockType = block.type; + + // Group consecutive list items + if ( + blockType === "bulleted_list_item" || + blockType === "numbered_list_item" + ) { + const isNumbered = blockType === "numbered_list_item"; + const children = []; + // Collect consecutive list items of the same type + while ( + i < blocks.length && + blocks[i].type === + (isNumbered ? "numbered_list_item" : "bulleted_list_item") + ) { + children.push({ + type: "listItem", + text: await extractRichText( + isNumbered + ? blocks[i].numbered_list_item.rich_text + : blocks[i].bulleted_list_item.rich_text, + navigationItems + ), + }); + i++; + } + // Step back one index because the for loop will increment it + i--; + content.push({ + type: "list", + ordered: isNumbered, + children, + }); + continue; + } + + switch (blockType) { + case "heading_1": + content.push({ + type: "title1", + text: await extractRichText( + block.heading_1.rich_text, + navigationItems + ), + }); + break; + + case "heading_2": + content.push({ + type: "title2", + text: await extractRichText( + block.heading_2.rich_text, + navigationItems + ), + }); + break; + + case "heading_3": + content.push({ + type: "title3", + text: await extractRichText( + block.heading_3.rich_text, + navigationItems + ), + }); + break; + + case "paragraph": + content.push({ + type: "paragraph", + text: await extractRichText( + block.paragraph.rich_text, + navigationItems + ), + }); + + break; + + case "divider": + content.push({ + type: "divider", + }); + break; + + case "image": + content.push({ + type: "image", + url: block.image.external?.url || block.image.file?.url, + caption: block.image.caption + ? await extractRichText(block.image.caption, navigationItems) + : null, + }); + break; + + case "quote": + content.push({ + type: "quote", + text: await extractRichText(block.quote.rich_text, navigationItems), + }); + break; + + case "code": + content.push({ + type: "code", + text: await extractRichText(block.code.rich_text, navigationItems), + language: block.code.language, + }); + break; + case "bookmark": + content.push({ + type: "button", + text: await extractRichText( + block.bookmark.caption, + navigationItems + ), + url: block.bookmark.url, + }); + break; + + case "callout": + console.log("Callout:", block); + var calloutChildren = []; + if (block.callout?.rich_text && block.callout.rich_text.length > 0) { + const text = await extractRichText( + block.callout.rich_text, + navigationItems + ); + calloutChildren.push({ + type: "paragraph", + text: text, + }); + } + if (block.has_children == true) { + const children = await transformContent( + env, + block.id, + navigationItems + ); + calloutChildren.push(...children); + } + content.push({ + type: "callout", + children: calloutChildren, + icon: block.callout?.icon + ? await processNotionIcon(block.callout.icon, "tb-callout-icon") + : undefined, + }); + break; + case "video": + content.push({ + type: "video", + url: block.video.file?.url || block.video?.external?.url, + }); + break; + case "file": + content.push({ + type: "file", + url: block.file.file?.url || block.file?.external?.url, + }); + break; + case "link_to_page": + const linkedNavigationItem = getItemByNotionId( + navigationItems, + block.link_to_page.page_id + ); + content.push({ + type: "button", + url: `/${linkedNavigationItem.slug}`, + text: linkedNavigationItem.name, + icon: linkedNavigationItem.icon, + }); + break; + case "column_list": + const childColumns = await getNotionBlocks(block.id); + const children = []; + for (const childColumn of childColumns) { + children.push({ + type: "columnFlexItem", + width: childColumn.column.width_ratio * 100 + "%", + children: await transformContent( + env, + childColumn.id, + navigationItems + ), + }); + } + content.push({ type: "columnFlex", children: children }); + break; + default: + // For unsupported block types, try to extract text if available + console.log(`Unsupported block type: ${blockType}`); + console.log(JSON.stringify(block)); + break; + } + } + + return content; + } catch (error) { + console.error(`Error transforming page content for ${pageId}:`, error); + // Return fallback content if transformation fails + return [ + { + type: "paragraph", + text: "Content could not be loaded at this time.", + }, + ]; + } +} + +function processLink(href, navigationItems) { + var pageIdNoDashes = null; + if (href.startsWith("/")) { + pageIdNoDashes = href.split("/").pop().replaceAll("#", ""); + } + if (href.includes("notion.so/")) { + if (href.includes("-")) { + pageIdNoDashes = href.split("-").pop().replaceAll("#", ""); + } else { + pageIdNoDashes = href.split("/").pop().replaceAll("#", ""); + } + } + + if (pageIdNoDashes != null) { + const linkedNavigationItem = getItemByNotionId( + navigationItems, + pageIdNoDashes + ); + if (!linkedNavigationItem.slug) { + return { + type: "externalLink", + url: href, + }; + } + return { + type: "internalLink", + url: `/${linkedNavigationItem.slug}`, + }; + } else { + return { + type: "externalLink", + url: href, + }; + } +} + +// Helper function to extract rich text content +export async function extractRichText(richTextArray, navigationItems) { + if (!richTextArray || !Array.isArray(richTextArray)) { + return []; + } + + const textPieces = await Promise.all( + richTextArray.map(async (textObj) => { + let text = textObj?.plain_text || ""; + let annotations = textObj?.annotations || {}; + let link = undefined; + let emoji = undefined; + // Handle custom emoji mentions + if ( + textObj.type === "mention" && + textObj.mention?.type === "custom_emoji" && + textObj.mention.custom_emoji?.url + ) { + emoji = await processNotionIcon( + textObj.mention.custom_emoji, + "tb-inline-emoji" + ); + } + + // Handle links + if (textObj.href) { + link = processLink(textObj.href, navigationItems); + } + + return { + text: text, + bold: annotations?.bold || false, + italic: annotations?.italic || false, + underline: annotations?.underline || false, + strikethrough: annotations?.strikethrough || false, + code: annotations?.code || false, + color: annotations?.color || undefined, + link: link, + }; + }) + ); + + return textPieces; +} + +// Utility: safely extract plain text from a Notion title or rich_text field +export function getPlainText(field) { + if (!field) return ""; + if (field.type === "title" || field.type === "rich_text") { + return field[field.type].map((t) => t.plain_text).join("") || ""; + } + return ""; +} + +// Utility: convert a string to camelCase +export function toCamelCase(str = "") { + return str + .replace(/[^a-zA-Z0-9 ]/g, " ") // remove non-alphanumeric except spaces + .trim() + .split(/\s+/) // split on spaces + .map((word, index) => { + if (index === 0) return word.toLowerCase(); + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + }) + .join(""); +} + +// Moved from Blogs.js: buildNewCache, now buildListCache +export function buildListCache( + cachedList, + { added = [], updated = [], deleted = [] } +) { + // Remove deleted and items that will be updated + const filtered = cachedList.filter( + (p) => + !deleted.some((d) => d.notionId === p.notionId) && + !updated.some((u) => u.notionId === p.notionId) + ); + + return [...filtered, ...updated, ...added]; +} + +export function getItemByNotionId(items, notionId) { + const itemWithDashes = items.find((item) => item.notionId === notionId); + if (itemWithDashes) { + return itemWithDashes; + } + const itemWithoutDashes = items.find( + (item) => item.notionId.replaceAll("-", "") === notionId.replaceAll("-", "") + ); + if (itemWithoutDashes) { + return itemWithoutDashes; + } + return null; +} + +// Sort function for objects with duration properties +// Sorts by end date (newest first), with ongoing positions (no end date) first +export function sortDuration(a, b) { + const aEnd = a.duration?.end; + const bEnd = b.duration?.end; + const aStart = a.duration?.start || ""; + const bStart = b.duration?.start || ""; + + // Positions without end date (ongoing) should be first (newest) + if (!aEnd && !bEnd) { + // Both ongoing - sort by start date, newest first + + return bStart.localeCompare(aStart); + } + if (!aEnd) return -1; // a is ongoing, should be first + if (!bEnd) return 1; // b is ongoing, should be first + + // Both have start dates - sort by start date, newest first + const startComparison = bStart.localeCompare(aStart); + if (startComparison !== 0) return startComparison; + + return bEnd.localeCompare(aEnd); +} diff --git a/src/utils/pages.js b/src/utils/pages.js new file mode 100644 index 0000000..c550ece --- /dev/null +++ b/src/utils/pages.js @@ -0,0 +1,214 @@ +import { + transformContent, + toCamelCase, + getItemByNotionId, +} from "../utils/notion.js"; +import { getNavigation } from "../utils/navigation.js"; +// Transform Notion page data to desired format + +export async function getPages(env, cached = false) { + try { + // If cached=true, try to get from Cloudflare Cache API first + if (cached) { + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/pages` + ); + const cachedResponse = await cache.match(cacheKey); + + if (cachedResponse) { + const cachedData = await cachedResponse.json(); + console.log("Pages retrieved from Cloudflare cache"); + return cachedData; + } + } catch (cacheError) { + console.log("Cache miss or error, falling back to KV:", cacheError); + } + } + + // Fall back to KV storage + const kvData = await env.CONTENT_KV.get(env.PAGES_KEY, { + type: "json", + }); + + // If we were trying to use cache but it failed, store the KV data in cache + if (cached && kvData) { + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/pages` + ); + + const response = new Response(JSON.stringify(kvData), { + headers: { + "Content-Type": "application/json", + "Cache-Control": "max-age=60", // 1 minute TTL + ETag: `"pages-${Date.now()}"`, // Add ETag for cache validation + }, + }); + + await cache.put(cacheKey, response); + console.log("Pages stored in Cloudflare cache after KV fallback"); + } catch (cacheError) { + console.warn("Error storing in cache after KV fallback:", cacheError); + } + } + + return kvData || []; + } catch (error) { + console.log("Error fetching pages cache:", error); + return null; + } +} + +export async function storePages(env, pages) { + try { + // Always store in KV first + await env.CONTENT_KV.put(env.PAGES_KEY, JSON.stringify(pages)); + console.log("Pages stored in KV."); + + // Purge the Cloudflare Cache API + try { + const cache = caches.default; + const cacheKey = new Request(`https://${env.CACHE_URL || "cache"}/pages`); + + await cache.delete(cacheKey); + console.log("Pages cache purged successfully."); + } catch (cacheError) { + console.warn( + "Error purging Cloudflare cache, but KV was updated successfully:", + cacheError + ); + } + } catch (error) { + console.error("Error storing pages cache:", error); + throw error; + } +} + +export async function transformPageData(env, notionPage, settingsData) { + const navigationItems = await getNavigation(env); + + const icon = getItemByNotionId(navigationItems, notionPage.id).icon; + + const properties = notionPage.properties; + + // Extract theme information + let theme = undefined; // default + if (properties.Theme?.relation?.[0]?.id) { + const themeId = properties.Theme.relation[0].id; + console.log(settingsData.themes); + const themeData = getItemByNotionId(settingsData.themes, themeId); + if (themeData?.name) { + theme = themeData.name; + } + } + + const published = properties["Published"]?.checkbox || false; + // Check if page is published - if not, return null to filter it out + + const pageType = toCamelCase(properties["Page Type"].select.name); + + const align = toCamelCase(properties["Align"].select?.name || "center"); + + const justify = toCamelCase(properties["Justify"].select?.name || "middle"); + // Extract slug from formula); + const slug = properties.Slug?.formula?.string || "unknown"; + + // Extract name from title + const name = properties.Name?.title?.[0]?.plain_text || "Untitled"; + + // Fetch and transform the actual page content from Notion blocks + + const content = []; + + const showPageIcon = properties["Show Page Icon"]?.checkbox || false; + + if (showPageIcon == true) { + content.push({ + type: "pageIcon", + icon: icon, + }); + } + + const pageContent = await transformContent( + env, + notionPage.id, + navigationItems + ); + content.push(...pageContent); + + const showBlogs = properties["Show Blogs"]?.checkbox || false; + const showProjects = properties["Show Projects"]?.checkbox || false; + const showExperience = properties["Show Experience"]?.checkbox || false; + + const showContactForm = properties["Show Contact Form"]?.checkbox || false; + const gradientBackground = + properties["Gradient Background"]?.checkbox || false; + + const showScroll = properties["Show Scroll Icon"]?.checkbox || false; + + const verticalScroll = properties["Vertical Scroll"]?.checkbox || false; + const horizontalScroll = properties["Horizontal Scroll"]?.checkbox || false; + + const scrollSnap = properties["Scroll Snap"]?.checkbox || false; + + const paragraphWidth = properties["Paragraph Width"]?.number || 400; + const order = properties["Order"]?.number || undefined; + + const spacing = properties["Spacing"]?.number || 5; + + const showScrollButtons = + properties["Show Scroll Buttons"]?.checkbox || false; + const scrollButtonDistance = + properties["Scroll Button Distance"]?.number || 10; + + if (showBlogs == true) { + content.push({ + type: "blogs", + }); + } + if (showProjects == true) { + content.push({ + type: "projects", + }); + } + if (showExperience == true) { + content.push({ + type: "companies", + }); + } + if (showContactForm == true) { + content.push({ + type: "contactForm", + }); + } + + return { + notionId: notionPage.id, + published: published, + icon: icon, + slug: slug, + name: name, + pageType: pageType, + content: content, + theme: theme, + align: align, + justify: justify, + showScroll: showScroll, + showBlogs: showBlogs, + showProjects: showProjects, + showExperience: showExperience, + showContactForm: showContactForm, + gradientBackground: gradientBackground, + paragraphWidth: paragraphWidth, + spacing: spacing, + order: order, + verticalScroll: verticalScroll, + horizontalScroll: horizontalScroll, + scrollSnap: scrollSnap, + showScrollButtons: showScrollButtons, + scrollButtonDistance: scrollButtonDistance, + }; +} diff --git a/src/utils/positions.js b/src/utils/positions.js new file mode 100644 index 0000000..20ef4d0 --- /dev/null +++ b/src/utils/positions.js @@ -0,0 +1,164 @@ +import { getItemByNotionId, getPlainText, transformContent } from "./notion.js"; +import _ from "lodash"; +import diff from "microdiff"; +import { getNavigation } from "../utils/navigation.js"; + +export async function getPositions(env, cached = false) { + try { + // If cached=true, try to get from Cloudflare Cache API first + if (cached) { + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/Positions` + ); + const cachedResponse = await cache.match(cacheKey); + + if (cachedResponse) { + const cachedData = await cachedResponse.json(); + console.log("Positions retrieved from Cloudflare cache"); + return cachedData; + } + } catch (cacheError) { + console.log("Cache miss or error, falling back to KV:", cacheError); + } + } + + // Fall back to KV storage + const kvData = await env.CONTENT_KV.get(env.POSITIONS_KEY, { + type: "json", + }); + + // If we were trying to use cache but it failed, store the KV data in cache + if (cached && kvData) { + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/Positions` + ); + + const response = new Response(JSON.stringify(kvData), { + headers: { + "Content-Type": "application/json", + "Cache-Control": "max-age=60", // 1 minute TTL + ETag: `"Positions-${Date.now()}"`, // Add ETag for cache validation + }, + }); + + await cache.put(cacheKey, response); + console.log("Positions stored in Cloudflare cache after KV fallback"); + } catch (cacheError) { + console.warn("Error storing in cache after KV fallback:", cacheError); + } + } + + return kvData || []; + } catch (error) { + console.log("Error fetching Positions cache:", error); + return null; + } +} + +export async function storePositions(env, Positions) { + try { + // Always store in KV first + await env.CONTENT_KV.put(env.POSITIONS_KEY, JSON.stringify(Positions)); + console.log("Positions stored in KV."); + + // Purge the Cloudflare Cache API + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/Positions` + ); + + await cache.delete(cacheKey); + console.log("Positions cache purged successfully."); + } catch (cacheError) { + console.warn( + "Error purging Cloudflare cache, but KV was updated successfully:", + cacheError + ); + } + } catch (error) { + console.error("Error storing Positions cache:", error); + throw error; + } +} + +export function diffPositions(newList, oldList) { + // Keys to check for changes + const keysToCheck = ["name", "duration", "company", "content"]; + + // Helper: index by notionId using lodash + const oldById = _.keyBy(oldList || [], "notionId"); + const newById = _.keyBy(newList || [], "notionId"); + + // toAdd: in newList but not in oldList + const toAdd = newList.filter((p) => !oldById[p.notionId]); + + // toDelete: in oldList but not in newList + const toDelete = oldList.filter((p) => !newById[p.notionId]); + + // toUpdate: in both, but with different content (using microdiff) + const toUpdate = newList.filter((p) => { + const old = oldById[p.notionId]; + if (!old) return false; + + // Prepare objects for comparison + const { notionId, ...restNew } = p; + const { notionId: _, ...restOld } = old; + + // Use microdiff to get differences + const differences = diff(restOld, restNew); + + // Filter differences to only include keys we care about + const relevantDifferences = differences.filter((change) => { + // Check if the changed path starts with any of our keysToCheck + const path = change.path.join("."); + return keysToCheck.some((key) => path.startsWith(key)); + }); + + return relevantDifferences.length > 0; + }); + + return { toAdd, toUpdate, toDelete }; +} + +// Transform Notion position data to desired format +export async function transformNotionPosition(env, notionPosition) { + const navigationItems = await getNavigation(env); + + const properties = notionPosition.properties; + + console.log("Notion Position:", notionPosition); + + // Extract company information + let company = undefined; + if (properties.Company?.relation?.[0]?.id) { + company = properties.Company.relation[0].id; + } + + // Extract duration + const duration = properties["Duration"].date; + + // Extract name from title + const name = properties.Name?.title?.[0]?.plain_text || "Untitled"; + + console.log("Company:", company); + + // Fetch and transform the actual page content from Notion blocks + const content = await transformContent( + env, + notionPosition.id, + navigationItems + ); + + return { + notionId: notionPosition.id, + name: name, + duration: duration, + company: company, + content, + }; +} diff --git a/src/utils/projects.js b/src/utils/projects.js new file mode 100644 index 0000000..8803345 --- /dev/null +++ b/src/utils/projects.js @@ -0,0 +1,225 @@ +import { + getItemByNotionId, + getPlainText, + transformContent, + toCamelCase, +} from "./notion.js"; +import _ from "lodash"; +import diff from "microdiff"; +import { getNavigation } from "../utils/navigation.js"; +import dayjs from "dayjs"; + +export async function getProjects(env, cached = false) { + try { + // If cached=true, try to get from Cloudflare Cache API first + if (cached) { + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/Projects` + ); + const cachedResponse = await cache.match(cacheKey); + + if (cachedResponse) { + const cachedData = await cachedResponse.json(); + console.log("Projects retrieved from Cloudflare cache"); + return cachedData; + } + } catch (cacheError) { + console.log("Cache miss or error, falling back to KV:", cacheError); + } + } + + // Fall back to KV storage + const kvData = await env.CONTENT_KV.get(env.PROJECTS_KEY, { + type: "json", + }); + + // If we were trying to use cache but it failed, store the KV data in cache + if (cached && kvData) { + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/Projects` + ); + + const response = new Response(JSON.stringify(kvData), { + headers: { + "Content-Type": "application/json", + "Cache-Control": "max-age=60", // 1 minute TTL + ETag: `"Projects-${Date.now()}"`, // Add ETag for cache validation + }, + }); + + await cache.put(cacheKey, response); + console.log("Projects stored in Cloudflare cache after KV fallback"); + } catch (cacheError) { + console.warn("Error storing in cache after KV fallback:", cacheError); + } + } + + return kvData || []; + } catch (error) { + console.log("Error fetching Projects cache:", error); + return null; + } +} + +export async function storeProjects(env, Projects) { + try { + // Always store in KV first + await env.CONTENT_KV.put(env.PROJECTS_KEY, JSON.stringify(Projects)); + console.log("Projects stored in KV."); + + // Purge the Cloudflare Cache API + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/Projects` + ); + + await cache.delete(cacheKey); + console.log("Projects cache purged successfully."); + } catch (cacheError) { + console.warn( + "Error purging Cloudflare cache, but KV was updated successfully:", + cacheError + ); + } + } catch (error) { + console.error("Error storing Projects cache:", error); + throw error; + } +} + +export function diffProjects(newList, oldList) { + // Keys to check for changes + const keysToCheck = [ + "name", + "date", + "image", + "content", + "published", + "externalLink", + "type", + "tools", + "client", + ]; + + // Helper: index by notionId using lodash + const oldById = _.keyBy(oldList || [], "notionId"); + const newById = _.keyBy(newList || [], "notionId"); + + // toAdd: in newList but not in oldList + const toAdd = newList.filter((p) => !oldById[p.notionId]); + + // toDelete: in oldList but not in newList + const toDelete = oldList.filter((p) => !newById[p.notionId]); + + // toUpdate: in both, but with different content (using microdiff) + const toUpdate = newList.filter((p) => { + const old = oldById[p.notionId]; + if (!old) return false; + + // Prepare objects for comparison + const { notionId, ...restNew } = p; + const { notionId: _, ...restOld } = old; + + // Use microdiff to get differences + const differences = diff(restOld, restNew); + + // Filter differences to only include keys we care about + const relevantDifferences = differences.filter((change) => { + // Check if the changed path starts with any of our keysToCheck + const path = change.path.join("."); + return keysToCheck.some((key) => path.startsWith(key)); + }); + + return relevantDifferences.length > 0; + }); + + return { toAdd, toUpdate, toDelete }; +} + +// Transform Notion project data to desired format +export async function transformNotionProject(env, notionProject, settingsData) { + const navigationItems = await getNavigation(env); + + const properties = notionProject.properties; + + console.log("Notion Project:", notionProject); + + // Extract theme information + let theme = undefined; // default + if (properties.Theme?.relation?.[0]?.id) { + const themeId = properties.Theme.relation[0].id; + console.log(settingsData.themes); + const themeData = getItemByNotionId(settingsData.themes, themeId); + if (themeData?.name) { + theme = themeData.name; + } + } + + const published = properties["Published"]?.checkbox || false; + + const slug = properties["Slug"]?.formula?.string || "unknown"; + + const status = toCamelCase(properties["Status"]?.status?.name || null); + + const dateRaw = properties["Date"]?.date; + const date = dateRaw?.start ? dayjs(dateRaw.start).format("DD/MM/YY") : null; + + // Extract image URL (handle both external and file images) + const imageFile = notionProject?.cover; + const image = imageFile?.external?.url || imageFile?.file?.url || null; + + const subTitle = getPlainText(properties["Subtitle"]); + + // Extract name from title + const name = properties.Name?.title?.[0]?.plain_text || "Untitled"; + + // Extract external link + const externalLink = properties["External Link"]?.url || null; + + // Extract type (select) + const type = properties["Type"]?.select?.name || null; + + // Extract tools (multi-select) + const tools = (properties["Tools"]?.multi_select || []).map( + (item) => item.name + ); + + // Extract client (text) + const client = getPlainText(properties["Client"]); + + // Fetch and transform the actual page content from Notion blocks + + const content = [{ type: "image", url: image, caption: subTitle }]; + const projectContent = await transformContent( + env, + notionProject.id, + navigationItems + ); + content.push(...projectContent); + + if (published == false) { + return null; + } + + return { + notionId: notionProject.id, + name: name, + date: date, + image: image, + subTitle: subTitle, + content, + published, + externalLink: externalLink, + type: type, + tools: tools, + client: client, + slug: slug, + status: status, + theme: theme, + }; +} diff --git a/src/utils/settings.js b/src/utils/settings.js new file mode 100644 index 0000000..11e0490 --- /dev/null +++ b/src/utils/settings.js @@ -0,0 +1,177 @@ +import { processNotionIcon } from "./icon"; +import { getNavigation } from "./navigation"; +import { toCamelCase, getPlainText, getItemByNotionId } from "./notion"; + +export async function getSettings(env, cached = false) { + try { + // If cached=true, try to get from Cloudflare Cache API first + if (cached) { + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/settings` + ); + const cachedResponse = await cache.match(cacheKey); + + if (cachedResponse) { + const cachedData = await cachedResponse.json(); + console.log("Settings retrieved from Cloudflare cache"); + return cachedData; + } + } catch (cacheError) { + console.log("Cache miss or error, falling back to KV:", cacheError); + } + } + + // Fall back to KV storage + const kvData = await env.CONTENT_KV.get(env.SETTINGS_KEY, { + type: "json", + }); + + // If we were trying to use cache but it failed, store the KV data in cache + if (cached && kvData) { + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/settings` + ); + + const response = new Response(JSON.stringify(kvData), { + headers: { + "Content-Type": "application/json", + "Cache-Control": "max-age=60", // 1 minute TTL + ETag: `"settings-${Date.now()}"`, // Add ETag for cache validation + }, + }); + + await cache.put(cacheKey, response); + console.log("Settings stored in Cloudflare cache after KV fallback"); + } catch (cacheError) { + console.warn("Error storing in cache after KV fallback:", cacheError); + } + } + + return kvData || {}; + } catch (error) { + console.log("Error fetching settings cache:", error); + return null; + } +} + +export async function storeSettings(env, settings) { + try { + // Always store in KV first + await env.CONTENT_KV.put(env.SETTINGS_KEY, JSON.stringify(settings)); + console.log("Settings stored in KV."); + + // Purge the Cloudflare Cache API + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/settings` + ); + + await cache.delete(cacheKey); + console.log("Settings cache purged successfully."); + } catch (cacheError) { + console.warn( + "Error purging Cloudflare cache, but KV was updated successfully:", + cacheError + ); + } + } catch (error) { + console.error("Error storing settings cache:", error); + throw error; + } +} + +export async function transformRedirectsData(env, redirectsData = []) { + const redirects = {}; + + const navigationItems = await getNavigation(env); + + // Iterate over redirects and build object + for (const redirect of redirectsData) { + const redirectName = getPlainText(redirect.properties?.Name); + const relationIds = + redirect.properties?.Page?.relation?.map((r) => r.id) || []; + // Map relations to full basicData entries + const relatedItem = relationIds + .map((notionId) => getItemByNotionId(navigationItems, notionId)) + .filter(Boolean)[0]; + + const key = toCamelCase(redirectName); + redirects[key] = relatedItem.slug; + } + + return redirects; +} + +// Transform Notion themes data to desired format +export async function transformThemes(themesData) { + if (!themesData || !Array.isArray(themesData)) { + return []; + } + + const transformed = await Promise.all( + themesData.map(async (theme) => { + const properties = theme.properties; + // Extract theme name + const name = properties.Name?.title?.[0]?.plain_text || "unknown"; + + // Extract background color + const backgroundColor = + properties["Background Color"]?.rich_text?.[0]?.plain_text || "#ffffff"; + + // Extract text color + const textColor = + properties["Text Color"]?.rich_text?.[0]?.plain_text || "#000000"; + + return { + notionId: theme.id, + name: toCamelCase(name), + backgroundColor, + textColor, + }; + }) + ); + + return transformed; +} + +// Transform Notion page data to desired format +export async function transformSettingsData( + env, + globalThemesData = [], + redirectsData = [], + themesData = [] +) { + const redirects = await transformRedirectsData(env, redirectsData); + const themes = await transformThemes(themesData); + const globalThemes = transformGlobalThemesData(env, globalThemesData, themes); + + return { redirects, themes, globalThemes }; +} + +export function transformGlobalThemesData( + env, + globalThemesData = [], + themes = [] +) { + const globalThemes = {}; + + // Iterate over redirects and build object + for (const globalTheme of globalThemesData) { + const globalThemeName = getPlainText(globalTheme.properties?.Name); + const relationIds = + globalTheme.properties?.Theme?.relation?.map((r) => r.id) || []; + // Map relations to full basicData entries + const relatedTheme = relationIds + .map((notionId) => getItemByNotionId(themes, notionId)) + .filter(Boolean)[0]; + const key = toCamelCase(globalThemeName); + globalThemes[key] = relatedTheme; + } + + return globalThemes; +} diff --git a/src/utils/videoCache.js b/src/utils/videoCache.js new file mode 100644 index 0000000..e507440 --- /dev/null +++ b/src/utils/videoCache.js @@ -0,0 +1,340 @@ +function generateVideoId(url) { + const urlObj = new URL(url); + const origin = urlObj.origin; + const pathname = urlObj.pathname; + + const combined = `${origin}${pathname}`; + + let hash = 0; + for (let i = 0; i < combined.length; i++) { + const char = combined.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; + } + + const positiveHash = Math.abs(hash); + const base36Hash = positiveHash.toString(36); + + return base36Hash.padStart(8, "0").substring(0, 12); +} + +function generateR2KeyFromVideoId(videoId, url) { + const urlObj = new URL(url); + const pathname = urlObj.pathname; + const filename = pathname.split("/").pop() || "video"; + const extension = filename.includes(".") ? filename.split(".").pop() : "mp4"; + const baseFilename = filename.replace(/\.[^/.]+$/, ""); + + return `videos/${videoId}-${baseFilename}.${extension}`; +} + +function extractR2KeyFromUrl(mirrorUrl) { + const urlObj = new URL(mirrorUrl); + return urlObj.pathname.substring(1); +} + +export async function getVideos(env, cached = false) { + try { + if (cached) { + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/videos` + ); + const cachedResponse = await cache.match(cacheKey); + + if (cachedResponse) { + const cachedData = await cachedResponse.json(); + console.log("Videos retrieved from Cloudflare cache"); + return cachedData; + } + } catch (cacheError) { + console.log( + "Video cache miss or error, falling back to KV:", + cacheError + ); + } + } + + const kvData = await env.CONTENT_KV.get(env.VIDEOS_KEY, { + type: "json", + }); + + if (cached && kvData) { + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/videos` + ); + + const response = new Response(JSON.stringify(kvData), { + headers: { + "Content-Type": "application/json", + "Cache-Control": "max-age=60", + ETag: `"videos-${Date.now()}"`, + }, + }); + + await cache.put(cacheKey, response); + console.log("Videos stored in Cloudflare cache after KV fallback"); + } catch (cacheError) { + console.warn( + "Error storing videos in cache after KV fallback:", + cacheError + ); + } + } + + return kvData || []; + } catch (error) { + console.log("Error fetching videos cache:", error); + return null; + } +} + +export async function updateVideos(env, currentVideoUrls) { + console.log("Updating video cache..."); + + const currentVideos = currentVideoUrls.map((url) => ({ + id: generateVideoId(url), + url, + })); + + const existingVideos = await getVideos(env); + const existingVideoMap = new Map( + existingVideos.map((video) => [video.id, video]) + ); + + const videosToAdd = currentVideos.filter( + (video) => !existingVideoMap.has(video.id) + ); + + const currentVideoMap = new Map( + currentVideos.map((video) => [video.id, video]) + ); + const videosToKeep = existingVideos + .filter((video) => currentVideoMap.has(video.id)) + .map((video) => ({ + ...video, + url: currentVideoMap.get(video.id).url, + })); + + const videosToRemove = existingVideos.filter( + (video) => !currentVideoMap.has(video.id) + ); + + console.log( + `Videos to add: ${videosToAdd.length}, Videos to keep: ${videosToKeep.length}, Videos to remove: ${videosToRemove.length}` + ); + + for (const video of videosToRemove) { + if (video.mirrorUrl) { + try { + const key = extractR2KeyFromUrl(video.mirrorUrl); + await env.TB_STORAGE.delete(key); + console.log(`Deleted ${key} from R2 for video ID ${video.id}`); + } catch (error) { + console.error( + `Failed to delete ${video.mirrorUrl} from R2 for video ID ${video.id}:`, + error + ); + } + } + } + + const newVideos = []; + for (let i = 0; i < videosToAdd.length; i++) { + const videoData = videosToAdd[i]; + const { id, url } = videoData; + + console.log( + `Uploading video to R2 (${i + 1}/${ + videosToAdd.length + }): ${url} (ID: ${id})` + ); + + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch video: ${response.status}`); + } + + const contentType = response.headers.get("content-type") || null; + const contentLengthHeader = response.headers.get("content-length"); + const contentLength = contentLengthHeader + ? Number.parseInt(contentLengthHeader, 10) + : null; + + const key = generateR2KeyFromVideoId(id, url); + + const putOptions = contentType + ? { httpMetadata: { contentType } } + : undefined; + const result = await env.TB_STORAGE.put(key, response.body, putOptions); + + if (result == null) { + throw new Error(`Failed to upload video to R2: ${key}`); + } + + const mirrorUrl = `${env.R2_PUBLIC_URL}/${key}`; + + newVideos.push({ + id, + url, + metadata: + contentType || contentLength ? { contentType, contentLength } : null, + mirrorUrl, + }); + + console.log( + `Successfully uploaded and mirrored video: ${url} -> ${mirrorUrl} (ID: ${id})` + ); + } catch (error) { + console.error(`Failed to upload ${url} to R2 (ID: ${id}):`, error); + newVideos.push({ + id, + url, + metadata: null, + mirrorUrl: null, + }); + } + + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + const updatedVideos = [...videosToKeep, ...newVideos]; + + await storeVideos(env, updatedVideos); + + return updatedVideos; +} + +export async function storeVideos(env, videos) { + try { + await env.CONTENT_KV.put(env.VIDEOS_KEY, JSON.stringify(videos)); + console.log("Videos stored in KV."); + + try { + const cache = caches.default; + const cacheKey = new Request( + `https://${env.CACHE_URL || "cache"}/videos` + ); + + await cache.delete(cacheKey); + console.log("Videos cache purged successfully."); + } catch (cacheError) { + console.warn( + "Error purging Cloudflare video cache, but KV was updated successfully:", + cacheError + ); + } + } catch (error) { + console.error("Error storing videos cache:", error); + throw error; + } +} + +function findVideoChildren(obj, videoUrls = []) { + if (!obj || typeof obj !== "object") { + return videoUrls; + } + + if (obj.type === "video") { + if (obj.url) { + videoUrls.push(obj.url); + } else if (obj.src) { + videoUrls.push(obj.src); + } else if (obj.video) { + videoUrls.push(obj.video); + } + } + + if (obj.children && Array.isArray(obj.children)) { + for (const child of obj.children) { + findVideoChildren(child, videoUrls); + } + } + + for (const [key, value] of Object.entries(obj)) { + if ( + key !== "children" && + key !== "content" && + key !== "blocks" && + typeof value === "object" + ) { + findVideoChildren(value, videoUrls); + } + } + + return videoUrls; +} + +export function collectVideoUrls(contentObjects) { + const videoUrls = []; + + for (const object of contentObjects) { + if (object.video) { + videoUrls.push(object.video); + } + if (object.videos) { + object.videos.forEach((url) => videoUrls.push(url)); + } + if (object.content) { + findVideoChildren(object.content, videoUrls); + } + } + + return videoUrls; +} + +export async function handleVideoMetadataUpdate(request, env) { + const cachedVideos = await env.CONTENT_KV.get(env.VIDEOS_KEY, { + type: "json", + }); + if (!cachedVideos) return; + + let updated = false; + + for (const video of cachedVideos) { + if (!video.metadata) { + const targetUrl = video.mirrorUrl || video.url; + try { + const response = await fetch(targetUrl, { method: "HEAD" }); + if (response.ok) { + const contentType = response.headers.get("content-type") || null; + const contentLengthHeader = response.headers.get("content-length"); + const contentLength = contentLengthHeader + ? Number.parseInt(contentLengthHeader, 10) + : null; + + if (contentType || contentLength) { + video.metadata = { contentType, contentLength }; + updated = true; + console.log( + `Updated metadata for video ID ${video.id} (${video.url})` + ); + } + } + } catch (error) { + console.warn( + `Failed to update metadata for video ID ${video.id} (${video.url}):`, + error + ); + } + } + } + + if (updated) { + await env.CONTENT_KV.put(env.VIDEOS_KEY, JSON.stringify(cachedVideos)); + } + + return cachedVideos; +} + +export function getVideoById(cachedVideos, videoId) { + return cachedVideos.find((video) => video.id === videoId); +} + +export function getVideoIdFromUrl(url) { + return generateVideoId(url); +} diff --git a/tb-api.paw b/tb-api.paw new file mode 100644 index 0000000000000000000000000000000000000000..1d06168c9fc7051bc87f40f3c3b962f2535f1067 GIT binary patch literal 18134 zcmd6P349af*8V#)Y1*XNEI>nmG)-wLZ8Od63knEr*(pmYYoW{*sI*P#!lG~;5qCut z5#*`}hzO|YRj>O+-1h}=-$h)({i@gXJCn4uDfQC-|L6VgM}Ez;VKSNbJ?A{#*3#@n*m>92xLp;MqbJal z=o$1ZdJa90UO+FRm(eTe9rP}G4}FL}MxUc^(Rb(v^b7hGBQP80#Rg&lEEfx7#aI*@ zk4?ZPVUw{b*i>u=R)fvQbZjBE8ao%e0J{*o2)hj1fc+KQjP1Z~!ggb~VSmT&!(PE& z#ooZ)#NNW*#y-FfVV`41u%p=5*gvsfu-|cnr{S4+FWiksa1y6*8fS13ABvB_=i>8l z1y^wm*YR3BjyK{h_*uA#pM$T)FT^jxufW&gSK?RW*W%mp8}Xg^t@xezJ@`TVW&Ab# zb^HzdUHmZqDUn9l2?yaMGKpSYp+f-(HzE-C+H}7%xLmuQsy-^?37iA+K>WBQOKN^4r zq5#T4L6nO^C=U%n`6!GE(8*}9iJOFJGt*4FnQms74%2C7n!U`E%q-Jox=oMiHG7+V z%)Vx}=`;J8e(NKOP%(-i5|yARq7aQ3#3ByyNI)Wzkc@_)Q_!hsC>n-_gMExJ`Nf3(UY4RBPs#xp3_!V=-K?WmLRrjG^dY z;QJE-t4HxIs6T9Ja6ej%nou)pnVpnX1ANBJy2d2?6p2hm)JEI&g;|)q>nqXv=@pd~V7pt< zm8doO;s&%aA<7PUO;j2!?c9bopsP_U=woFI_?@;_Q`Pz!bS*&rb)c7|NtuyZ6DCeg z>Rh{8Zb92DwcKi!Y(Y1eQ8RaXqtawFv?O)nCQB3hfT4pXT2OK`itlmz(Ji1GUUZwK zA?OZx+?9B~2PGe?(1Y;$UU<#=|HQ+jL3ap~N$3!3vL**VtjV`M2Acx>j|i_=I+lC{ zXOzYpS`v4XREDP$DjAe>I=rnm zR%Xaj%*P;NtqEvQVM{)wg90q|IDs(l2-x1*x~fL~fFYmNZu z8<|kW;G8mJiBT7Cw05%{nZ8dbBc(Bt5)(O+l^NN>K9-cY7)NqUOyy~g1{IZ3ci>;> zCkVTKh8-w3$CzdFj@jerrg&>(`xg9$AbtT&49!1}=YR${(3!yF51 z9Au7bH@d0`aZRafTO}4utdf>#mXcx&8RJ=<a;z@62&`&1e&E#Qlqb1UMOkIWqt zk1uN9fQrNhC=x5mjHr-|ECc4SF`85vN+uaj=lGb!iyT7%AWZH61b9`~WmScZqF~Da zX#fjU$+9$A1iVpso&9qF!O$d86v6$14+NXm8J(1Q1we=bZ^m&FLrWZd(&^3d2C(Kc ztMe>;sm>cxo!6)=FiIMm;*CaAOD%*tt&O@B_f+Ras`KDC1x|v~linctyY*^KbG)@l zGiqw#M7Rhe$-2%6Iw?@9KuR<$RF26aNl}!lPyo*WfYo^`SFSt}u8gakRFMiKb63{5 z=X7i-D{_J^@H8n>qDXR54E(dggRKdIqzM4wu+=@<(!9x&DlHfXiJjbWd5LCt!_XK~ z5NL4!GHZ~sW;qu@G&n9M@SN7O%byU!6^-Tv*d$UCB#@IvLCC?zRFYSXm`uqKGSM+u z$bT8a8xqD~*+N$o$EIObkTzP@U1iSdubNW`S8k=j*FlU;%b6cD0 zCM!)4y0)~3xR}z#<_!zH^B`+zL#o;&2AmGoorl#YY#ag&tm*hb16z?2Xv_xd?rqLV z*tiv9RKri#Kw~bvl7a!*MNyi{EoysIY6}g$wa(DnQQ-Up3TT3?%Zee9DyV}MMo^?` z@PPQb0M^bMf+27zafhe_K(Z`E1rlfBrlG)F0XJi^3h!2Fiqn;zX>2m?0EHdgp|G+9 z1{M=3(okqC=HS8bWq8uqm=y`}5Dcq5y7~zfmenA_;$)dLcuFBT&R|JZRCJPNAb?_H zl&C9Kbn{0T+n{YcXu(X`;%t$ z0Nh|7Y%lf@94#|?v>P*eKE;e`VIgVe>E${p!-_m$}qtXD*q&r{w zdSV9z(0;HK2G%V>$ix~d#Nm);X`HM;z@+dLBc=LM0fK0Vc@+|JNk}kh1wff%43ej1 z2)nwcnN^K8IL**VUWy)R`oo&|r*k|Gb1 z(rqequNAv7r*MBj$Ukh)^PLO5`vatzE1mLq;?0qorMzht7*SPzq(~V4s@JW(#Bq z?dj2+PFS3bvA?i@Aq;$6GTXtvvHtb}ch~m!_b7e?_5=1~0?C$`OOrU(4qm?{Ojo3N z2wi#58a-xt98efpQ5cer$+E#vtN~uF#=WGIi((Ci2CXG&o&|f-!Kff8rAS6KAfr%p zmSK8G4J~YGX>1-+9j%TkjkP5$3yp=4q{drY>RR*?$WXfiK91uAFxnPsE;m;kLrvU{ zX8>yA>E;<*aEE!OnLA@)C*BP|$pU;2kef-s$2|%3#J#X7EAYP7)>tVB-rssoO+@f~ zc%>r|Sqa;hXZ|IziB<}77W{NgL`-<4QxH?amrYT^95|jrXP3*hMi7}0q6;fa= z0F=RV$Q&>ryb56?Ma48xQd0sC4mOw4R1NZcD`BHF1>OKLI#35OhE+tCrhB;WZGne- zb%#(H9asX3b^)#fo*=9tB?+QsEoJ}*p=yjO_sqESI)qvmUr^gn(p(>ptF4wi6JoB8 zHW|%E(-OtX%9@W8F^BU|vam$F%3R$cVhJD8?ile?%yZftBR(wEG2$bWqAdlX`S4Nr zXgGpn#K#}+81bqU$9OIXEyFy|JRgphW4xdn$9Q2^$LJXeE9}lG;`0*{W^_$v8Hytz zo&yO(lmL#$Fi1)lAiv_}m?W}YouWk3F`XxMj0`>(`wnPf#(Qz6KwHMWO}-ED%&sXV93kU7Yz1g;9?Mgrdkq$C3@iV+j( zSd95|oFb*utfWCE9#dnGDZ~9kybezgM#UJ)5Ln%i;gdS~h;}#gzsV8)u~Woj_yVX! zYGB$In`_JgZQ(k8I$md?_agI>EqJ|osYM9-Iw~kt$0eEgV$0|YfCx)EIlQ&4!htUX z6h8w$(?V>(=|SdY@UzyuJb}kR33Xx}XaHG={ea;!KwumNu&Wvh1ZD;eF{q@#A3c*= zo(6m@fLJ@UuSys`D+z+e@G2<^aAm-Gh>InjCn@+0aIhjR3jpo&JJ45Tc>|~=kp$Wa z?4A)UYgA-Xlqp_kn3zKM(C+ivv)!WOko7$Ld^8Ea0Ji^1^J;S)6dzJzlClKn+Afrf z@ih>P;FsW+n(NK0w&IuJYt0SjMl-j%lYTYNYL85k6)t=|I4<;qU!gu30L)F`IB-fgoI~>U#{PtsxQPfu@K?a#Hu-P1_Hzg zKuH<^(pG&$(WMwEh!DvGYXR&4lM*6`oXA4)fFW6qfo;+`;3jlP12hQE1&yu4097GhrzrZb?-}3q2b|b1^Q$%$W2r0|lY3_ofC92)s zi0bB4Q9*HcGVrV2`FtWHY5KsLgY%4$V$6Ub9}zQiUOoHR9hG&Nh$=h zkfDeYP-{BGIvQ)S=Uuj|n=S1=5e7?w3o)>Tz|{c*sL`YhIS)w-MhtKqN^t_+qsyOA zVvw}cU|SkVK{NsJpbm~C2Em3VG77J2Kq_+<*VqY@|LZ>gAS5|$-4{d_no799toN9= z&Y3W=6j zfm&ijaHar<5CO#)j)YQ)p;(-g+@)eg1c)4Hda{t^4)d;K$U=mOLG8#w} z%SMb%Yyd<7f~r8$4J`~%&44Nl@NEzg@{nn$vP2oM4XNNFK^KQaX*{rOP-EdlDDtVm zH<4@%bW|4=S&doT*NH9RgsRE{Z>&pT1)!f02wP%OGzhkV{A9oY1&QMSqX~s9rm$?# zJ|gSjG%avZ0q=n2`?D%mnnJTs6ecO)Hm%^!AXSBdeU$*HC548Nzh_0j?!g5@8Cqe% zA3)EePk{xWRKnYNw;d!oy^jQbP z)j?U7w8ZP7nGkAKO(jd?O^Xs-*Kyl*8L<}HZ!LfHjQL!LKUznuKjx3FGM{bVrmMU7 zqid7?=sMVz0mK$!Ynwm1u^WGMTZ%t=9yYL-`GWZ(9IZ(ArEdJu{#1W7r!qm?b#9v= z?oT=)a6kg3$4H180iZOXiX|Gzc};KoFS#;4bY6FQ&!mnia-`$%vp^l3=b`M z6p$7?z_!veUF_02KnpySc0jWk$*Q_p!4V{bH1MgE1|hEDC!PXyAfA9$uvefJjCh)O z#(dR$4Vn~}BwBHk@!<;=nczo*pi-M?{(?>?;-F;_9o=2TYd|ItuLDZInShEn0H5DX zf<+~CC$s~`2MN9HNt;{>g+Lr8K7vMh%Ua(y-|Mi}Pl+RN`^P#Wh@-?8<~!!Q?N_?Co@0biZr26~n=k2GpvBMtx)QQNhz>aw_!X6hR!0sJP+g`VIaN~Y5F*X6q9j2& zWwk;9-a>UwpgEFe4Imt35qb`L#slPa*8l;Oti}Qh4*9QTY!J7RDwqI31~7KI2)x># z)ni3Vh79E@4q`;Q4VPpZ`X;UBFIfhj1>D7d-|SKwAUb7Nwt;AtEda{-vH1}+9G1nk zR;!h9Y(p%bFd$nlitkFOm!)FK2UN3cMF|D!s8Lzn!rjO8=)$T4HF@iwvDwb>-|^EM-~osw!K zw&4k*u$>0?8Mc+$%HU`j#h7l4Vrq&}9EH2jHoq{xgrj8?Uv*;?Uw1VMSLyWbC>-0| z#M)y53&aalBn+!dfCKIgn#Z9d2wLA{HU^QHE_Er}0Iw#f(6MKAFo6EZB4{rK&3iG0 zhqf;$`2bngqiE}dt^&F&AORG4U<82-1-XE+YCv2=VO)&y5_CHJXDJ+@usZbxQo!B- z-I45Npcw;lAC3ESC>$Wppa~bc^`RRHU;t17HkXFp1J3#kO;AOxhh0|PmTe@_fcu6X zJ)MW$gK{V2Z;DmOhLBR?XdxylJxk(FC^2i=fQ1~`YUr}4eHGG04RnKt?m!6KHDLb# z51>p+*#>2R-_1i280Q%RV>?qIWSxiRD3_-vS~s)u>19WXbw8@~kyIvu|QzYD(` zzZbtBe-Ph`@53L#AH$!(pTeKPpTl3kU&0T-WYw!MN!5$Uf=Mbb(TB)}S*rfTKq3d` zsq%|#_BWThz;2swqCX@ zn;WLA@@*u5(+~ig2{zSMYg=MF%eK~bh3!h)Rkn?`O}1-n*V(SOZMEHCyD=?2&7Iai zZD3ky+Jv-uX+~N@+Ba!Gr2Wf|+a2~SyU$MAIeV#nrhT@(+CI-d-+sD%sr?N53j101 zv+d{DFS4(*UuEBD-(=rrf6e}e{Vn@D_V?@`*gv#?WdFqenf-|U3;S30Z|vXMzqkKr z|H=M~{kL?Kj;Gtw?dcim&h%dCebNKz!Sqq-W74;$??~U7@lwV|8J}c)mT@HGi;Q0! z4o8;5?eIE$4!>i7BjAWS1jlg4NJp7tlw*=(p2Ki7I2Jpa9j%VPI4*Qt?6}l%nd5TD zI>&m)7RN5f&5i?(PaK~)jyS$>eC7Dv*~{s2dYpZn+0K5>kh91+!ddDpcaCmhf*GuAWD zGvA|nbWhCF>{;eH!?VJ3w&xtrxt{YqYdz~c>pdGh*L${kwtIGXZu8vldC;@hv(NL0 z=P}O{p8cLTJ#Tw$UcYyMH{cC=L*5cE?Pa}!SMm<=mU>5d$9TtiE4>$ZFY;dEUE^Kr zy~2B?_bTs3?r3i@HP6D z`OfyO_MPXuz<0Utao>}^r+v@*p7*`z+wVK*d&T#f?+xEuzIS}@`9AP{==;d`iSIMt z5#JZSuYBM5zVm(W`_cDPzx00Ye%^j^zpMNKKjY{8f?x6v@sIY8^H1-<~%+x*-8JN!HSyZyKL_xNx3-{HT@f4Bc$|NZ_4{d@iU{Ezq_^FQH#%Kwc2 zIsXg(m;49(FZ)0Bf8+nI|9}DU0nZKmF>q4A74QUl2l@u`0^z{PfucYpP!bpum=c&4 zm>!rJPy$VXmcWw0vcMUE6@kA5%)qL^>c9tq&jUwu`s6Ilxg=*z&f1(Sa<0tTm2-E_ z13CZ5c_``J%|TwL3_{@>>nH$%n9ZO^Md)o zf*=~mgiZ+!3!N4!4V8yRhbD(ALsg*}q4}Zpp$(y{Lw^lj8`>P&651Bp z9@-Jw8QLAXCA24Wd+3hPU7@=}_lE8dJs8>>+825x^jPSL&{LteLm!1c$#V_T2JOo~ zlK*S|?_n%Vgk9l&;b1rv9uy9TPY%oBk>RrNqOciW6lEK_l3U6Prf*}Q^1?2^GCqFber*LfHyu$f~YN1{jD{L-YQn;+}%)*t0XBA#u zxV3P5;f}(cg?}r2r0}uACkme`e5UZZ!WRl(Dm+m5auHi}YSFMFqv+Y9_lph{9WMH~ z=+mO_i+(QpwdnU^Td}=3qu5#8yEwbJUvdB9LB$2dgNuub+2T`+hZUbzJhHf~cvSJE z;wi<;ihqu{Bi=}#NOq)OBpewWDTWdBHJT7B0D3yBez8ML~f7V5!oAgGV*lfYjP~P zqhx={$0eVZd|q<26Dg6QCAG%7`hMu$g7L`$O+qLZRiqLt7; zw;);@T@5J;>8{&r080H951;2<+IF{<4@rhSqbG08b?Q>0Z@W6NODwxV*f^?d`)Fo3DSWa+Zn`quhU z)(D41Sx!%W6+w+Vr+xGkrcvwKULAXEda$|un$uw9sKy=a=HxL-^FpJ!tQIQeE!|(P zbfKXwYHqEEaq1ek{|_%w;|`?U^6~eY`-kssyYc<5C`ES~IPL}8Fp4b)?h!hF_>puO zME_-w#*8W}Cs_)}dfTnhG*JONS1vaYv@;JYTD^6KBol*-;H;{7899Y-*D-V?liK^vUIm&Zw$j z$4r|lEt@(<8hQX!bJWy%yGM^1J8t}hi4~J3PwANdhhhE!Fc=?zvG?Ha%4t>8XUv>6 zdrtLSGZ%*Ctp==o7%(qLjF=Y!FHrn%+n_rah&$$)|AL!^OuLOxV-kxy~|stO4kj(=(X)H z-1_z3f8SZM{jP$W;FSBRh0|tay?^mbKm7X6Wv1ih(lxiazW9*Y`1+0;?|VAq_QP<> zX4`3>&sjWDH@@F#ziRjN==vA8iR0dzeVw@B_rJ3r+^`W&`R)4QFD`xYy1~~Ez2}94 zkFGgo;2qEIyg^@j@}oZ=EPtl_rZAkM$@?X^A--&Ay`TUzdVPn<$r*=ELjTjIyV2?F+9I*$ucSB+Yj6XoW1PKt;-}xXFc%67y0mg z84MC(VrT+<3Caxknv*qXJfgb&79{8YRGpNh|cFCJNd z*WnHD)gw*#1^A`-dVC|k8UKkWgar4G(%P=cYy;5= 1.0.0" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +confbox@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06" + integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w== + +cookie@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +cookie@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.0.2.tgz#27360701532116bd3f1f9416929d176afe1e4610" + integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA== + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cross-spawn-async@^2.1.8: + version "2.2.5" + resolved "https://registry.yarnpkg.com/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz#845ff0c0834a3ded9d160daca6d390906bb288cc" + integrity sha512-snteb3aVrxYYOX9e8BabYFK9WhCDhTlw1YQktfTthBogxri4/2r9U2nQc0ffY73ZAxezDc+U8gvHAeU1wy1ubQ== + dependencies: + lru-cache "^4.0.0" + which "^1.2.8" + +css-select@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.0.0.tgz#b1121ca51848dd264e2244d058cee254deeb44b0" + integrity sha512-/xPlD7betkfd7ChGkLGGWx5HWyiHDOSn7aACLzdH0nwucPvB0EAm8hMBm7Xn7vGfAeRRN7KZ8wumGm8NoNcMRw== + dependencies: + boolbase "~1.0.0" + css-what "1.0" + domutils "1.4" + nth-check "~1.0.0" + +css-what@1.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-1.0.0.tgz#d7cc2df45180666f99d2b14462639469e00f736c" + integrity sha512-60SUMPBreXrLXgvpM8kYpO0AOyMRhdRlXFX5BMQbZq1SIJCyNE56nqFQhmvREQdUJpedbGRYZ5wOyq3/F6q5Zw== + +css@^2.2.0: + version "2.2.4" + resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929" + integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw== + dependencies: + inherits "^2.0.3" + source-map "^0.6.1" + source-map-resolve "^0.5.2" + urix "^0.1.0" + +cssom@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.0.tgz#386d5135528fe65c1ee1bc7c4e55a38854dbcf7a" + integrity sha512-rdm8ap6kLpJjI9MDKoECB/Eb8ft+JZNDWJ6zETzR34vUTLLgztSNUVFxOzojw3F1WJewhzQETeh5EHwHMUumbQ== + +d@1, d@^1.0.1, d@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.2.tgz#2aefd554b81981e7dccf72d6842ae725cb17e5de" + integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw== + dependencies: + es5-ext "^0.10.64" + type "^2.7.2" + +d@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309" + integrity sha512-0SdM9V9pd/OXJHoWmTfNPTAeD+lw6ZqHg+isPyBFuJsZLSE0Ygg1cYZ/0l6DrKQXMOqGOu1oWupMoOfoRfMZrQ== + dependencies: + es5-ext "~0.10.2" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== + dependencies: + assert-plus "^1.0.0" + +data-uri-to-buffer@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz#d296973d5a4897a5dbe31716d118211921f04770" + integrity sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA== + +datauri@~0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/datauri/-/datauri-0.2.1.tgz#f4e8addbb3e54e3dc12d1c88543b8b0b1bf692fa" + integrity sha512-DnSr++hwAaHxdj7vwxbPTGz+MAiN+ZaiutJW4CpuYGIFMaJcox6v2/Va3++CQOhx0tYD1dPQn/qKeTnrLihlUw== + dependencies: + mimer "*" + templayed "*" + +dayjs@^1.11.18: + version "1.11.18" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.18.tgz#835fa712aac52ab9dec8b1494098774ed7070a11" + integrity sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA== + +debug@^4.3.7: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +decamelize@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== + +decode-uri-component@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== + +deep-eql@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341" + integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== + +deep-extend@^0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" + integrity sha512-cQ0iXSEKi3JRNhjUsLWvQ+MVPxLVqpwCd0cFsWbJxlCim2TlCo1JvN5WaPdPvSpUdEnkJ/X+mPGcq5RJ68EK8g== + +deep-rename-keys@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/deep-rename-keys/-/deep-rename-keys-0.2.1.tgz#ede78537d7a66a2be61517e2af956d7f58a3f1d8" + integrity sha512-RHd9ABw4Fvk+gYDWqwOftG849x0bYOySl/RgX0tLI9i27ZIeSO91mLZJEp7oPHOMFqHvpgu21YptmDt0FYD/0A== + dependencies: + kind-of "^3.0.2" + rename-keys "^1.1.2" + +defu@^6.1.4: + version "6.1.4" + resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479" + integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +detect-libc@^2.0.3, detect-libc@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + +devalue@^4.3.0: + version "4.3.3" + resolved "https://registry.yarnpkg.com/devalue/-/devalue-4.3.3.tgz#e35df3bdc49136837e77986f629b9fa6fef50726" + integrity sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg== + +dom-serializer@0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + +dom-serializer@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" + integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== + dependencies: + domelementtype "^1.3.0" + entities "^1.1.1" + +domelementtype@1, domelementtype@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domelementtype@^2.0.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@2.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.3.0.tgz#2de59a0822d5027fabff6f032c2b25a2a8abe738" + integrity sha512-q9bUwjfp7Eif8jWxxxPSykdRZAb6GkguBGSgvvCrhI9wB71W2K/Kvv4E61CF/mcCfnVJDeDWx/Vb/uAqbDj6UQ== + dependencies: + domelementtype "1" + +domutils@1.4: + version "1.4.3" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.4.3.tgz#0865513796c6b306031850e175516baf80b72a6f" + integrity sha512-ZkVgS/PpxjyJMb+S2iVHHEZjVnOUtjGp0/zstqKGTE9lrZtNHlNQmLwP/lhLMEApYbzc08BKMx9IFpKhaSbW1w== + dependencies: + domelementtype "1" + +domutils@1.5: + version "1.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" + integrity sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw== + dependencies: + dom-serializer "0" + domelementtype "1" + +duplexify@^3.2.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" + integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +end-of-stream@^1.0.0: + version "1.4.5" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c" + integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg== + dependencies: + once "^1.4.0" + +entities@1.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.0.0.tgz#b2987aa3821347fcde642b24fdfc9e4fb712bf26" + integrity sha512-LbLqfXgJMmy81t+7c14mnulFHJ170cM6E+0vMXR9k/ZiZwgX8i5pNgjTCX3SO4VeUsFLV+8InixoretwU+MjBQ== + +entities@^1.1.1, entities@~1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" + integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +error-stack-parser-es@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz#e6a1655dd12f39bb3a85bf4c7088187d78740327" + integrity sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA== + +es-module-lexer@^1.5.4: + version "1.7.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" + integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== + +es5-ext@^0.10.35, es5-ext@^0.10.62, es5-ext@^0.10.64, es5-ext@~0.10.11, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.5, es5-ext@~0.10.6: + version "0.10.64" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714" + integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + esniff "^2.0.1" + next-tick "^1.1.0" + +es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-iterator@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-0.1.3.tgz#d6f58b8c4fc413c249b4baa19768f8e4d7c8944e" + integrity sha512-6TOmbFM6OPWkTe+bQ3ZuUkvqcWUjAnYjKUCLdbvRsAUz2Pr+fYIibwNXNkLNtIK9PPFbNMZZddaRNkyJhlGJhA== + dependencies: + d "~0.1.1" + es5-ext "~0.10.5" + es6-symbol "~2.0.1" + +es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.4.tgz#f4e7d28013770b4208ecbf3e0bf14d3bcb557b8c" + integrity sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg== + dependencies: + d "^1.0.2" + ext "^1.7.0" + +es6-symbol@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-2.0.1.tgz#761b5c67cfd4f1d18afb234f691d678682cb3bf3" + integrity sha512-wjobO4zO8726HVU7mI2OA/B6QszqwHJuKab7gKHVx+uRfVVYGcWJkCIFxV2Madqb9/RUSrhJ/r6hPfG7FsWtow== + dependencies: + d "~0.1.1" + es5-ext "~0.10.5" + +es6-weak-map@~0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-0.1.4.tgz#706cef9e99aa236ba7766c239c8b9e286ea7d228" + integrity sha512-P+N5Cd2TXeb7G59euFiM7snORspgbInS29Nbf3KNO2JQp/DyhvMCDWd58nsVAXwYJ6W3Bx7qDdy6QQ3PCJ7jKQ== + dependencies: + d "~0.1.1" + es5-ext "~0.10.6" + es6-iterator "~0.1.3" + es6-symbol "~2.0.1" + +esbuild@0.17.19: + version "0.17.19" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.19.tgz#087a727e98299f0462a3d0bcdd9cd7ff100bd955" + integrity sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw== + optionalDependencies: + "@esbuild/android-arm" "0.17.19" + "@esbuild/android-arm64" "0.17.19" + "@esbuild/android-x64" "0.17.19" + "@esbuild/darwin-arm64" "0.17.19" + "@esbuild/darwin-x64" "0.17.19" + "@esbuild/freebsd-arm64" "0.17.19" + "@esbuild/freebsd-x64" "0.17.19" + "@esbuild/linux-arm" "0.17.19" + "@esbuild/linux-arm64" "0.17.19" + "@esbuild/linux-ia32" "0.17.19" + "@esbuild/linux-loong64" "0.17.19" + "@esbuild/linux-mips64el" "0.17.19" + "@esbuild/linux-ppc64" "0.17.19" + "@esbuild/linux-riscv64" "0.17.19" + "@esbuild/linux-s390x" "0.17.19" + "@esbuild/linux-x64" "0.17.19" + "@esbuild/netbsd-x64" "0.17.19" + "@esbuild/openbsd-x64" "0.17.19" + "@esbuild/sunos-x64" "0.17.19" + "@esbuild/win32-arm64" "0.17.19" + "@esbuild/win32-ia32" "0.17.19" + "@esbuild/win32-x64" "0.17.19" + +esbuild@0.25.4: + version "0.25.4" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.4.tgz#bb9a16334d4ef2c33c7301a924b8b863351a0854" + integrity sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q== + optionalDependencies: + "@esbuild/aix-ppc64" "0.25.4" + "@esbuild/android-arm" "0.25.4" + "@esbuild/android-arm64" "0.25.4" + "@esbuild/android-x64" "0.25.4" + "@esbuild/darwin-arm64" "0.25.4" + "@esbuild/darwin-x64" "0.25.4" + "@esbuild/freebsd-arm64" "0.25.4" + "@esbuild/freebsd-x64" "0.25.4" + "@esbuild/linux-arm" "0.25.4" + "@esbuild/linux-arm64" "0.25.4" + "@esbuild/linux-ia32" "0.25.4" + "@esbuild/linux-loong64" "0.25.4" + "@esbuild/linux-mips64el" "0.25.4" + "@esbuild/linux-ppc64" "0.25.4" + "@esbuild/linux-riscv64" "0.25.4" + "@esbuild/linux-s390x" "0.25.4" + "@esbuild/linux-x64" "0.25.4" + "@esbuild/netbsd-arm64" "0.25.4" + "@esbuild/netbsd-x64" "0.25.4" + "@esbuild/openbsd-arm64" "0.25.4" + "@esbuild/openbsd-x64" "0.25.4" + "@esbuild/sunos-x64" "0.25.4" + "@esbuild/win32-arm64" "0.25.4" + "@esbuild/win32-ia32" "0.25.4" + "@esbuild/win32-x64" "0.25.4" + +esbuild@^0.21.3: + version "0.21.5" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" + integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== + optionalDependencies: + "@esbuild/aix-ppc64" "0.21.5" + "@esbuild/android-arm" "0.21.5" + "@esbuild/android-arm64" "0.21.5" + "@esbuild/android-x64" "0.21.5" + "@esbuild/darwin-arm64" "0.21.5" + "@esbuild/darwin-x64" "0.21.5" + "@esbuild/freebsd-arm64" "0.21.5" + "@esbuild/freebsd-x64" "0.21.5" + "@esbuild/linux-arm" "0.21.5" + "@esbuild/linux-arm64" "0.21.5" + "@esbuild/linux-ia32" "0.21.5" + "@esbuild/linux-loong64" "0.21.5" + "@esbuild/linux-mips64el" "0.21.5" + "@esbuild/linux-ppc64" "0.21.5" + "@esbuild/linux-riscv64" "0.21.5" + "@esbuild/linux-s390x" "0.21.5" + "@esbuild/linux-x64" "0.21.5" + "@esbuild/netbsd-x64" "0.21.5" + "@esbuild/openbsd-x64" "0.21.5" + "@esbuild/sunos-x64" "0.21.5" + "@esbuild/win32-arm64" "0.21.5" + "@esbuild/win32-ia32" "0.21.5" + "@esbuild/win32-x64" "0.21.5" + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +esniff@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308" + integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg== + dependencies: + d "^1.0.1" + es5-ext "^0.10.62" + event-emitter "^0.3.5" + type "^2.7.2" + +estree-walker@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" + integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== + +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + +event-emitter@^0.3.5, event-emitter@~0.3.4: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA== + dependencies: + d "1" + es5-ext "~0.10.14" + +eventemitter3@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba" + integrity sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg== + +exit-hook@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-2.2.1.tgz#007b2d92c6428eda2b76e7016a34351586934593" + integrity sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw== + +expect-type@^1.1.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.2.2.tgz#c030a329fb61184126c8447585bc75a7ec6fbff3" + integrity sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA== + +exsolve@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.7.tgz#3b74e4c7ca5c5f9a19c3626ca857309fa99f9e9e" + integrity sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw== + +ext@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== + dependencies: + type "^2.7.2" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== + +extsprintf@^1.2.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" + integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +find-index@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/find-index/-/find-index-0.1.1.tgz#675d358b2ca3892d795a1ab47232f8b6e2e0dde4" + integrity sha512-uJ5vWrfBKMcE6y2Z8834dwEZj9mNGxYa3t3I53OwFeuZ8D9oc2E5zcsrkuhX6h4iYrjhiv0T3szQmxlAV9uxDg== + +first-chunk-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz#59bfb50cd905f60d7c394cd3d9acaab4e6ad934e" + integrity sha512-ArRi5axuv66gEsyl3UuK80CzW7t56hem73YGNYxNWTGNKFJUadSb9Gu9SHijYEUi8ulQMf1bJomYNwSCPHhtTQ== + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +gaze@^0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/gaze/-/gaze-0.5.2.tgz#40b709537d24d1d45767db5a908689dfe69ac44f" + integrity sha512-3IWbXGkDDHFX8zIlNdfnmhvlSMhpBO6tDr4InB8fGku6dh/gjFPGNqcdsXJajZg05x9jRzXbL6gCnCnuMap4tw== + dependencies: + globule "~0.1.0" + +get-source@^2.0.12: + version "2.0.12" + resolved "https://registry.yarnpkg.com/get-source/-/get-source-2.0.12.tgz#0b47d57ea1e53ce0d3a69f4f3d277eb8047da944" + integrity sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w== + dependencies: + data-uri-to-buffer "^2.0.0" + source-map "^0.6.1" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== + dependencies: + assert-plus "^1.0.0" + +glob-stream@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-4.1.1.tgz#b842df10d688c7eb6bcfcebd846f3852296b3200" + integrity sha512-P/IscjTmgjOQ53stHEjq2QUzTqcdBOzxL/Cif8SYad+Kb8IgqjYlggefqCSi33Vg5Ey1tx1pn+pyE+tRtf8j1A== + dependencies: + glob "^4.3.1" + glob2base "^0.0.12" + minimatch "^2.0.1" + ordered-read-streams "^0.1.0" + through2 "^0.6.1" + unique-stream "^2.0.2" + +glob-to-regexp@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob-watcher@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/glob-watcher/-/glob-watcher-0.0.8.tgz#68aeb661e7e2ce8d3634381b2ec415f00c6bc2a4" + integrity sha512-Wbq7bkUwTSc3L25dKq3CPpFaaeYxARAUJtEUeNMmjcDOwQl6T2CI3blthbfLP5BCiYbAX7c/eSxZ3M1R+bPnDw== + dependencies: + gaze "^0.5.1" + +glob2base@^0.0.12: + version "0.0.12" + resolved "https://registry.yarnpkg.com/glob2base/-/glob2base-0.0.12.tgz#9d419b3e28f12e83a362164a277055922c9c0d56" + integrity sha512-ZyqlgowMbfj2NPjxaZZ/EtsXlOch28FRXgMd64vqZWk1bT9+wvSRLYD1om9M7QfQru51zJPAT17qXm4/zd+9QA== + dependencies: + find-index "^0.1.1" + +glob@^4.3.1: + version "4.5.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-4.5.3.tgz#c6cb73d3226c1efef04de3c56d012f03377ee15f" + integrity sha512-I0rTWUKSZKxPSIAIaqhSXTM/DiII6wame+rEC3cFA5Lqmr9YmdL7z6Hj9+bdWtTvoY1Su4/OiMLmb37Y7JzvJQ== + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "^2.0.1" + once "^1.3.0" + +glob@~3.1.21: + version "3.1.21" + resolved "https://registry.yarnpkg.com/glob/-/glob-3.1.21.tgz#d29e0a055dea5138f4d07ed40e8982e83c2066cd" + integrity sha512-ANhy2V2+tFpRajE3wN4DhkNQ08KDr0Ir1qL12/cUe5+a7STEK8jkW4onUYuY8/06qAFuT5je7mjAqzx0eKI2tQ== + dependencies: + graceful-fs "~1.2.0" + inherits "1" + minimatch "~0.2.11" + +globule@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/globule/-/globule-0.1.0.tgz#d9c8edde1da79d125a151b79533b978676346ae5" + integrity sha512-3eIcA2OjPCm4VvwIwZPzIxCVssA8HSpM2C6c6kK5ufJH4FGwWoyqL3In19uuX4oe+TwH3w2P1nQDmW56iehO4A== + dependencies: + glob "~3.1.21" + lodash "~1.0.1" + minimatch "~0.2.11" + +graceful-fs@^3.0.0: + version "3.0.12" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-3.0.12.tgz#0034947ce9ed695ec8ab0b854bc919e82b1ffaef" + integrity sha512-J55gaCS4iTTJfTXIxSVw3EMQckcqkpdRv3IR7gu6sq0+tbC363Zx6KH/SEwXASK9JRbhyZmVjJEVJIOxYsB3Qg== + dependencies: + natives "^1.1.3" + +graceful-fs@^4.2.6: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +graceful-fs@~1.2.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-1.2.3.tgz#15a4806a57547cb2d2dbf27f42e89a8c3451b364" + integrity sha512-iiTUZ5vZ+2ZV+h71XAgwCSu6+NAizhFU3Yw8aC/hH5SQ3SnISqEqAek40imAFGtDcwJKNhXvSY+hzIolnLwcdQ== + +"graceful-readlink@>= 1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" + integrity sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w== + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== + +har-validator@~5.1.3: + version "5.1.5" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== + dependencies: + ajv "^6.12.3" + har-schema "^2.0.0" + +htmlparser2@~3.8.1: + version "3.8.3" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.8.3.tgz#996c28b191516a8be86501a7d79757e5c70c1068" + integrity sha512-hBxEg3CYXe+rPIua8ETe7tmG3XDn9B0edOE/e9wH2nLczxzgdu0m0aNHY+5wFZiviLWLdANPJTssa92dMcXQ5Q== + dependencies: + domelementtype "1" + domhandler "2.3" + domutils "1.5" + entities "1.0" + readable-stream "1.1" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ== + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-1.0.2.tgz#ca4309dadee6b54cc0b8d247e8d7c7a0975bdc9b" + integrity sha512-Al67oatbRSo3RV5hRqIoln6Y5yMVbJSIn4jEJNL7VCImzq/kLr7vvb6sFRJXqr8rpHc/2kJOM+y0sPKN47VdzA== + +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-arrayish@^0.3.1: + version "0.3.4" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.4.tgz#1ee5553818511915685d33bb13d31bf854e5059d" + integrity sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA== + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + integrity sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q== + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== + +jpeg-js@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa" + integrity sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg== + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + +jsprim@^1.2.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" + integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.4.0" + verror "1.10.0" + +juice@^1.0.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/juice/-/juice-1.11.0.tgz#4a7ff6bef3f3fa4b610f824aeb27ed8f3601ad73" + integrity sha512-i2lbYX1H5xjtV6Tqa8kIcE4rKZ5cFeuNlQZTXZ5uh2nk5CKCCWtgDs6v5DehQMc0cD6+6LdyjIHIZnRLHGbohw== + dependencies: + batch "0.5.3" + cheerio "0.19.0" + commander "2.9.0" + cross-spawn-async "^2.1.8" + cssom "0.3.0" + deep-extend "^0.4.0" + slick "1.12.2" + util-deprecate "^1.0.2" + web-resource-inliner "1.2.1" + +kind-of@^3.0.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ== + dependencies: + is-buffer "^1.1.5" + +kleur@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" + integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + integrity sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ== + +lodash@^3.10.1, lodash@^3.2.0, lodash@^3.5.0: + version "3.10.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" + integrity sha512-9mDDwqVIma6OZX79ZlDACZl8sBm0TEnkf99zV3iMA4GzkIT/9hiqP5mY0HoT1iNLCrKc/R1HByV+yJfRWVJryQ== + +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +lodash@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-1.0.2.tgz#8f57560c83b59fc270bd3d561b690043430e2551" + integrity sha512-0VSEDVec/Me2eATuoiQd8IjyBMMX0fahob8YJ96V1go2RjvCk1m1GxmtfXn8RNSaLaTtop7fsuhhu9oLk3hUgA== + +longest@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + integrity sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg== + +loupe@^3.1.0, loupe@^3.1.2: + version "3.2.1" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.2.1.tgz#0095cf56dc5b7a9a7c08ff5b1a8796ec8ad17e76" + integrity sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ== + +lru-cache@2: + version "2.7.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" + integrity sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ== + +lru-cache@^4.0.0: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +lru-queue@0.1: + version "0.1.0" + resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" + integrity sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ== + dependencies: + es5-ext "~0.10.2" + +magic-string@^0.25.3: + version "0.25.9" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" + integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== + dependencies: + sourcemap-codec "^1.4.8" + +magic-string@^0.30.12: + version "0.30.19" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.19.tgz#cebe9f104e565602e5d2098c5f2e79a77cc86da9" + integrity sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.5" + +memoizee@~0.3.8: + version "0.3.10" + resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.3.10.tgz#4eca0d8aed39ec9d017f4c5c2f2f6432f42e5c8f" + integrity sha512-LLzVUuWwGBKK188spgOK/ukrp5zvd9JGsiLDH41pH9vt5jvhZfsu5pxDuAnYAMG8YEGce72KO07sSBy9KkvOfw== + dependencies: + d "~0.1.1" + es5-ext "~0.10.11" + es6-weak-map "~0.1.4" + event-emitter "~0.3.4" + lru-queue "0.1" + next-tick "~0.2.2" + timers-ext "0.1" + +merge-stream@^0.1.7: + version "0.1.8" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-0.1.8.tgz#48a07b3b4a121d74a3edbfdcdb4b08adbf0240b1" + integrity sha512-ivGsLZth/AkvevAzPlRLSie8Q3GdyH/5xUYgn+ItAJYslT0NsKd2cxx0bAjmqoY5swX0NoWJjvkDkfpaVZx9lw== + dependencies: + through2 "^0.6.1" + +microdiff@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/microdiff/-/microdiff-1.5.0.tgz#d16219b223396f11ffcf441da26a43d3e6bd06f8" + integrity sha512-Drq+/THMvDdzRYrK0oxJmOKiC24ayUV8ahrt8l3oRK51PWt6gdtrIGrlIH3pT/lFh1z93FbAcidtsHcWbnRz8Q== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + +mimer@*: + version "2.0.2" + resolved "https://registry.yarnpkg.com/mimer/-/mimer-2.0.2.tgz#941da26070e80bb485aed8a1ef4a5a325cbbfa96" + integrity sha512-izxvjsB7Ur5HrTbPu6VKTrzxSMBFBqyZQc6dWlZNQ4/wAvf886fD4lrjtFd8IQ8/WmZKdxKjUtqFFNaj3hQ52g== + +miniflare@3.20250204.1: + version "3.20250204.1" + resolved "https://registry.yarnpkg.com/miniflare/-/miniflare-3.20250204.1.tgz#12c6d6b1ca9c3afdfdf05432652188d787edfabc" + integrity sha512-B4PQi/Ai4d0ZTWahQwsFe5WAfr1j8ISMYxJZTc56g2/btgbX+Go099LmojAZY/fMRLhIYsglcStW8SeW3f/afA== + dependencies: + "@cspotcode/source-map-support" "0.8.1" + acorn "8.14.0" + acorn-walk "8.3.2" + exit-hook "2.2.1" + glob-to-regexp "0.4.1" + stoppable "1.1.0" + undici "^5.28.4" + workerd "1.20250204.0" + ws "8.18.0" + youch "3.2.3" + zod "3.22.3" + +miniflare@4.20251008.0: + version "4.20251008.0" + resolved "https://registry.yarnpkg.com/miniflare/-/miniflare-4.20251008.0.tgz#b8cc820ea06089f7955edd56a46e1c3fedabd83d" + integrity sha512-sKCNYNzXG6l8qg0Oo7y8WcDKcpbgw0qwZsxNpdZilFTR4EavRow2TlcwuPSVN99jqAjhz0M4VXvTdSGdtJ2VfQ== + dependencies: + "@cspotcode/source-map-support" "0.8.1" + acorn "8.14.0" + acorn-walk "8.3.2" + exit-hook "2.2.1" + glob-to-regexp "0.4.1" + sharp "^0.33.5" + stoppable "1.1.0" + undici "7.14.0" + workerd "1.20251008.0" + ws "8.18.0" + youch "4.1.0-beta.10" + zod "3.22.3" + +minimatch@^2.0.1: + version "2.0.10" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-2.0.10.tgz#8d087c39c6b38c001b97fca7ce6d0e1e80afbac7" + integrity sha512-jQo6o1qSVLEWaw3l+bwYA2X0uLuK2KjNh2wjgO7Q/9UJnXr1Q3yQKR8BI0/Bt/rPg75e6SMW4hW/6cBHVTZUjA== + dependencies: + brace-expansion "^1.0.0" + +minimatch@~0.2.11: + version "0.2.14" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.2.14.tgz#c74e780574f63c6f9a090e90efbe6ef53a6a756a" + integrity sha512-zZ+Jy8lVWlvqqeM8iZB7w7KmQkoJn8djM585z88rywrEbzoqawVa9FR5p2hwD+y74nfuKOjmNvi9gtWJNLqHvA== + dependencies: + lru-cache "2" + sigmund "~1.0.0" + +minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@^0.5.0: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mlly@^1.7.4: + version "1.8.0" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.8.0.tgz#e074612b938af8eba1eaf43299cbc89cb72d824e" + integrity sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g== + dependencies: + acorn "^8.15.0" + pathe "^2.0.3" + pkg-types "^1.3.1" + ufo "^1.6.1" + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +mustache@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64" + integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ== + +nanoid@^3.3.11: + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + +natives@^1.1.3: + version "1.1.6" + resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.6.tgz#a603b4a498ab77173612b9ea1acdec4d980f00bb" + integrity sha512-6+TDFewD4yxY14ptjKaS63GVdtKiES1pTPyxn9Jb0rBqPMZ7VcCiooEhPNsr+mqHtMGxa/5c/HhcC4uPEUw/nA== + +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + +next-tick@~0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-0.2.2.tgz#75da4a927ee5887e39065880065b7336413b310d" + integrity sha512-f7h4svPtl+QidoBv4taKXUjJ70G2asaZ8G28nS0OkqaalX8dwwrtWtyxEDPK62AC00ur/+/E0pUwBwY5EPn15Q== + +nth-check@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== + dependencies: + boolbase "~1.0.0" + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-2.1.1.tgz#43c36e5d569ff8e4816c4efa8be02d26967c18aa" + integrity sha512-CdsOUYIh5wIiozhJ3rLQgmUTgcyzFwZZrqhkKhODMoGtPKM+wt0h0CNIoauJWMsS9822EdzPsF/6mb4nLvPN5g== + +ohash@^1.1.4: + version "1.1.6" + resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.1.6.tgz#9ff7b0271d7076290794537d68ec2b40a60d133e" + integrity sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg== + +ohash@^2.0.11: + version "2.0.11" + resolved "https://registry.yarnpkg.com/ohash/-/ohash-2.0.11.tgz#60b11e8cff62ca9dee88d13747a5baa145f5900b" + integrity sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ== + +once@^1.3.0, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +ordered-read-streams@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-0.1.0.tgz#fd565a9af8eb4473ba69b6ed8a34352cb552f126" + integrity sha512-PMX5ehiNri4+lgk9fl09xuPeciGmyPyVUSBwwPT4C/3EHGxoVf7UdgKDE3SLBD4pUDmlzrg1L1cK5igrp+Tyuw== + +pako@^1.0.5: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + +path-to-regexp@6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4" + integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ== + +path@^0.11.14: + version "0.11.14" + resolved "https://registry.yarnpkg.com/path/-/path-0.11.14.tgz#cbc7569355cb3c83afeb4ace43ecff95231e5a7d" + integrity sha512-CzEXTDgcEfa0yqMe+DJCSbEB5YCv4JZoic5xulBNFF2ifIMjNrTWbNSPNhgKfSo0MjneGIx9RLy4pCFuZPaMSQ== + +pathe@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" + integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== + +pathe@^2.0.1, pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== + +pathval@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.1.tgz#8855c5a2899af072d6ac05d11e46045ad0dc605d" + integrity sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ== + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +pkg-types@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.3.1.tgz#bd7cc70881192777eef5326c19deb46e890917df" + integrity sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ== + dependencies: + confbox "^0.1.8" + mlly "^1.7.4" + pathe "^2.0.1" + +postcss@^8.4.43: + version "8.5.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" + integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== + dependencies: + nanoid "^3.3.11" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +printable-characters@^1.0.42: + version "1.0.42" + resolved "https://registry.yarnpkg.com/printable-characters/-/printable-characters-1.0.42.tgz#3f18e977a9bd8eb37fcc4ff5659d7be90868b3d8" + integrity sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ== + +psl@^1.1.28: + version "1.15.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.15.0.tgz#bdace31896f1d97cec6a79e8224898ce93d974c6" + integrity sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w== + dependencies: + punycode "^2.3.1" + +punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +qs@~6.5.2: + version "6.5.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== + +readable-stream@1.1: + version "1.1.13" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e" + integrity sha512-E98tWzqShvKDGpR2MbjsDkDQWLW2TfWUC15H4tNQhIJ5Lsta84l8nUGL9/ybltGwe+wZzWPpc1Kmd2wQP4bdCA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +"readable-stream@>=1.0.33-1 <1.1.0-0": + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + integrity sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^2.0.0, readable-stream@~2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +remove-svg-properties@^0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/remove-svg-properties/-/remove-svg-properties-0.3.4.tgz#52f10f0685a596def822b5619d0b698578899574" + integrity sha512-y7neLGtA2egeW9JjmtdKJ1PXNVWsM9ZzJQWRNGQSQvmmXLKh7VS0nEVvoEqHdwLFyvY0KGO7pHxPIKjxUoJdhQ== + dependencies: + cheerio "^0.19.0" + colors "^1.1.0" + css "^2.2.0" + graceful-fs "^4.2.6" + juice "^1.0.0" + lodash "^3.5.0" + mkdirp "^0.5.0" + path "^0.11.14" + stream-consume "^0.1.0" + through2 "^0.6.3" + vinyl-fs "^1.0.0" + +rename-keys@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/rename-keys/-/rename-keys-1.2.0.tgz#be602fb0b750476b513ebe85ba4465d03254f0a3" + integrity sha512-U7XpAktpbSgHTRSNRrjKSrjYkZKuhUukfoBlXWXUExCAqhzh1TU3BDRAfJmarcl5voKS+pbKU9MvyLWKZ4UEEg== + +repeat-string@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== + +request@^2.49.0: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg== + +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" + integrity sha512-yqINtL/G7vs2v+dFIZmFUDbnVyFUJFKd6gK22Kgo6R4jfJGFtisKyncWDDULgjfqf4ASQuIQyjJ7XZ+3aWpsAg== + dependencies: + align-text "^0.1.1" + +rollup-plugin-inject@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz#e4233855bfba6c0c12a312fd6649dff9a13ee9f4" + integrity sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w== + dependencies: + estree-walker "^0.6.1" + magic-string "^0.25.3" + rollup-pluginutils "^2.8.1" + +rollup-plugin-node-polyfills@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz#53092a2744837164d5b8a28812ba5f3ff61109fd" + integrity sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA== + dependencies: + rollup-plugin-inject "^3.0.0" + +rollup-pluginutils@^2.8.1: + version "2.8.2" + resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" + integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ== + dependencies: + estree-walker "^0.6.1" + +rollup@^4.20.0: + version "4.52.4" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.52.4.tgz#71e64cce96a865fcbaa6bb62c6e82807f4e378a1" + integrity sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ== + dependencies: + "@types/estree" "1.0.8" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.52.4" + "@rollup/rollup-android-arm64" "4.52.4" + "@rollup/rollup-darwin-arm64" "4.52.4" + "@rollup/rollup-darwin-x64" "4.52.4" + "@rollup/rollup-freebsd-arm64" "4.52.4" + "@rollup/rollup-freebsd-x64" "4.52.4" + "@rollup/rollup-linux-arm-gnueabihf" "4.52.4" + "@rollup/rollup-linux-arm-musleabihf" "4.52.4" + "@rollup/rollup-linux-arm64-gnu" "4.52.4" + "@rollup/rollup-linux-arm64-musl" "4.52.4" + "@rollup/rollup-linux-loong64-gnu" "4.52.4" + "@rollup/rollup-linux-ppc64-gnu" "4.52.4" + "@rollup/rollup-linux-riscv64-gnu" "4.52.4" + "@rollup/rollup-linux-riscv64-musl" "4.52.4" + "@rollup/rollup-linux-s390x-gnu" "4.52.4" + "@rollup/rollup-linux-x64-gnu" "4.52.4" + "@rollup/rollup-linux-x64-musl" "4.52.4" + "@rollup/rollup-openharmony-arm64" "4.52.4" + "@rollup/rollup-win32-arm64-msvc" "4.52.4" + "@rollup/rollup-win32-ia32-msvc" "4.52.4" + "@rollup/rollup-win32-x64-gnu" "4.52.4" + "@rollup/rollup-win32-x64-msvc" "4.52.4" + fsevents "~2.3.2" + +safe-buffer@^5.0.1, safe-buffer@^5.1.2: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^7.5.1, semver@^7.6.3, semver@^7.7.2: + version "7.7.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" + integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== + +sharp@^0.33.5: + version "0.33.5" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.5.tgz#13e0e4130cc309d6a9497596715240b2ec0c594e" + integrity sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw== + dependencies: + color "^4.2.3" + detect-libc "^2.0.3" + semver "^7.6.3" + optionalDependencies: + "@img/sharp-darwin-arm64" "0.33.5" + "@img/sharp-darwin-x64" "0.33.5" + "@img/sharp-libvips-darwin-arm64" "1.0.4" + "@img/sharp-libvips-darwin-x64" "1.0.4" + "@img/sharp-libvips-linux-arm" "1.0.5" + "@img/sharp-libvips-linux-arm64" "1.0.4" + "@img/sharp-libvips-linux-s390x" "1.0.4" + "@img/sharp-libvips-linux-x64" "1.0.4" + "@img/sharp-libvips-linuxmusl-arm64" "1.0.4" + "@img/sharp-libvips-linuxmusl-x64" "1.0.4" + "@img/sharp-linux-arm" "0.33.5" + "@img/sharp-linux-arm64" "0.33.5" + "@img/sharp-linux-s390x" "0.33.5" + "@img/sharp-linux-x64" "0.33.5" + "@img/sharp-linuxmusl-arm64" "0.33.5" + "@img/sharp-linuxmusl-x64" "0.33.5" + "@img/sharp-wasm32" "0.33.5" + "@img/sharp-win32-ia32" "0.33.5" + "@img/sharp-win32-x64" "0.33.5" + +sharp@^0.34.3: + version "0.34.4" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.34.4.tgz#73c2c5a425e98250b8b927e5537f711da8966e38" + integrity sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA== + dependencies: + "@img/colour" "^1.0.0" + detect-libc "^2.1.0" + semver "^7.7.2" + optionalDependencies: + "@img/sharp-darwin-arm64" "0.34.4" + "@img/sharp-darwin-x64" "0.34.4" + "@img/sharp-libvips-darwin-arm64" "1.2.3" + "@img/sharp-libvips-darwin-x64" "1.2.3" + "@img/sharp-libvips-linux-arm" "1.2.3" + "@img/sharp-libvips-linux-arm64" "1.2.3" + "@img/sharp-libvips-linux-ppc64" "1.2.3" + "@img/sharp-libvips-linux-s390x" "1.2.3" + "@img/sharp-libvips-linux-x64" "1.2.3" + "@img/sharp-libvips-linuxmusl-arm64" "1.2.3" + "@img/sharp-libvips-linuxmusl-x64" "1.2.3" + "@img/sharp-linux-arm" "0.34.4" + "@img/sharp-linux-arm64" "0.34.4" + "@img/sharp-linux-ppc64" "0.34.4" + "@img/sharp-linux-s390x" "0.34.4" + "@img/sharp-linux-x64" "0.34.4" + "@img/sharp-linuxmusl-arm64" "0.34.4" + "@img/sharp-linuxmusl-x64" "0.34.4" + "@img/sharp-wasm32" "0.34.4" + "@img/sharp-win32-arm64" "0.34.4" + "@img/sharp-win32-ia32" "0.34.4" + "@img/sharp-win32-x64" "0.34.4" + +siginfo@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" + integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== + +sigmund@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" + integrity sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g== + +simple-swizzle@^0.2.2: + version "0.2.4" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.4.tgz#a8d11a45a11600d6a1ecdff6363329e3648c3667" + integrity sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw== + dependencies: + is-arrayish "^0.3.1" + +slick@1.12.2: + version "1.12.2" + resolved "https://registry.yarnpkg.com/slick/-/slick-1.12.2.tgz#bd048ddb74de7d1ca6915faa4a57570b3550c2d7" + integrity sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A== + +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +source-map-resolve@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-url@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" + integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== + +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@~0.5.1: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + +sourcemap-codec@^1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +sshpk@^1.7.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028" + integrity sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +stackback@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" + integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== + +stacktracey@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/stacktracey/-/stacktracey-2.1.8.tgz#bf9916020738ce3700d1323b32bd2c91ea71199d" + integrity sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw== + dependencies: + as-table "^1.0.36" + get-source "^2.0.12" + +std-env@^3.8.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.10.0.tgz#d810b27e3a073047b2b5e40034881f5ea6f9c83b" + integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg== + +stoppable@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b" + integrity sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw== + +stream-consume@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/stream-consume/-/stream-consume-0.1.1.tgz#d3bdb598c2bd0ae82b8cac7ac50b1107a7996c48" + integrity sha512-tNa3hzgkjEP7XbCkbRXe1jpg+ievoa0O4SCFlMOYEscGSS4JJsckGL8swUyAa/ApGU3Ae4t6Honor4HhL+tRyg== + +stream-shift@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.3.tgz#85b8fab4d71010fc3ba8772e8046cc49b8a3864b" + integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-bom@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-1.0.0.tgz#85b8862f3844b5a6d5ec8467a93598173a36f794" + integrity sha512-qVAeAIjblKDp/8Cd0tJdxpe3Iq/HooI7En98alEaMbz4Wedlrcj3WI72dDQSrziRW5IQ0zeBo3JXsmS8RcS9jg== + dependencies: + first-chunk-stream "^1.0.0" + is-utf8 "^0.2.0" + +supports-color@^10.0.0: + version "10.2.2" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-10.2.2.tgz#466c2978cc5cd0052d542a0b576461c2b802ebb4" + integrity sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g== + +svgson@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/svgson/-/svgson-5.3.1.tgz#f3df0303231f2e99e4618983cfa7e8db155f79d7" + integrity sha512-qdPgvUNWb40gWktBJnbJRelWcPzkLed/ShhnRsjbayXz8OtdPOzbil9jtiZdrYvSDumAz/VNQr6JaNfPx/gvPA== + dependencies: + deep-rename-keys "^0.2.1" + xml-reader "2.4.3" + +templayed@*: + version "0.2.3" + resolved "https://registry.yarnpkg.com/templayed/-/templayed-0.2.3.tgz#4706df625bc6aecd86b7c9f6b0fb548b95cdf769" + integrity sha512-PkryVKPQ++1gp2sVUREJhGmtK2GF/DNEZ0KnGcam0dyD+XbFO+IWp9/vPIOfpLRzwW1SMrcr/5BkA0yMbNYPGw== + +through2-filter@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254" + integrity sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA== + dependencies: + through2 "~2.0.0" + xtend "~4.0.0" + +through2@^0.6.1, through2@^0.6.3: + version "0.6.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48" + integrity sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg== + dependencies: + readable-stream ">=1.0.33-1 <1.1.0-0" + xtend ">=4.0.0 <4.1.0-0" + +through2@~2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +timers-ext@0.1: + version "0.1.8" + resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.8.tgz#b4e442f10b7624a29dd2aa42c295e257150cf16c" + integrity sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww== + dependencies: + es5-ext "^0.10.64" + next-tick "^1.1.0" + +tinybench@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" + integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== + +tinyexec@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2" + integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== + +tinypool@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.1.1.tgz#059f2d042bd37567fbc017d3d426bdd2a2612591" + integrity sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg== + +tinyrainbow@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz#5c57d2fc0fb3d1afd78465c33ca885d04f02abb5" + integrity sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ== + +tinyspy@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a" + integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q== + +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tslib@^2.4.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + +type@^2.7.2: + version "2.7.3" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.3.tgz#436981652129285cc3ba94f392886c2637ea0486" + integrity sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ== + +ufo@^1.5.4, ufo@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.1.tgz#ac2db1d54614d1b22c1d603e3aef44a85d8f146b" + integrity sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA== + +uglify-js@^2.4.1: + version "2.8.29" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" + integrity sha512-qLq/4y2pjcU3vhlhseXGGJ7VbFO4pBANu0kwl8VCa9KEI0V8VfZIx2Fy3w01iSTA/pGwKZSmu/+I4etLNDdt5w== + dependencies: + source-map "~0.5.1" + yargs "~3.10.0" + optionalDependencies: + uglify-to-browserify "~1.0.0" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" + integrity sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q== + +undici@7.14.0: + version "7.14.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-7.14.0.tgz#7e616eeb3900deb1c4dda0e51384303975eec72c" + integrity sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ== + +undici@^5.28.4: + version "5.29.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.29.0.tgz#419595449ae3f2cdcba3580a2e8903399bd1f5a3" + integrity sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg== + dependencies: + "@fastify/busboy" "^2.0.0" + +unenv@2.0.0-rc.1: + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/unenv/-/unenv-2.0.0-rc.1.tgz#7299a1ae1613d441e207ced9a5ee6f90c40856af" + integrity sha512-PU5fb40H8X149s117aB4ytbORcCvlASdtF97tfls4BPIyj4PeVxvpSuy1jAptqYHqB0vb2w2sHvzM0XWcp2OKg== + dependencies: + defu "^6.1.4" + mlly "^1.7.4" + ohash "^1.1.4" + pathe "^1.1.2" + ufo "^1.5.4" + +unenv@2.0.0-rc.21: + version "2.0.0-rc.21" + resolved "https://registry.yarnpkg.com/unenv/-/unenv-2.0.0-rc.21.tgz#cc6082ef32370eb7098cb2f86ca7af3ef4c2c49f" + integrity sha512-Wj7/AMtE9MRnAXa6Su3Lk0LNCfqDYgfwVjwRFVum9U7wsto1imuHqk4kTm7Jni+5A0Hn7dttL6O/zjvUvoo+8A== + dependencies: + defu "^6.1.4" + exsolve "^1.0.7" + ohash "^2.0.11" + pathe "^2.0.3" + ufo "^1.6.1" + +unique-stream@^2.0.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.4.0.tgz#5d995309556b5ba324197a3f541d675a0a052ff4" + integrity sha512-V6QarSfeSgDipGA9EZdoIzu03ZDlOFkk+FbEP5cwgrZXN3iIkYR91IjU2EnM6rB835kGQsqHX8qncObTXV+6KA== + dependencies: + json-stable-stringify-without-jsonify "^1.0.1" + through2-filter "3.0.0" + +upng-js@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/upng-js/-/upng-js-2.1.0.tgz#7176e73973db361ca95d0fa14f958385db6b9dd2" + integrity sha512-d3xzZzpMP64YkjP5pr8gNyvBt7dLk/uGI67EctzDuVp4lCZyVMo0aJO6l/VDlgbInJYDY6cnClLoBp29eKWI6g== + dependencies: + pako "^1.0.5" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg== + +util-deprecate@^1.0.2, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vinyl-fs@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-1.0.0.tgz#d15752e68c2dad74364e7e853473735354692edf" + integrity sha512-FETN6RUe6r2Dbrhq1GxovuyHf2G5ub6NO1HFtz6nMY21YQzTQhHPARcyzJWPUohjG2eCaUOyoTxnNyEtOtCI1Q== + dependencies: + duplexify "^3.2.0" + glob-stream "^4.0.1" + glob-watcher "^0.0.8" + graceful-fs "^3.0.0" + merge-stream "^0.1.7" + mkdirp "^0.5.0" + object-assign "^2.0.0" + strip-bom "^1.0.0" + through2 "^0.6.1" + vinyl "^0.4.0" + +vinyl@^0.4.0: + version "0.4.6" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.4.6.tgz#2f356c87a550a255461f36bbeb2a5ba8bf784847" + integrity sha512-pmza4M5VA15HOImIQYWhoXGlGNafCm0QK5BpBUXkzzEwrRxKqBsbAhTfkT2zMcJhUX1G1Gkid0xaV8WjOl7DsA== + dependencies: + clone "^0.2.0" + clone-stats "^0.0.1" + +vite-node@2.1.9: + version "2.1.9" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.9.tgz#549710f76a643f1c39ef34bdb5493a944e4f895f" + integrity sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA== + dependencies: + cac "^6.7.14" + debug "^4.3.7" + es-module-lexer "^1.5.4" + pathe "^1.1.2" + vite "^5.0.0" + +vite@^5.0.0: + version "5.4.20" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.20.tgz#3267a5e03f21212f44edfd72758138e8fcecd76a" + integrity sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g== + dependencies: + esbuild "^0.21.3" + postcss "^8.4.43" + rollup "^4.20.0" + optionalDependencies: + fsevents "~2.3.3" + +vitest@~2.1.9: + version "2.1.9" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.9.tgz#7d01ffd07a553a51c87170b5e80fea3da7fb41e7" + integrity sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q== + dependencies: + "@vitest/expect" "2.1.9" + "@vitest/mocker" "2.1.9" + "@vitest/pretty-format" "^2.1.9" + "@vitest/runner" "2.1.9" + "@vitest/snapshot" "2.1.9" + "@vitest/spy" "2.1.9" + "@vitest/utils" "2.1.9" + chai "^5.1.2" + debug "^4.3.7" + expect-type "^1.1.0" + magic-string "^0.30.12" + pathe "^1.1.2" + std-env "^3.8.0" + tinybench "^2.9.0" + tinyexec "^0.3.1" + tinypool "^1.0.1" + tinyrainbow "^1.2.0" + vite "^5.0.0" + vite-node "2.1.9" + why-is-node-running "^2.3.0" + +web-resource-inliner@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/web-resource-inliner/-/web-resource-inliner-1.2.1.tgz#c02332ad985ed00da4c2310c0699fb2e5ba9675f" + integrity sha512-0532DtlDjxRLTjJebh9o41hoPATspwJOWSN3/eSgtRZVVeU/6NiEo+oMrtcivAYhVGFvVChOy6dniRIkc5IeaQ== + dependencies: + async "^0.9.0" + clean-css "1.1.7" + cli-color "^0.3.2" + datauri "~0.2.0" + lodash "^3.10.1" + request "^2.49.0" + uglify-js "^2.4.1" + xtend "^4.0.0" + +which@^1.2.8: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +why-is-node-running@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04" + integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== + dependencies: + siginfo "^2.0.0" + stackback "0.0.2" + +window-size@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" + integrity sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg== + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + integrity sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q== + +workerd@1.20250204.0: + version "1.20250204.0" + resolved "https://registry.yarnpkg.com/workerd/-/workerd-1.20250204.0.tgz#fffc680dbc5589cba6dba33877d68f00a226e779" + integrity sha512-zcKufjVFsQMiD3/acg1Ix00HIMCkXCrDxQXYRDn/1AIz3QQGkmbVDwcUk1Ki2jBUoXmBCMsJdycRucgMVEypWg== + optionalDependencies: + "@cloudflare/workerd-darwin-64" "1.20250204.0" + "@cloudflare/workerd-darwin-arm64" "1.20250204.0" + "@cloudflare/workerd-linux-64" "1.20250204.0" + "@cloudflare/workerd-linux-arm64" "1.20250204.0" + "@cloudflare/workerd-windows-64" "1.20250204.0" + +workerd@1.20251008.0: + version "1.20251008.0" + resolved "https://registry.yarnpkg.com/workerd/-/workerd-1.20251008.0.tgz#622d61520fa95b3a98d96eed63ec0f701641ad6c" + integrity sha512-HwaJmXO3M1r4S8x2ea2vy8Rw/y/38HRQuK/gNDRQ7w9cJXn6xSl1sIIqKCffULSUjul3wV3I3Nd/GfbmsRReEA== + optionalDependencies: + "@cloudflare/workerd-darwin-64" "1.20251008.0" + "@cloudflare/workerd-darwin-arm64" "1.20251008.0" + "@cloudflare/workerd-linux-64" "1.20251008.0" + "@cloudflare/workerd-linux-arm64" "1.20251008.0" + "@cloudflare/workerd-windows-64" "1.20251008.0" + +wrangler@3.109.1: + version "3.109.1" + resolved "https://registry.yarnpkg.com/wrangler/-/wrangler-3.109.1.tgz#92191865bc4e593540e412434f1e0b7b880c460a" + integrity sha512-1Jx+nZ6eCXPQ2rsGdrV6Qy/LGvhpqudeuTl4AYHl9P8Zugp44Uzxnj5w11qF4v/rv1dOZoA5TydSt9xMFfhpKg== + dependencies: + "@cloudflare/kv-asset-handler" "0.3.4" + "@esbuild-plugins/node-globals-polyfill" "0.2.3" + "@esbuild-plugins/node-modules-polyfill" "0.2.2" + blake3-wasm "2.1.5" + esbuild "0.17.19" + miniflare "3.20250204.1" + path-to-regexp "6.3.0" + unenv "2.0.0-rc.1" + workerd "1.20250204.0" + optionalDependencies: + fsevents "~2.3.2" + sharp "^0.33.5" + +wrangler@^4.38.0: + version "4.43.0" + resolved "https://registry.yarnpkg.com/wrangler/-/wrangler-4.43.0.tgz#cadd8703fef40f71118f0646c7d72ee2531dc2ef" + integrity sha512-IBNqXlYHSUSCNNWj/tQN4hFiQy94l7fTxEnJWETXyW69+cjUyjQ7MfeoId3vIV9KBgY8y5M5uf2XulU95OikJg== + dependencies: + "@cloudflare/kv-asset-handler" "0.4.0" + "@cloudflare/unenv-preset" "2.7.7" + blake3-wasm "2.1.5" + esbuild "0.25.4" + miniflare "4.20251008.0" + path-to-regexp "6.3.0" + unenv "2.0.0-rc.21" + workerd "1.20251008.0" + optionalDependencies: + fsevents "~2.3.2" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + +xml-lexer@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/xml-lexer/-/xml-lexer-0.2.2.tgz#518193a4aa334d58fc7d248b549079b89907e046" + integrity sha512-G0i98epIwiUEiKmMcavmVdhtymW+pCAohMRgybyIME9ygfVu8QheIi+YoQh3ngiThsT0SQzJT4R0sKDEv8Ou0w== + dependencies: + eventemitter3 "^2.0.0" + +xml-reader@2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/xml-reader/-/xml-reader-2.4.3.tgz#9f810caf7c425a5aafb848b1c45103c9e71d7530" + integrity sha512-xWldrIxjeAMAu6+HSf9t50ot1uL5M+BtOidRCWHXIeewvSeIpscWCsp4Zxjk8kHHhdqFBrfK8U0EJeCcnyQ/gA== + dependencies: + eventemitter3 "^2.0.0" + xml-lexer "^0.2.2" + +"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A== + +yargs@~3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" + integrity sha512-QFzUah88GAGy9lyDKGBqZdkYApt63rCXYBGYnEP4xDJPXNqXXnBDACnbrXnViV6jRSqAePwrATi2i8mfYm4L1A== + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "0.1.0" + +youch-core@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/youch-core/-/youch-core-0.3.3.tgz#c5d3d85aeea0d8bc7b36e9764ed3f14b7ceddc7d" + integrity sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA== + dependencies: + "@poppinss/exception" "^1.2.2" + error-stack-parser-es "^1.0.5" + +youch@3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/youch/-/youch-3.2.3.tgz#63c94ea504950a1a5bf1d5969439addba6c726e2" + integrity sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw== + dependencies: + cookie "^0.5.0" + mustache "^4.2.0" + stacktracey "^2.1.8" + +youch@4.1.0-beta.10: + version "4.1.0-beta.10" + resolved "https://registry.yarnpkg.com/youch/-/youch-4.1.0-beta.10.tgz#94702059e0ba7668025f5cd1b5e5c0f3eb0e83c2" + integrity sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ== + dependencies: + "@poppinss/colors" "^4.1.5" + "@poppinss/dumper" "^0.6.4" + "@speed-highlight/core" "^1.2.7" + cookie "^1.0.2" + youch-core "^0.3.3" + +zod@3.22.3: + version "3.22.3" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.3.tgz#2fbc96118b174290d94e8896371c95629e87a060" + integrity sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug== + +zod@^3.22.3: + version "3.25.76" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" + integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==