From 26d4e08754609491014ae9d30f56a79c3dde379c Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Thu, 31 Jul 2025 11:32:24 -0400 Subject: [PATCH 01/27] feat: Add basic purchase page for accessible escape room with form handling and Stripe integration - Implemented the purchase page layout and form for selecting kit type and theme. - Integrated Stripe for payment processing with a backend API for creating checkout sessions. - Added validation for form inputs and error handling for payment processing. - Created Azure functions for handling checkout session creation and retrieval. - Implemented storage of purchase data in Azure Table Storage with proof code generation. - Added environment configuration example for Stripe and Azure settings. --- .../pages/services/escape-room/index.astro | 3 + .../escape-room/purchase-success.astro | 663 ++++++++++++++++++ .../services/escape-room/purchase/index.astro | 396 +++++++++++ azure/create-checkout-session.js | 152 ++++ azure/example.env | 13 + azure/get-checkout-session.js | 52 ++ azure/pop-code-gen.js | 71 ++ azure/save-purchase-data.js | 83 +++ 8 files changed, 1433 insertions(+) create mode 100644 astro/src/pages/services/escape-room/purchase-success.astro create mode 100644 astro/src/pages/services/escape-room/purchase/index.astro create mode 100644 azure/create-checkout-session.js create mode 100644 azure/example.env create mode 100644 azure/get-checkout-session.js create mode 100644 azure/pop-code-gen.js create mode 100644 azure/save-purchase-data.js diff --git a/astro/src/pages/services/escape-room/index.astro b/astro/src/pages/services/escape-room/index.astro index 05f07697..de55a4c1 100644 --- a/astro/src/pages/services/escape-room/index.astro +++ b/astro/src/pages/services/escape-room/index.astro @@ -123,6 +123,9 @@ import { Icon } from "astro-icon/components"; descriptions. + +
+
+
+
+

+ Purchase Successful! +

+

+ Thank you for your purchase of the Accessible Escape Room kit. +

+
+
+
+
+ + + +
+
+
+

Your Purchase Details

+

+ Save this information for your records and to access your kit materials. +

+
+ + +
+
+

+ Purchase Summary +

+
+
+
+
+ Loading purchase details... +
+

Retrieving your purchase information...

+
+ + + + +
+
+ + +
+
+
+
+

Access Your Kit

+

+ Use your proof of purchase code to access digital materials and instructions. +

+ +
+
+
+
+
+
+

Customer Portal

+

+ View all your purchases and manage your account in one place. +

+ + View All Purchases + +
+
+
+
+ + +
+
+

Email Confirmation

+

+ A confirmation email with your purchase details and proof of purchase code + has been sent to your email address. +

+ + Don't see it? Check your spam folder or contact support if you need assistance. + +
+
+
+
+
+ + + +
+

Need Help?

+

+ Our support team is here to help you get the most out of your escape room kit. +

+
+
+
+
+
Email Support
+

escaperoom@accessiblecommunity.org

+
+
+
Documentation
+

Access setup guides and FAQs

+
+
+
Community
+

Connect with other kit owners

+
+
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/astro/src/pages/services/escape-room/purchase/index.astro b/astro/src/pages/services/escape-room/purchase/index.astro new file mode 100644 index 00000000..3d3801a4 --- /dev/null +++ b/astro/src/pages/services/escape-room/purchase/index.astro @@ -0,0 +1,396 @@ +--- +import { getOpenGraphImageData } from "@lib/og-image"; +import type { Breadcrumbs, PageMetadata } from "@lib/types"; + +const title = "Purchase - The Accessible Escape Room"; +const metadata: PageMetadata = { + title, + description: "Purchase your accessible escape room kit", + image: getOpenGraphImageData(Astro.site, "pages", "escape-room-purchase"), +}; +const crumbs: Breadcrumbs = [ + { name: "Home", href: "/" }, + { name: "Services", href: "/services" }, + { name: "Escape Room", href: "/services/escape-room" }, +]; + +import Branding from "@components/Branding.astro"; +import Layout from "src/layouts/Layout.astro"; +import ThemedBox from "@components/ThemedBox.astro"; +import ThemedSection from "@components/ThemedSection.astro"; +import { Icon } from "astro-icon/components"; +--- + + +
+
+
+
+

+ Purchase Your Accessible Escape Room +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. +

+
+
+
+
+ + + +
+
+
+

Purchase Options

+

+ Select your kit type and theme below +

+
+ + +
+
+

Purchase Information

+
+
+
+ + +
+ Kit Type * +
+ + +
+
+ + +
+ +
+ + +
+ Choose Your Theme * +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+ + +
+
+ + + +
+
+ + + +
+
+ + +
+ + + +
+ + +
+ + +
+ Optional: Any specific needs or customizations +
+
+ +
+ +
+
+ + + + +
+
+
+
+
+ + + +
+

Ready-Made Kits

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit

+ +
+
+
+
+

Coming Soon

+

+ Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation. +

+
$3,500
+

Available later this year

+ + Get Notified + +
+ Email us to be notified when ready-made kits become available +
+
+
+
+
+
+
+ + + + + + +
\ No newline at end of file diff --git a/azure/create-checkout-session.js b/azure/create-checkout-session.js new file mode 100644 index 00000000..622d0ead --- /dev/null +++ b/azure/create-checkout-session.js @@ -0,0 +1,152 @@ +const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); + +// Helper function to add standard headers +function withHeaders(body, status = 200) { + return { + status, + headers: { + 'Access-Control-Allow-Origin': process.env.ALLOWED_ORIGIN || '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + 'Content-Type': 'application/json' + }, + body + }; +} + +module.exports = async function (context, req) { + context.log('Escape Room checkout function started'); + + // Handle CORS preflight + if (req.method === 'OPTIONS') { + context.res = withHeaders(null, 200); + return; + } + + try { + const { items, customerEmail, returnUrl, shippingAddress, metadata } = req.body; + + context.log('Processing checkout for:', { + itemsCount: items?.length, + customerEmail, + hasShipping: !!shippingAddress, + metadata + }); + + // Validate required fields + if (!items || !Array.isArray(items) || items.length === 0) { + context.res = withHeaders({ + error: 'Invalid items data' + }, 400); + return; + } + + if (!customerEmail) { + context.res = withHeaders({ + error: 'Customer email is required' + }, 400); + return; + } + + if (!returnUrl) { + context.res = withHeaders({ + error: 'Return URL is required' + }, 400); + return; + } + + // Build line items for Stripe + const lineItems = items.map(item => ({ + price_data: { + currency: 'usd', + product_data: { + name: item.name, + description: item.description || '', + metadata: { + kit_version: item.version || 'standard', + product_type: 'escape_room_kit' + } + }, + unit_amount: Math.round(item.price * 100), // Convert to cents + }, + quantity: item.quantity || 1, + })); + + // Create session config + const sessionConfig = { + ui_mode: 'embedded', + line_items: lineItems, + mode: 'payment', + return_url: `${returnUrl}?session_id={CHECKOUT_SESSION_ID}`, + customer_email: customerEmail, + metadata: { + source: 'escape_room_kit_purchase', + timestamp: new Date().toISOString(), + order_type: 'physical_product', + // Add custom metadata from the form + ...(metadata && { + kitType: metadata.kitType, + theme: metadata.theme, + organization: metadata.organization, + contactName: metadata.contactName, + specialRequirements: metadata.specialRequirements || '' + }) + } + }; + + // Add shipping if requested + if (shippingAddress) { + sessionConfig.shipping_address_collection = { + allowed_countries: ['US', 'CA'], + }; + + sessionConfig.shipping_options = [ + { + shipping_rate_data: { + type: 'fixed_amount', + fixed_amount: { + amount: 999, // $9.99 in cents + currency: 'usd', + }, + display_name: 'Standard Shipping', + delivery_estimate: { + minimum: { unit: 'business_day', value: 5 }, + maximum: { unit: 'business_day', value: 7 }, + }, + }, + }, + { + shipping_rate_data: { + type: 'fixed_amount', + fixed_amount: { + amount: 1999, // $19.99 in cents + currency: 'usd', + }, + display_name: 'Express Shipping', + delivery_estimate: { + minimum: { unit: 'business_day', value: 2 }, + maximum: { unit: 'business_day', value: 3 }, + }, + }, + }, + ]; + } + + // Create the Stripe session + const session = await stripe.checkout.sessions.create(sessionConfig); + + context.log('Stripe session created successfully:', session.id); + + context.res = withHeaders({ + clientSecret: session.client_secret, + sessionId: session.id + }, 200); + + } catch (error) { + context.log.error('Stripe error:', error.message); + context.res = withHeaders({ + error: 'Failed to create checkout session', + details: error.message + }, 500); + } +}; \ No newline at end of file diff --git a/azure/example.env b/azure/example.env new file mode 100644 index 00000000..e095f970 --- /dev/null +++ b/azure/example.env @@ -0,0 +1,13 @@ +# Stripe Keys +STRIPE_SECRET_KEY= +PUBLIC_STRIPE_PUBLISHABLE_KEY= + +PUBLIC_AZURE_CHECKOUT_URL= +PUBLIC_AZURE_GET_CHECKOUT_URL= +PUBLIC_AZURE_PROOF_GENERATOR_URL= +PUBLIC_AZURE_SAVE_URL= + +API_SECRET_KEY=secret + +# Optional: CORS origin for Azure Function +ALLOWED_ORIGIN= diff --git a/azure/get-checkout-session.js b/azure/get-checkout-session.js new file mode 100644 index 00000000..abfbfa65 --- /dev/null +++ b/azure/get-checkout-session.js @@ -0,0 +1,52 @@ + +const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); + +function withHeaders(body, status = 200) { + return { + status, + headers: { + 'Access-Control-Allow-Origin': process.env.ALLOWED_ORIGIN || '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + 'Content-Type': 'application/json' + }, + body + }; +} + +module.exports = async function (context, req) { + try { + context.log('Function get-checkout-session started'); + + // Handle CORS preflight requests + if (req.method === 'OPTIONS') { + context.res = withHeaders(null, 200); + return; + } + + const { sessionId } = req.body || {}; + + context.log('Received sessionId:', sessionId); + + if (!sessionId) { + context.res = withHeaders({ error: 'sessionId is required' }, 400); + return; + } + + // Retrieve the Stripe checkout session with expanded line_items and customer_details + const session = await stripe.checkout.sessions.retrieve(sessionId, { + expand: ['line_items', 'customer_details'] + }); + + context.log('Stripe session retrieved:', session.id); + + // Return the full session object to the frontend + context.res = withHeaders(session); + } catch (error) { + context.log.error('Error retrieving Stripe session:', error); + context.res = withHeaders({ + error: error.message, + stack: error.stack + }, 500); + } +}; \ No newline at end of file diff --git a/azure/pop-code-gen.js b/azure/pop-code-gen.js new file mode 100644 index 00000000..1fbf2c04 --- /dev/null +++ b/azure/pop-code-gen.js @@ -0,0 +1,71 @@ +const { TableClient } = require("@azure/data-tables"); +const crypto = require('crypto'); +const { v4: uuidv4 } = require('uuid'); + +module.exports = async function (context, req) { + try { + // Validate input + const sessionId = req.body.sessionId; + const purchaseData = req.body.purchaseData; + + if (!sessionId || typeof sessionId !== 'string' || sessionId.length < 10) { + context.res = { + status: 400, + body: JSON.stringify({ error: "Invalid session ID format" }) + }; + return; + } + + // Generate secure proof code + const proofCode = generateSecureProofCode(); + + // Store in Azure Table + await storeInTableStorage(sessionId, purchaseData, proofCode); + + context.res = { + status: 200, + body: JSON.stringify({ proofCode }), + headers: { "Content-Type": "application/json" } + }; + } catch (error) { + context.res = { + status: 500, + body: JSON.stringify({ error: error.message }) + }; + } +}; + +function generateSecureProofCode() { + // Generate crypto-secure token + const randomBytes = crypto.randomBytes(24); + const token = randomBytes.toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + + return `AER-${token}`; +} + +async function storeInTableStorage(sessionId, purchaseData, proofCode) { + const connectionString = process.env.AZURE_STORAGE_CONNECTION_STRING; + const tableName = "PurchaseProofs"; + const client = TableClient.fromConnectionString(connectionString, tableName); + + // Create table if not exists + try { + await client.createTable(); + } catch (e) { + if (e.statusCode !== 409) throw e; // Ignore "already exists" errors + } + + const entity = { + partitionKey: "proof", + rowKey: proofCode, // Use proofCode as rowKey for fast lookup + sessionId, + purchaseData: JSON.stringify(purchaseData), + createdAt: new Date().toISOString(), + expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString() // 1 year + }; + + await client.upsertEntity(entity, "Replace"); +} \ No newline at end of file diff --git a/azure/save-purchase-data.js b/azure/save-purchase-data.js new file mode 100644 index 00000000..b8e9766a --- /dev/null +++ b/azure/save-purchase-data.js @@ -0,0 +1,83 @@ +const { TableClient } = require("@azure/data-tables"); + +module.exports = async function (context, req) { + context.log('=== save-purchase-data START ==='); + + const connectionString = process.env["AZURE_STORAGE_CONNECTION_STRING"]; + if (!connectionString) { + context.log.error("Missing AZURE_STORAGE_CONNECTION_STRING"); + context.res = { status: 500, body: "ERROR: AZURE_STORAGE_CONNECTION_STRING not configured." }; + context.log('=== save-purchase-data END ==='); + return; + } + context.log("Connection string OK"); + + let client; + try { + client = TableClient.fromConnectionString(connectionString, "Purchases"); + context.log("TableClient created"); + } catch (err) { + context.log.error("TableClient creation FAILED:", err.message); + context.res = { status: 500, body: "ERROR: Could not create TableClient." }; + context.log('=== save-purchase-data END ==='); + return; + } + + const purchase = req.body; + context.log("Request body:", JSON.stringify(purchase)); + + if (!purchase || typeof purchase !== "object") { + context.res = { status: 400, body: "ERROR: Request body must be JSON." }; + context.log('=== save-purchase-data END ==='); + return; + } + + if (!purchase.id || !purchase.customerEmail) { + context.res = { status: 400, body: "ERROR: 'id' and 'customerEmail' fields are required." }; + context.log('=== save-purchase-data END ==='); + return; + } + + const entity = { + partitionKey: String(purchase.customerEmail), // lowercase 'p' + rowKey: String(purchase.id), // lowercase 'r' + paymentStatus: purchase.paymentStatus || "unknown", + amountTotal: purchase.amountTotal ?? 0, + currency: purchase.currency || "usd", + created: purchase.created || new Date().toISOString() + }; + + context.log("Prepared entity:", JSON.stringify(entity)); + context.log(`PartitionKey: '${entity.PartitionKey}' (type: ${typeof entity.PartitionKey})`); + context.log(`RowKey: '${entity.RowKey}' (type: ${typeof entity.RowKey})`); + context.log("Entity keys:", Object.keys(entity)); + + // Simple test upsert to isolate problem + try { + await client.upsertEntity({ + partitionKey: "test-partition", // lowercase + rowKey: "test-row" // lowercase + }, "Merge"); + context.log("Simple test upsert succeeded"); + } catch (err) { + context.log.error("Simple test upsert FAILED:", err.message); + } + + try { + // Try with forced serialization as a last resort + await client.upsertEntity(JSON.parse(JSON.stringify(entity)), "Merge"); + context.log("Upsert successful"); + context.res = { + status: 200, + body: { message: "Purchase saved.", entity } + }; + } catch (err) { + context.log.error("Upsert FAILED:", err.message); + context.res = { + status: 500, + body: "ERROR: Failed to save purchase." + }; + } + + context.log('=== save-purchase-data END ==='); +}; From 863eb85b40ae8a5c08d6f606bc6057a6c0574e2a Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Thu, 21 Aug 2025 10:06:43 -0400 Subject: [PATCH 02/27] feat: Implement Stripe API integration for session status and webhook handling - Added session-status API to retrieve payment status using Stripe Checkout session ID. - Implemented Stripe webhook to handle checkout session completion and store purchase data. - Created verify-purchase API to validate purchase codes and manage user sessions. - Developed access.astro page for users to access their purchased escape room kits. - Configured Netlify deployment settings and added necessary environment variables. - Updated package.json and package-lock.json to include Stripe and UUID dependencies. feat: Basic implementation of post-purchase page according to Figma --- .gitignore | 24 +- astro/astro.config.mjs | 5 +- astro/public/robots.txt | 14 + astro/src/middleware.ts | 18 + .../src/pages/api/create-checkout-session.ts | 85 +++ astro/src/pages/api/digital-content.ts | 482 ++++++++++++++++ astro/src/pages/api/download-material.ts | 127 +++++ .../src/pages/api/generate-download-token.ts | 75 +++ .../src/pages/api/send-confirmation-email.ts | 72 +++ astro/src/pages/api/session-status.ts | 40 ++ astro/src/pages/api/stripe-webhook.ts | 117 ++++ astro/src/pages/api/verify-purchase.ts | 127 +++++ .../pages/services/escape-room/access.astro | 539 ++++++++++++++++++ .../pages/services/escape-room/index.astro | 17 +- .../escape-room/purchase-success.astro | 262 ++------- .../services/escape-room/purchase/index.astro | 152 ++--- netlify.toml | 20 + package-lock.json | 309 ++++++++++ package.json | 6 + 19 files changed, 2183 insertions(+), 308 deletions(-) create mode 100644 astro/public/robots.txt create mode 100644 astro/src/middleware.ts create mode 100644 astro/src/pages/api/create-checkout-session.ts create mode 100644 astro/src/pages/api/digital-content.ts create mode 100644 astro/src/pages/api/download-material.ts create mode 100644 astro/src/pages/api/generate-download-token.ts create mode 100644 astro/src/pages/api/send-confirmation-email.ts create mode 100644 astro/src/pages/api/session-status.ts create mode 100644 astro/src/pages/api/stripe-webhook.ts create mode 100644 astro/src/pages/api/verify-purchase.ts create mode 100644 astro/src/pages/services/escape-room/access.astro create mode 100644 netlify.toml create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore index 496ee2ca..f2295288 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,23 @@ -.DS_Store \ No newline at end of file +# System files +.DS_Store + +# Node.js dependencies +node_modules/ + +# Astro build outputs +dist/ +.astro/ + +# Environment variables +.env + +# Local development files +astro/local-dev/ + +# Protected materials +astro/src/protected-materials/ + +# Test files and debug content +**/test*.astro +**/debug*.astro +**/*-test.* \ No newline at end of file diff --git a/astro/astro.config.mjs b/astro/astro.config.mjs index 002e8846..0120908a 100644 --- a/astro/astro.config.mjs +++ b/astro/astro.config.mjs @@ -33,6 +33,7 @@ const botsToDisallow = [ // https://astro.build/config export default defineConfig({ site: "https://accessiblecommunity.org", + output: "server", server: { host: true, }, @@ -63,13 +64,15 @@ export default defineConfig({ // Bootstrap Icons bi: [ // Navigation - 'arrow-down-square', 'arrow-up-right-square', 'list', + 'arrow-down-square', 'arrow-up-right-square', 'arrow-right-square', 'list', // Contact Info 'envelope-at-fill', 'telephone-fill', 'geo-alt-fill', // Social Media 'facebook', 'instagram', 'linkedin', 'rss-fill', 'tiktok', 'youtube','globe', 'mastodon', 'twitter', // Descriptive 'gift-fill', 'pencil-fill', 'people-fill', 'person-fill', + // Additional icons + 'check-circle-fill', 'exclamation-triangle-fill', 'file-text-fill', 'display-fill', 'puzzle-fill', 'tools', ], // CoreUI Brands cib: [ diff --git a/astro/public/robots.txt b/astro/public/robots.txt new file mode 100644 index 00000000..9dc69c47 --- /dev/null +++ b/astro/public/robots.txt @@ -0,0 +1,14 @@ +User-agent: * +Allow: / + +# Protect premium materials from crawling +Disallow: /materials/premium/ +Disallow: /protected-materials/ +Disallow: /api/download-material +Disallow: /api/digital-content +Disallow: /api/verify-purchase + +# Allow basic materials +Allow: /materials/basic/ + +Sitemap: https://accessiblecommunity.github.io/sitemap.xml diff --git a/astro/src/middleware.ts b/astro/src/middleware.ts new file mode 100644 index 00000000..af8ddfc6 --- /dev/null +++ b/astro/src/middleware.ts @@ -0,0 +1,18 @@ +import type { MiddlewareHandler } from 'astro'; + +export const onRequest: MiddlewareHandler = ({ url }, next) => { + // Block direct access to protected materials + if (url.pathname.startsWith('/materials/premium/') || + url.pathname.startsWith('/protected-materials/')) { + return new Response('Unauthorized', { + status: 401, + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'X-Robots-Tag': 'noindex, nofollow' + } + }); + } + + // Continue to the next middleware or route + return next(); +}; diff --git a/astro/src/pages/api/create-checkout-session.ts b/astro/src/pages/api/create-checkout-session.ts new file mode 100644 index 00000000..626a81b4 --- /dev/null +++ b/astro/src/pages/api/create-checkout-session.ts @@ -0,0 +1,85 @@ +import type { APIRoute } from 'astro'; +import Stripe from 'stripe'; +import { v4 as uuidv4 } from 'uuid'; + +const stripe = new Stripe(import.meta.env.STRIPE_SECRET_KEY); + +export const POST: APIRoute = async ({ request }) => { + try { + const body = await request.json(); + const { kitType, theme, organization, contactName, email, specialRequirements } = body; + + // Validate required fields + if (!kitType || !theme || !organization || !contactName || !email) { + return new Response( + JSON.stringify({ error: 'Missing required fields' }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + // Generate unique purchase code + const purchaseCode = `ESC-${uuidv4().split('-')[0].toUpperCase()}`; + + // Determine price based on kit type + const price = kitType === 'build' ? 50000 : 350000; // Stripe uses cents + const kitTypeName = kitType === 'build' ? 'Build-your-own Kit' : 'Ready-made Kit'; + + // Map theme values to display names + const themeNames: Record = { + 'corporate': 'Corporate Conundrum', + 'baking': 'Baking Bonanza', + 'picnic': 'Puzzling Picnic', + 'casino': 'Cryptic Casino' + }; + + const themeName = themeNames[theme] || theme; + + // Create Stripe checkout session with embedded checkout + const session = await stripe.checkout.sessions.create({ + ui_mode: 'embedded', + line_items: [ + { + price_data: { + currency: 'usd', + product_data: { + name: `${kitTypeName} - ${themeName}`, + description: `Accessible Escape Room Kit for ${organization}`, + }, + unit_amount: price, + }, + quantity: 1, + }, + ], + mode: 'payment', + return_url: `${new URL(request.url).origin}/services/escape-room/purchase-success?session_id={CHECKOUT_SESSION_ID}`, + customer_email: email, + metadata: { + purchaseCode, + kitType, + theme, + organization, + contactName, + email, + specialRequirements: specialRequirements || '', + }, + }); + + return new Response( + JSON.stringify({ + clientSecret: session.client_secret, + purchaseCode + }), + { + status: 200, + headers: { 'Content-Type': 'application/json' } + } + ); + + } catch (error) { + console.error('Error creating checkout session:', error); + return new Response( + JSON.stringify({ error: 'Failed to create checkout session' }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } +}; diff --git a/astro/src/pages/api/digital-content.ts b/astro/src/pages/api/digital-content.ts new file mode 100644 index 00000000..2808583f --- /dev/null +++ b/astro/src/pages/api/digital-content.ts @@ -0,0 +1,482 @@ +import type { APIRoute } from 'astro'; +import { sessions } from './verify-purchase'; + +export const GET: APIRoute = async ({ request, url, clientAddress }) => { + try { + const searchParams = url.searchParams; + const sessionId = searchParams.get('session'); + + if (!sessionId) { + return new Response('Unauthorized', { status: 401 }); + } + + // Also check for session cookie + const cookieHeader = request.headers.get('cookie'); + const sessionFromCookie = cookieHeader?.split(';') + .find(c => c.trim().startsWith('session=')) + ?.split('=')[1]; + + // Session must match both URL parameter and cookie + if (sessionFromCookie !== sessionId) { + return new Response('Unauthorized - Session mismatch', { status: 401 }); + } + + // Verify session + const sessionData = sessions.get(sessionId); + + if (!sessionData || Date.now() > sessionData.expiresAt) { + if (sessionData) { + sessions.delete(sessionId); // Clean up expired session + } + return new Response('Session expired', { status: 401 }); + } + + // Verify browser fingerprint + const userAgent = request.headers.get('user-agent') || ''; + const acceptLanguage = request.headers.get('accept-language') || ''; + const acceptEncoding = request.headers.get('accept-encoding') || ''; + const currentFingerprint = Buffer.from(`${userAgent}:${acceptLanguage}:${acceptEncoding}`).toString('base64'); + + if (currentFingerprint !== sessionData.browserFingerprint) { + sessions.delete(sessionId); // Delete session on fingerprint mismatch + return new Response('Unauthorized - Browser mismatch', { status: 401 }); + } + + // Generate secure access page for digital content + const digitalContentHtml = generateDigitalContentPage(sessionData, sessionId); + + return new Response(digitalContentHtml, { + status: 200, + headers: { + 'Content-Type': 'text/html', + 'Cache-Control': 'private, no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + 'X-Robots-Tag': 'noindex, nofollow' + } + }); + + } catch (error) { + console.error('Error serving digital content:', error); + return new Response('Internal Server Error', { status: 500 }); + } +}; + +function generateDigitalContentPage(sessionData: any, sessionId: string): string { + const { theme, organization, kitType } = sessionData; + + return ` + + + + + Digital Content - ${theme} + + + + + + + + +
+
+
+
+

Digital Content

+

Kit: ${theme}

+

Purchased by: ${organization}

+

Kit Type: ${kitType === 'build' ? 'Build-your-own Kit' : 'Ready-made Kit'}

+
+
+
+
+ + + + +
+
+ +
+
+
+ +
+
+
+

Setup Instructions

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.

+ +

Video Guide

+
+

[Video Player Placeholder]

+ Content Type: MP4 Video with Captions +
+ +

Step-by-Step Guide

+
    +
  1. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
  2. +
  3. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
  4. +
  5. Ut enim ad minim veniam, quis nostrud exercitation ullamco.
  6. +
  7. Duis aute irure dolor in reprehenderit in voluptate velit esse.
  8. +
+
+
+

Audio Descriptions

+
+

[Audio Player Placeholder]

+ Content Type: MP3 Audio +
+
+
+
+ + +
+

Interactive Elements

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut enim ad minim veniam, quis nostrud exercitation.

+ +
+
+
+
+
Puzzle Element 1
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

+ +
+
+
+
+
+
+
Puzzle Element 2
+

Sed do eiusmod tempor incididunt ut labore et dolore.

+ +
+
+
+
+
+
+
Final Challenge
+

Ut enim ad minim veniam, quis nostrud exercitation.

+ +
+
+
+
+
+ + +
+

Room Layout & Setup

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

+ +
+
+
+

[Room Layout Diagram Placeholder]

+ Interactive room layout would appear here +
+ +

Setup Requirements

+
    +
  • Lorem ipsum dolor sit amet - minimum 10x10 feet
  • +
  • Consectetur adipiscing elit - adequate lighting
  • +
  • Sed do eiusmod tempor - 4-6 participants recommended
  • +
  • Incididunt ut labore - tables and chairs for setup
  • +
+
+ +
+
+ + +
+

Leaderboard & Results

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Track your team's progress and compare with others.

+ +
+
+

Current Rankings

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RankTeam NameCompletion Time
1Lorem Ipsum Team24:35
2Consectetur Elit27:12
3Sed Do Eiusmod31:48
4Tempor Incididunt35:22
+
+
+
+

Your Stats

+
+
+
Team Progress
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

+
    +
  • Best Time: Not yet completed
  • +
  • Attempts: 0
  • +
  • Current Rank: Unranked
  • +
+
+
+
+
+
+
+
+ + +
+ + + + +`; +} diff --git a/astro/src/pages/api/download-material.ts b/astro/src/pages/api/download-material.ts new file mode 100644 index 00000000..7df506f8 --- /dev/null +++ b/astro/src/pages/api/download-material.ts @@ -0,0 +1,127 @@ +import type { APIRoute } from 'astro'; +import fs from 'fs/promises'; +import path from 'path'; +import { sessions } from './verify-purchase'; +import { downloadTokens } from './generate-download-token'; + +export const GET: APIRoute = async ({ request, url, clientAddress }) => { + try { + const searchParams = url.searchParams; + const downloadToken = searchParams.get('token'); + + if (!downloadToken) { + return new Response('Missing download token', { status: 401 }); + } + + // Verify download token + const tokenData = downloadTokens.get(downloadToken); + + if (!tokenData || Date.now() > tokenData.expiresAt || tokenData.used) { + if (tokenData) { + downloadTokens.delete(downloadToken); // Clean up + } + return new Response('Invalid or expired download token', { status: 401 }); + } + + // Mark token as used (one-time use only) + tokenData.used = true; + + // Verify the associated session still exists + const sessionData = sessions.get(tokenData.sessionId); + + if (!sessionData || Date.now() > sessionData.expiresAt) { + downloadTokens.delete(downloadToken); + return new Response('Session expired', { status: 401 }); + } + + // Verify browser fingerprint + const userAgent = request.headers.get('user-agent') || ''; + const acceptLanguage = request.headers.get('accept-language') || ''; + const acceptEncoding = request.headers.get('accept-encoding') || ''; + const currentFingerprint = Buffer.from(`${userAgent}:${acceptLanguage}:${acceptEncoding}`).toString('base64'); + + if (currentFingerprint !== sessionData.browserFingerprint) { + downloadTokens.delete(downloadToken); + return new Response('Unauthorized - Browser mismatch', { status: 401 }); + } + + const materialType = tokenData.materialType; + + // Validate material type + const validMaterialTypes = [ + 'setup-guide', + 'accessibility-guide', + 'prop-templates', + 'assembly-instructions', + 'shopping-list' + ]; + + if (!validMaterialTypes.includes(materialType)) { + return new Response('Not Found', { status: 404 }); + } + + // Construct file path based on material type + let filePath: string; + let fileName: string; + let contentType: string; + + switch (materialType) { + case 'setup-guide': + filePath = path.join(process.cwd(), 'src', 'protected-materials', 'placeholder.pdf'); + fileName = `setup-guide-${sessionData.theme}.pdf`; + contentType = 'application/pdf'; + break; + case 'accessibility-guide': + filePath = path.join(process.cwd(), 'src', 'protected-materials', 'placeholder.pdf'); + fileName = 'accessibility-guide.pdf'; + contentType = 'application/pdf'; + break; + case 'prop-templates': + filePath = path.join(process.cwd(), 'src', 'protected-materials', 'placeholder.pdf'); + fileName = `prop-templates-${sessionData.theme}.zip`; + contentType = 'application/zip'; + break; + case 'assembly-instructions': + filePath = path.join(process.cwd(), 'src', 'protected-materials', 'placeholder.pdf'); + fileName = `assembly-instructions-${sessionData.theme}.pdf`; + contentType = 'application/pdf'; + break; + case 'shopping-list': + filePath = path.join(process.cwd(), 'src', 'protected-materials', 'placeholder.pdf'); + fileName = `shopping-list-${sessionData.theme}.pdf`; + contentType = 'application/pdf'; + break; + default: + return new Response('Not Found', { status: 404 }); + } + + try { + // Check if file exists + await fs.access(filePath); + + // Read file + const fileBuffer = await fs.readFile(filePath); + + // Return file with appropriate headers + return new Response(new Uint8Array(fileBuffer), { + status: 200, + headers: { + 'Content-Type': contentType, + 'Content-Disposition': `attachment; filename="${fileName}"`, + 'Cache-Control': 'private, no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + 'X-Robots-Tag': 'noindex, nofollow' + } + }); + + } catch (fileError) { + console.error('File not found:', filePath); + return new Response('File not found', { status: 404 }); + } + + } catch (error) { + console.error('Error serving protected material:', error); + return new Response('Internal Server Error', { status: 500 }); + } +}; diff --git a/astro/src/pages/api/generate-download-token.ts b/astro/src/pages/api/generate-download-token.ts new file mode 100644 index 00000000..840bf972 --- /dev/null +++ b/astro/src/pages/api/generate-download-token.ts @@ -0,0 +1,75 @@ +import type { APIRoute } from 'astro'; +import { sessions } from './verify-purchase'; +import { v4 as uuidv4 } from 'uuid'; + +// Store for one-time download tokens +const downloadTokens = new Map(); + +// Clean up expired tokens every 10 minutes +setInterval(() => { + const now = Date.now(); + for (const [tokenId, token] of downloadTokens.entries()) { + if (now > token.expiresAt || token.used) { + downloadTokens.delete(tokenId); + } + } +}, 10 * 60 * 1000); + +export const POST: APIRoute = async ({ request, clientAddress }) => { + try { + const body = await request.json(); + const { sessionId, materialType } = body; + + if (!sessionId || !materialType) { + return new Response('Missing parameters', { status: 400 }); + } + + // Verify session exists and is valid + const sessionData = sessions.get(sessionId); + + if (!sessionData || Date.now() > sessionData.expiresAt) { + return new Response('Invalid session', { status: 401 }); + } + + // Verify browser fingerprint + const userAgent = request.headers.get('user-agent') || ''; + const acceptLanguage = request.headers.get('accept-language') || ''; + const acceptEncoding = request.headers.get('accept-encoding') || ''; + const currentFingerprint = Buffer.from(`${userAgent}:${acceptLanguage}:${acceptEncoding}`).toString('base64'); + + if (currentFingerprint !== sessionData.browserFingerprint) { + return new Response('Unauthorized', { status: 401 }); + } + + // Create one-time download token (expires in 5 minutes) + const downloadToken = uuidv4(); + downloadTokens.set(downloadToken, { + sessionId, + materialType, + createdAt: Date.now(), + expiresAt: Date.now() + (5 * 60 * 1000), // 5 minutes + used: false + }); + + return new Response( + JSON.stringify({ downloadToken }), + { + status: 200, + headers: { 'Content-Type': 'application/json' } + } + ); + + } catch (error) { + console.error('Error generating download token:', error); + return new Response('Internal Server Error', { status: 500 }); + } +}; + +// Export for use in download endpoint +export { downloadTokens }; diff --git a/astro/src/pages/api/send-confirmation-email.ts b/astro/src/pages/api/send-confirmation-email.ts new file mode 100644 index 00000000..e628ea65 --- /dev/null +++ b/astro/src/pages/api/send-confirmation-email.ts @@ -0,0 +1,72 @@ +import type { APIRoute } from 'astro'; + +export const POST: APIRoute = async ({ request }) => { + try { + const body = await request.json(); + const { email, purchaseCode, organization, kitType, theme } = body; + + // In a production environment, you would use a service like: + // - SendGrid + // - Nodemailer with SMTP + // - AWS SES + // - Mailgun + // etc. + + // For now, we'll just log the email content + const emailContent = ` +Subject: Your Accessible Escape Room Kit Purchase Confirmation + +Dear ${organization}, + +Thank you for purchasing an Accessible Escape Room Kit! + +Your Purchase Details: +- Kit Type: ${kitType === 'build' ? 'Build-your-own Kit' : 'Ready-made Kit'} +- Theme: ${theme} +- Organization: ${organization} + +Your Secure Access Code: ${purchaseCode} + +To access your kit materials, visit: +https://accessiblecommunity.org/services/escape-room/access + +Enter your access code and the email address used for this purchase. + +Keep this code secure - it's your proof of purchase and gateway to your materials. + +If you need any assistance, please contact us at escaperoom@accessiblecommunity.org + +Best regards, +The Accessible Community Team + `; + + // In production, replace this with actual email sending + // await sendEmail({ + // to: email, + // subject: 'Your Accessible Escape Room Kit Purchase Confirmation', + // text: emailContent, + // html: generateHtmlEmail(purchaseCode, organization, kitType, theme) + // }); + + return new Response( + JSON.stringify({ + success: true, + message: 'Confirmation email sent successfully' + }), + { + status: 200, + headers: { 'Content-Type': 'application/json' } + } + ); + + } catch (error) { + console.error('Error sending confirmation email:', error); + return new Response( + JSON.stringify({ + success: false, + error: 'Failed to send confirmation email' + }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } +}; diff --git a/astro/src/pages/api/session-status.ts b/astro/src/pages/api/session-status.ts new file mode 100644 index 00000000..f6b1f332 --- /dev/null +++ b/astro/src/pages/api/session-status.ts @@ -0,0 +1,40 @@ +import type { APIRoute } from 'astro'; +import Stripe from 'stripe'; + +const stripe = new Stripe(import.meta.env.STRIPE_SECRET_KEY); + +export const GET: APIRoute = async ({ url, request }) => { + try { + const sessionId = new URL(url).searchParams.get('session_id'); + + if (!sessionId) { + return new Response( + JSON.stringify({ error: 'Session ID is required' }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + // TODO: Add authentication check here before exposing session data + // Should verify the request is authorized to access this session + + const session = await stripe.checkout.sessions.retrieve(sessionId); + + // Only return payment status - no sensitive customer data + return new Response( + JSON.stringify({ + status: session.status + }), + { + status: 200, + headers: { 'Content-Type': 'application/json' } + } + ); + + } catch (error) { + console.error('Error retrieving session:', error); + return new Response( + JSON.stringify({ error: 'Failed to retrieve session' }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } +}; diff --git a/astro/src/pages/api/stripe-webhook.ts b/astro/src/pages/api/stripe-webhook.ts new file mode 100644 index 00000000..b213caa8 --- /dev/null +++ b/astro/src/pages/api/stripe-webhook.ts @@ -0,0 +1,117 @@ +import type { APIRoute } from 'astro'; +import Stripe from 'stripe'; +import fs from 'fs/promises'; +import path from 'path'; + +const stripe = new Stripe(import.meta.env.STRIPE_SECRET_KEY); +const endpointSecret = import.meta.env.STRIPE_WEBHOOK_SECRET; + +export const POST: APIRoute = async ({ request }) => { + const body = await request.text(); + const sig = request.headers.get('stripe-signature'); + + let event: Stripe.Event; + + try { + if (!endpointSecret) { + console.warn('Stripe webhook secret not configured, skipping signature verification'); + event = JSON.parse(body); + } else { + event = stripe.webhooks.constructEvent(body, sig!, endpointSecret); + } + } catch (err) { + console.error('Webhook signature verification failed:', err); + return new Response('Webhook signature verification failed', { status: 400 }); + } + + try { + // Handle the event + switch (event.type) { + case 'checkout.session.completed': + const session = event.data.object as Stripe.Checkout.Session; + await handleSuccessfulPayment(session); + break; + default: + console.log(`Unhandled event type ${event.type}`); + } + + return new Response('Webhook handled successfully', { status: 200 }); + } catch (error) { + console.error('Error handling webhook:', error); + return new Response('Webhook handler failed', { status: 500 }); + } +}; + +async function handleSuccessfulPayment(session: Stripe.Checkout.Session) { + const metadata = session.metadata!; + const purchaseData = { + sessionId: session.id, + purchaseCode: metadata.purchaseCode, + kitType: metadata.kitType, + theme: metadata.theme, + organization: metadata.organization, + contactName: metadata.contactName, + email: metadata.email, + specialRequirements: metadata.specialRequirements, + amountPaid: session.amount_total, + currency: session.currency, + paymentStatus: session.payment_status, + createdAt: new Date().toISOString(), + }; + + // Store purchase data securely on server + await storePurchaseData(purchaseData); + + // Send confirmation email with secure purchase code + try { + const emailResponse = await fetch('/api/send-confirmation-email', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email: metadata.email, + purchaseCode: metadata.purchaseCode, + organization: metadata.organization, + kitType: metadata.kitType, + theme: metadata.theme, + }), + }); + + if (!emailResponse.ok) { + console.error('Failed to send confirmation email'); + } else { + console.log('Confirmation email sent successfully'); + } + } catch (error) { + console.error('Error sending confirmation email:', error); + } +} + +async function storePurchaseData(purchaseData: any) { + try { + // TODO: For user accounts/login system, also store purchase by email: + // - Link purchase codes to customer email addresses + // - Store in database: { email, purchaseCode, productId, purchaseDate } + // - Enable "view all my purchases" functionality + + // TODO?: For Google OAuth integration: + // - Add optional account linking after purchase + // - Store OAuth user ID with purchase data + // - Allow login to see purchase history + + // Create local storage directory if it doesn't exist + const storageDir = path.join(process.cwd(), 'local-dev', 'purchases'); + await fs.mkdir(storageDir, { recursive: true }); + + // Store purchase data as JSON file + const filename = `${purchaseData.purchaseCode}.json`; + const filepath = path.join(storageDir, filename); + + await fs.writeFile(filepath, JSON.stringify(purchaseData, null, 2)); + // Purchase data stored successfully + } catch (error) { + console.error('Error storing purchase data:', error); + throw error; + } +} diff --git a/astro/src/pages/api/verify-purchase.ts b/astro/src/pages/api/verify-purchase.ts new file mode 100644 index 00000000..2e3cee85 --- /dev/null +++ b/astro/src/pages/api/verify-purchase.ts @@ -0,0 +1,127 @@ +import type { APIRoute } from 'astro'; +import fs from 'fs/promises'; +import path from 'path'; +import { v4 as uuidv4 } from 'uuid'; + +// In-memory session store (in production, use Redis or database) +const sessions = new Map(); + +// Clean up expired sessions every hour +setInterval(() => { + const now = Date.now(); + for (const [sessionId, session] of sessions.entries()) { + if (now > session.expiresAt) { + sessions.delete(sessionId); + } + } +}, 60 * 60 * 1000); + +export const POST: APIRoute = async ({ request, clientAddress }) => { + try { + const body = await request.json(); + const { purchaseCode, email } = body; + + if (!purchaseCode || !email) { + return new Response( + JSON.stringify({ error: 'Invalid purchase code or email address' }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + // Look up purchase data + const purchaseData = await getPurchaseData(purchaseCode); + + if (!purchaseData) { + return new Response( + JSON.stringify({ error: 'Invalid purchase code or email address' }), + { status: 404, headers: { 'Content-Type': 'application/json' } } + ); + } + + // Verify email matches + if (purchaseData.email.toLowerCase() !== email.toLowerCase()) { + return new Response( + JSON.stringify({ error: 'Invalid purchase code or email address' }), + { status: 403, headers: { 'Content-Type': 'application/json' } } + ); + } + + // Create browser fingerprint from headers + const userAgent = request.headers.get('user-agent') || ''; + const acceptLanguage = request.headers.get('accept-language') || ''; + const acceptEncoding = request.headers.get('accept-encoding') || ''; + const browserFingerprint = Buffer.from(`${userAgent}:${acceptLanguage}:${acceptEncoding}`).toString('base64'); + + // Create secure session + const sessionId = uuidv4(); + const sessionData = { + purchaseCode, + email: purchaseData.email, + theme: purchaseData.theme, + kitType: purchaseData.kitType, + organization: purchaseData.organization, + createdAt: Date.now(), + expiresAt: Date.now() + (30 * 60 * 1000), // Reduced to 30 minutes + browserFingerprint, + ipAddress: clientAddress || 'unknown' + }; + + sessions.set(sessionId, sessionData); + + // Return session ID and purchase details with security token + const response = new Response( + JSON.stringify({ + valid: true, + sessionId, + kitType: purchaseData.kitType, + theme: purchaseData.theme, + organization: purchaseData.organization, + purchaseDate: purchaseData.createdAt, + }), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + // Set HttpOnly cookie for additional security + 'Set-Cookie': `session=${sessionId}; HttpOnly; Secure; SameSite=Strict; Max-Age=1800; Path=/api/` + } + } + ); + + return response; + + } catch (error) { + console.error('Error verifying purchase:', error); + return new Response( + JSON.stringify({ error: 'Invalid purchase code or email address' }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } +}; + +async function getPurchaseData(purchaseCode: string) { + try { + const storageDir = path.join(process.cwd(), 'local-dev', 'purchases'); + const filename = `${purchaseCode}.json`; + const filepath = path.join(storageDir, filename); + + const data = await fs.readFile(filepath, 'utf8'); + return JSON.parse(data); + } catch (error) { + // File doesn't exist or other error + return null; + } +} + +// Export sessions for use in other APIs +export { sessions }; diff --git a/astro/src/pages/services/escape-room/access.astro b/astro/src/pages/services/escape-room/access.astro new file mode 100644 index 00000000..9eb3c4fe --- /dev/null +++ b/astro/src/pages/services/escape-room/access.astro @@ -0,0 +1,539 @@ +--- +import { getOpenGraphImageData } from "@lib/og-image"; +import type { Breadcrumbs, PageMetadata } from "@lib/types"; + +const title = "Access Your Kit - The Accessible Escape Room"; +const metadata: PageMetadata = { + title, + description: "Access your purchased escape room kit materials", + image: getOpenGraphImageData(Astro.site, "pages", "kit-access"), +}; +const crumbs: Breadcrumbs = [ + { name: "Home", href: "/" }, + { name: "Services", href: "/services" }, + { name: "Escape Room", href: "/services/escape-room" }, +]; + +import Branding from "@components/Branding.astro"; +import Layout from "src/layouts/Layout.astro"; +import ThemedSection from "@components/ThemedSection.astro"; +import { Icon } from "astro-icon/components"; +--- + + +
+
+
+
+

+ Access Your Escape Room Kit +

+

+ Enter your purchase code and email to access your kit materials +

+
+
+
+
+ + + +
+
+
+
+

+ + Verify Your Purchase +

+
+
+
+ + +
+ + +
+ Enter the purchase code from your confirmation email +
+ +
+ +
+ + + +
+ +
+ +
+ + +
+ + +
+
+
+
+
+ + + + + + +
+

Need Help?

+
+
+
+
+ +
Lost Your Code?
+

Check your email confirmation or contact support

+ + Email Support + +
+
+
+
+
+
+ +
Setup Help
+

Get assistance setting up your escape room

+ + Get Setup Help + +
+
+
+
+
+
+ +
Purchase More
+

Want another theme or kit type?

+ + View Kits + +
+
+
+
+
+
+ + + + +
diff --git a/astro/src/pages/services/escape-room/index.astro b/astro/src/pages/services/escape-room/index.astro index de55a4c1..530dee63 100644 --- a/astro/src/pages/services/escape-room/index.astro +++ b/astro/src/pages/services/escape-room/index.astro @@ -94,6 +94,9 @@ import { Icon } from "astro-icon/components"; venue, we now offer two options for purchase:

+
+

Already have a kit? Access your materials.

+
@@ -123,8 +126,8 @@ import { Icon } from "astro-icon/components"; descriptions. - +
-

- If you are interested in purchasing, please email us at escaperoom@accessiblecommunity.org. +

+ Ready to get started? Purchase Your Kit

diff --git a/astro/src/pages/services/escape-room/purchase-success.astro b/astro/src/pages/services/escape-room/purchase-success.astro index 76c86f6d..35292764 100644 --- a/astro/src/pages/services/escape-room/purchase-success.astro +++ b/astro/src/pages/services/escape-room/purchase-success.astro @@ -202,33 +202,9 @@ import ThemedSection from "@components/ThemedSection.astro"; - + \ No newline at end of file diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 00000000..53b88013 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,20 @@ +[build] + publish = "astro/dist" + command = "cd astro && npm install && npm run build" + +[build.environment] + NODE_VERSION = "18" + +[[redirects]] + from = "/api/*" + to = "/.netlify/functions/:splat" + status = 200 + +[functions] + directory = "netlify/functions" + node_bundler = "esbuild" + +# Environment variables needed: +# STRIPE_PUBLISHABLE_KEY +# STRIPE_SECRET_KEY +# STRIPE_WEBHOOK_SECRET diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..422c4078 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,309 @@ +{ + "name": "accessiblecommunity.github.io", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "stripe": "^18.4.0", + "uuid": "^11.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stripe": { + "version": "18.4.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-18.4.0.tgz", + "integrity": "sha512-LKFeDnDYo4U/YzNgx2Lc9PT9XgKN0JNF1iQwZxgkS4lOw5NunWCnzyH5RhTlD3clIZnf54h7nyMWkS8VXPmtTQ==", + "license": "MIT", + "dependencies": { + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + }, + "peerDependencies": { + "@types/node": ">=12.x.x" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..ff1a1079 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "stripe": "^18.4.0", + "uuid": "^11.1.0" + } +} From 08b3b2f4b1bdb2c57990b0ef42d82fa53c3ce3bc Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Wed, 3 Sep 2025 14:48:27 -0400 Subject: [PATCH 03/27] fix: Correct storage directory path for purchase data --- astro/src/pages/api/stripe-webhook.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astro/src/pages/api/stripe-webhook.ts b/astro/src/pages/api/stripe-webhook.ts index b213caa8..fe031365 100644 --- a/astro/src/pages/api/stripe-webhook.ts +++ b/astro/src/pages/api/stripe-webhook.ts @@ -101,7 +101,7 @@ async function storePurchaseData(purchaseData: any) { // - Allow login to see purchase history // Create local storage directory if it doesn't exist - const storageDir = path.join(process.cwd(), 'local-dev', 'purchases'); + const storageDir = path.join(process.cwd(), '..', 'local-dev', 'purchases'); await fs.mkdir(storageDir, { recursive: true }); // Store purchase data as JSON file From 67879ad0e78a6c40d48af3940cd0d60d452839e4 Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Wed, 3 Sep 2025 14:48:33 -0400 Subject: [PATCH 04/27] feat: Return only non-sensitive fields in session status response --- astro/src/pages/api/session-status.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/astro/src/pages/api/session-status.ts b/astro/src/pages/api/session-status.ts index f6b1f332..8039155e 100644 --- a/astro/src/pages/api/session-status.ts +++ b/astro/src/pages/api/session-status.ts @@ -19,11 +19,15 @@ export const GET: APIRoute = async ({ url, request }) => { const session = await stripe.checkout.sessions.retrieve(sessionId); - // Only return payment status - no sensitive customer data + // Return only non-sensitive fields the frontend needs + const safeSession = { + status: session.status, + metadata: session.metadata || null, + customer_email: session.customer_email || null, + }; + return new Response( - JSON.stringify({ - status: session.status - }), + JSON.stringify(safeSession), { status: 200, headers: { 'Content-Type': 'application/json' } From 606cef983f55be5d112a9d405006a4043d95007d Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Wed, 3 Sep 2025 14:48:46 -0400 Subject: [PATCH 05/27] Enhance debugging --- astro/src/pages/api/verify-purchase.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/astro/src/pages/api/verify-purchase.ts b/astro/src/pages/api/verify-purchase.ts index 2e3cee85..c68f07f7 100644 --- a/astro/src/pages/api/verify-purchase.ts +++ b/astro/src/pages/api/verify-purchase.ts @@ -31,7 +31,11 @@ export const POST: APIRoute = async ({ request, clientAddress }) => { const body = await request.json(); const { purchaseCode, email } = body; + // Add debugging + console.log('verify-purchase request:', { purchaseCode, email }); + if (!purchaseCode || !email) { + console.log('Missing required fields'); return new Response( JSON.stringify({ error: 'Invalid purchase code or email address' }), { status: 400, headers: { 'Content-Type': 'application/json' } } @@ -42,6 +46,7 @@ export const POST: APIRoute = async ({ request, clientAddress }) => { const purchaseData = await getPurchaseData(purchaseCode); if (!purchaseData) { + console.log('Purchase data not found for code:', purchaseCode); return new Response( JSON.stringify({ error: 'Invalid purchase code or email address' }), { status: 404, headers: { 'Content-Type': 'application/json' } } @@ -111,14 +116,20 @@ export const POST: APIRoute = async ({ request, clientAddress }) => { async function getPurchaseData(purchaseCode: string) { try { - const storageDir = path.join(process.cwd(), 'local-dev', 'purchases'); + // Go up one level from astro/ to repo root, then into local-dev/purchases + const storageDir = path.join(process.cwd(), '..', 'local-dev', 'purchases'); const filename = `${purchaseCode}.json`; const filepath = path.join(storageDir, filename); + // Add debugging + console.log('Looking for purchase file:', filepath); + const data = await fs.readFile(filepath, 'utf8'); + console.log('Found purchase data for:', purchaseCode); return JSON.parse(data); } catch (error) { // File doesn't exist or other error + console.log('Error reading purchase file:', error.message); return null; } } From eba9ab015331af180bdc3908dad756fede9a2767 Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Wed, 3 Sep 2025 15:00:20 -0400 Subject: [PATCH 06/27] Implement save purchase data API and local development instructions --- .gitignore | 4 +- astro/src/pages/api/save-purchase-data.ts | 44 +++++++++++++++++++ astro/src/pages/api/stripe-webhook.ts | 3 ++ .../escape-room/purchase-success.astro | 12 +++++ local-dev/README.md | 29 ++++++++++++ 5 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 astro/src/pages/api/save-purchase-data.ts create mode 100644 local-dev/README.md diff --git a/.gitignore b/.gitignore index f2295288..793eea77 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,9 @@ dist/ .env # Local development files -astro/local-dev/ +local-dev/purchases/ +local-dev/emails/ +local-dev/uploads/ # Protected materials astro/src/protected-materials/ diff --git a/astro/src/pages/api/save-purchase-data.ts b/astro/src/pages/api/save-purchase-data.ts new file mode 100644 index 00000000..35b8385e --- /dev/null +++ b/astro/src/pages/api/save-purchase-data.ts @@ -0,0 +1,44 @@ +import type { APIRoute } from 'astro'; +import Stripe from 'stripe'; +import { handleSuccessfulPayment } from './stripe-webhook'; + +const stripe = new Stripe(import.meta.env.STRIPE_SECRET_KEY); + +export const POST: APIRoute = async ({ request }) => { + try { + const body = await request.json(); + const { sessionId } = body; + + if (!sessionId) { + return new Response( + JSON.stringify({ error: 'Session ID required' }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + // Get the full session from Stripe + const session = await stripe.checkout.sessions.retrieve(sessionId); + + if (session.payment_status !== 'paid') { + return new Response( + JSON.stringify({ error: 'Payment not completed' }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + // Use the existing secure webhook handler + await handleSuccessfulPayment(session); + + return new Response( + JSON.stringify({ success: true }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ); + + } catch (error) { + console.error('Error processing purchase:', error); + return new Response( + JSON.stringify({ error: 'Failed to process purchase' }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } +}; diff --git a/astro/src/pages/api/stripe-webhook.ts b/astro/src/pages/api/stripe-webhook.ts index fe031365..e84796c7 100644 --- a/astro/src/pages/api/stripe-webhook.ts +++ b/astro/src/pages/api/stripe-webhook.ts @@ -88,6 +88,9 @@ async function handleSuccessfulPayment(session: Stripe.Checkout.Session) { } } +// Export for use in save-purchase-data +export { handleSuccessfulPayment }; + async function storePurchaseData(purchaseData: any) { try { // TODO: For user accounts/login system, also store purchase by email: diff --git a/astro/src/pages/services/escape-room/purchase-success.astro b/astro/src/pages/services/escape-room/purchase-success.astro index 35292764..9a3f5ef4 100644 --- a/astro/src/pages/services/escape-room/purchase-success.astro +++ b/astro/src/pages/services/escape-room/purchase-success.astro @@ -245,6 +245,18 @@ import ThemedSection from "@components/ThemedSection.astro"; // Display purchase details displayPurchaseDetails(sessionId, purchaseCode, sessionData); + // Save purchase data locally (since webhook won't fire in development) + try { + await fetch('/api/save-purchase-data', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ sessionId }) + }); + console.log('Purchase data saved locally'); + } catch (error) { + console.error('Failed to save purchase data:', error); + } + // Enable kit access if (accessKitBtn) { accessKitBtn.disabled = false; diff --git a/local-dev/README.md b/local-dev/README.md new file mode 100644 index 00000000..82ec7e6a --- /dev/null +++ b/local-dev/README.md @@ -0,0 +1,29 @@ +# Local Development Test Data + +### Manual File Creation + +Create files in `local-dev/purchases/` with this format: + +```json +{ + "sessionId": "cs_test_12345678", + "purchaseCode": "ESC-12345678", + "kitType": "ready", + "theme": "corporate", + "organization": "Test Organization", + "contactName": "Test User", + "email": "test@example.com", + "specialRequirements": "", + "amountPaid": 35000, + "currency": "usd", + "paymentStatus": "paid", + "createdAt": "2025-09-03T14:30:00.000Z" +} +``` + +## Completing a Test Purchase + +1. Start the dev server: `npm run dev` (from `astro/` folder) +2. Visit: http://localhost:4321/services/escape-room/purchase +3. Complete a test checkout using Stripe test cards +4. Purchase data will be automatically saved to `local-dev/purchases/` From e698c3b361cf569f03d9b5f36b074b36c67c1b67 Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Thu, 11 Sep 2025 12:52:38 -0400 Subject: [PATCH 07/27] Remove deprecated Azure checkout session and purchase data handling files --- azure/create-checkout-session.js | 152 ------------------------------- azure/example.env | 13 --- azure/get-checkout-session.js | 52 ----------- azure/pop-code-gen.js | 71 --------------- azure/save-purchase-data.js | 83 ----------------- 5 files changed, 371 deletions(-) delete mode 100644 azure/create-checkout-session.js delete mode 100644 azure/example.env delete mode 100644 azure/get-checkout-session.js delete mode 100644 azure/pop-code-gen.js delete mode 100644 azure/save-purchase-data.js diff --git a/azure/create-checkout-session.js b/azure/create-checkout-session.js deleted file mode 100644 index 622d0ead..00000000 --- a/azure/create-checkout-session.js +++ /dev/null @@ -1,152 +0,0 @@ -const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); - -// Helper function to add standard headers -function withHeaders(body, status = 200) { - return { - status, - headers: { - 'Access-Control-Allow-Origin': process.env.ALLOWED_ORIGIN || '*', - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', - 'Content-Type': 'application/json' - }, - body - }; -} - -module.exports = async function (context, req) { - context.log('Escape Room checkout function started'); - - // Handle CORS preflight - if (req.method === 'OPTIONS') { - context.res = withHeaders(null, 200); - return; - } - - try { - const { items, customerEmail, returnUrl, shippingAddress, metadata } = req.body; - - context.log('Processing checkout for:', { - itemsCount: items?.length, - customerEmail, - hasShipping: !!shippingAddress, - metadata - }); - - // Validate required fields - if (!items || !Array.isArray(items) || items.length === 0) { - context.res = withHeaders({ - error: 'Invalid items data' - }, 400); - return; - } - - if (!customerEmail) { - context.res = withHeaders({ - error: 'Customer email is required' - }, 400); - return; - } - - if (!returnUrl) { - context.res = withHeaders({ - error: 'Return URL is required' - }, 400); - return; - } - - // Build line items for Stripe - const lineItems = items.map(item => ({ - price_data: { - currency: 'usd', - product_data: { - name: item.name, - description: item.description || '', - metadata: { - kit_version: item.version || 'standard', - product_type: 'escape_room_kit' - } - }, - unit_amount: Math.round(item.price * 100), // Convert to cents - }, - quantity: item.quantity || 1, - })); - - // Create session config - const sessionConfig = { - ui_mode: 'embedded', - line_items: lineItems, - mode: 'payment', - return_url: `${returnUrl}?session_id={CHECKOUT_SESSION_ID}`, - customer_email: customerEmail, - metadata: { - source: 'escape_room_kit_purchase', - timestamp: new Date().toISOString(), - order_type: 'physical_product', - // Add custom metadata from the form - ...(metadata && { - kitType: metadata.kitType, - theme: metadata.theme, - organization: metadata.organization, - contactName: metadata.contactName, - specialRequirements: metadata.specialRequirements || '' - }) - } - }; - - // Add shipping if requested - if (shippingAddress) { - sessionConfig.shipping_address_collection = { - allowed_countries: ['US', 'CA'], - }; - - sessionConfig.shipping_options = [ - { - shipping_rate_data: { - type: 'fixed_amount', - fixed_amount: { - amount: 999, // $9.99 in cents - currency: 'usd', - }, - display_name: 'Standard Shipping', - delivery_estimate: { - minimum: { unit: 'business_day', value: 5 }, - maximum: { unit: 'business_day', value: 7 }, - }, - }, - }, - { - shipping_rate_data: { - type: 'fixed_amount', - fixed_amount: { - amount: 1999, // $19.99 in cents - currency: 'usd', - }, - display_name: 'Express Shipping', - delivery_estimate: { - minimum: { unit: 'business_day', value: 2 }, - maximum: { unit: 'business_day', value: 3 }, - }, - }, - }, - ]; - } - - // Create the Stripe session - const session = await stripe.checkout.sessions.create(sessionConfig); - - context.log('Stripe session created successfully:', session.id); - - context.res = withHeaders({ - clientSecret: session.client_secret, - sessionId: session.id - }, 200); - - } catch (error) { - context.log.error('Stripe error:', error.message); - context.res = withHeaders({ - error: 'Failed to create checkout session', - details: error.message - }, 500); - } -}; \ No newline at end of file diff --git a/azure/example.env b/azure/example.env deleted file mode 100644 index e095f970..00000000 --- a/azure/example.env +++ /dev/null @@ -1,13 +0,0 @@ -# Stripe Keys -STRIPE_SECRET_KEY= -PUBLIC_STRIPE_PUBLISHABLE_KEY= - -PUBLIC_AZURE_CHECKOUT_URL= -PUBLIC_AZURE_GET_CHECKOUT_URL= -PUBLIC_AZURE_PROOF_GENERATOR_URL= -PUBLIC_AZURE_SAVE_URL= - -API_SECRET_KEY=secret - -# Optional: CORS origin for Azure Function -ALLOWED_ORIGIN= diff --git a/azure/get-checkout-session.js b/azure/get-checkout-session.js deleted file mode 100644 index abfbfa65..00000000 --- a/azure/get-checkout-session.js +++ /dev/null @@ -1,52 +0,0 @@ - -const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); - -function withHeaders(body, status = 200) { - return { - status, - headers: { - 'Access-Control-Allow-Origin': process.env.ALLOWED_ORIGIN || '*', - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', - 'Content-Type': 'application/json' - }, - body - }; -} - -module.exports = async function (context, req) { - try { - context.log('Function get-checkout-session started'); - - // Handle CORS preflight requests - if (req.method === 'OPTIONS') { - context.res = withHeaders(null, 200); - return; - } - - const { sessionId } = req.body || {}; - - context.log('Received sessionId:', sessionId); - - if (!sessionId) { - context.res = withHeaders({ error: 'sessionId is required' }, 400); - return; - } - - // Retrieve the Stripe checkout session with expanded line_items and customer_details - const session = await stripe.checkout.sessions.retrieve(sessionId, { - expand: ['line_items', 'customer_details'] - }); - - context.log('Stripe session retrieved:', session.id); - - // Return the full session object to the frontend - context.res = withHeaders(session); - } catch (error) { - context.log.error('Error retrieving Stripe session:', error); - context.res = withHeaders({ - error: error.message, - stack: error.stack - }, 500); - } -}; \ No newline at end of file diff --git a/azure/pop-code-gen.js b/azure/pop-code-gen.js deleted file mode 100644 index 1fbf2c04..00000000 --- a/azure/pop-code-gen.js +++ /dev/null @@ -1,71 +0,0 @@ -const { TableClient } = require("@azure/data-tables"); -const crypto = require('crypto'); -const { v4: uuidv4 } = require('uuid'); - -module.exports = async function (context, req) { - try { - // Validate input - const sessionId = req.body.sessionId; - const purchaseData = req.body.purchaseData; - - if (!sessionId || typeof sessionId !== 'string' || sessionId.length < 10) { - context.res = { - status: 400, - body: JSON.stringify({ error: "Invalid session ID format" }) - }; - return; - } - - // Generate secure proof code - const proofCode = generateSecureProofCode(); - - // Store in Azure Table - await storeInTableStorage(sessionId, purchaseData, proofCode); - - context.res = { - status: 200, - body: JSON.stringify({ proofCode }), - headers: { "Content-Type": "application/json" } - }; - } catch (error) { - context.res = { - status: 500, - body: JSON.stringify({ error: error.message }) - }; - } -}; - -function generateSecureProofCode() { - // Generate crypto-secure token - const randomBytes = crypto.randomBytes(24); - const token = randomBytes.toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, ''); - - return `AER-${token}`; -} - -async function storeInTableStorage(sessionId, purchaseData, proofCode) { - const connectionString = process.env.AZURE_STORAGE_CONNECTION_STRING; - const tableName = "PurchaseProofs"; - const client = TableClient.fromConnectionString(connectionString, tableName); - - // Create table if not exists - try { - await client.createTable(); - } catch (e) { - if (e.statusCode !== 409) throw e; // Ignore "already exists" errors - } - - const entity = { - partitionKey: "proof", - rowKey: proofCode, // Use proofCode as rowKey for fast lookup - sessionId, - purchaseData: JSON.stringify(purchaseData), - createdAt: new Date().toISOString(), - expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString() // 1 year - }; - - await client.upsertEntity(entity, "Replace"); -} \ No newline at end of file diff --git a/azure/save-purchase-data.js b/azure/save-purchase-data.js deleted file mode 100644 index b8e9766a..00000000 --- a/azure/save-purchase-data.js +++ /dev/null @@ -1,83 +0,0 @@ -const { TableClient } = require("@azure/data-tables"); - -module.exports = async function (context, req) { - context.log('=== save-purchase-data START ==='); - - const connectionString = process.env["AZURE_STORAGE_CONNECTION_STRING"]; - if (!connectionString) { - context.log.error("Missing AZURE_STORAGE_CONNECTION_STRING"); - context.res = { status: 500, body: "ERROR: AZURE_STORAGE_CONNECTION_STRING not configured." }; - context.log('=== save-purchase-data END ==='); - return; - } - context.log("Connection string OK"); - - let client; - try { - client = TableClient.fromConnectionString(connectionString, "Purchases"); - context.log("TableClient created"); - } catch (err) { - context.log.error("TableClient creation FAILED:", err.message); - context.res = { status: 500, body: "ERROR: Could not create TableClient." }; - context.log('=== save-purchase-data END ==='); - return; - } - - const purchase = req.body; - context.log("Request body:", JSON.stringify(purchase)); - - if (!purchase || typeof purchase !== "object") { - context.res = { status: 400, body: "ERROR: Request body must be JSON." }; - context.log('=== save-purchase-data END ==='); - return; - } - - if (!purchase.id || !purchase.customerEmail) { - context.res = { status: 400, body: "ERROR: 'id' and 'customerEmail' fields are required." }; - context.log('=== save-purchase-data END ==='); - return; - } - - const entity = { - partitionKey: String(purchase.customerEmail), // lowercase 'p' - rowKey: String(purchase.id), // lowercase 'r' - paymentStatus: purchase.paymentStatus || "unknown", - amountTotal: purchase.amountTotal ?? 0, - currency: purchase.currency || "usd", - created: purchase.created || new Date().toISOString() - }; - - context.log("Prepared entity:", JSON.stringify(entity)); - context.log(`PartitionKey: '${entity.PartitionKey}' (type: ${typeof entity.PartitionKey})`); - context.log(`RowKey: '${entity.RowKey}' (type: ${typeof entity.RowKey})`); - context.log("Entity keys:", Object.keys(entity)); - - // Simple test upsert to isolate problem - try { - await client.upsertEntity({ - partitionKey: "test-partition", // lowercase - rowKey: "test-row" // lowercase - }, "Merge"); - context.log("Simple test upsert succeeded"); - } catch (err) { - context.log.error("Simple test upsert FAILED:", err.message); - } - - try { - // Try with forced serialization as a last resort - await client.upsertEntity(JSON.parse(JSON.stringify(entity)), "Merge"); - context.log("Upsert successful"); - context.res = { - status: 200, - body: { message: "Purchase saved.", entity } - }; - } catch (err) { - context.log.error("Upsert FAILED:", err.message); - context.res = { - status: 500, - body: "ERROR: Failed to save purchase." - }; - } - - context.log('=== save-purchase-data END ==='); -}; From 5f16546ccbcf96a97c0b6718f30f4efc31947903 Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Thu, 11 Sep 2025 16:19:53 -0400 Subject: [PATCH 08/27] Add comprehensive tests for API endpoints and setup configuration Configure Vitest with a setup file and path alias for cleaner imports. --- astro/src/test/api/basic-api.test.ts | 93 +++++ .../test/api/create-checkout-session.test.ts | 227 ++++++++++ astro/src/test/api/digital-content.test.ts | 316 ++++++++++++++ astro/src/test/api/download-material.test.ts | 358 ++++++++++++++++ .../test/api/generate-download-token.test.ts | 250 +++++++++++ astro/src/test/api/save-purchase-data.test.ts | 175 ++++++++ astro/src/test/api/session-status.test.ts | 146 +++++++ astro/src/test/api/stripe-webhook.test.ts | 393 ++++++++++++++++++ astro/src/test/api/verify-purchase.test.ts | 265 ++++++++++++ astro/src/test/setup.ts | 19 + astro/vitest.config.ts | 15 + 11 files changed, 2257 insertions(+) create mode 100644 astro/src/test/api/basic-api.test.ts create mode 100644 astro/src/test/api/create-checkout-session.test.ts create mode 100644 astro/src/test/api/digital-content.test.ts create mode 100644 astro/src/test/api/download-material.test.ts create mode 100644 astro/src/test/api/generate-download-token.test.ts create mode 100644 astro/src/test/api/save-purchase-data.test.ts create mode 100644 astro/src/test/api/session-status.test.ts create mode 100644 astro/src/test/api/stripe-webhook.test.ts create mode 100644 astro/src/test/api/verify-purchase.test.ts create mode 100644 astro/src/test/setup.ts create mode 100644 astro/vitest.config.ts diff --git a/astro/src/test/api/basic-api.test.ts b/astro/src/test/api/basic-api.test.ts new file mode 100644 index 00000000..feac78be --- /dev/null +++ b/astro/src/test/api/basic-api.test.ts @@ -0,0 +1,93 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +describe('Basic API Tests', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should test session-status API', async () => { + // Mock Stripe + const mockStripe = { + checkout: { + sessions: { + retrieve: vi.fn().mockResolvedValue({ + id: 'cs_test_session123', + status: 'complete', + customer_email: 'test@example.com', + metadata: { + purchaseCode: 'ESC-12345678', + kitType: 'build', + theme: 'corporate', + }, + payment_intent: 'pi_secret', + client_secret: 'cs_secret', + }), + }, + }, + }; + + vi.doMock('stripe', () => ({ + default: vi.fn().mockImplementation(() => mockStripe), + })); + + const { GET } = await import('../../pages/api/session-status'); + + const mockURL = new URL('http://localhost:4321/api/session-status?session_id=cs_test_session123'); + const request = { url: mockURL.toString() }; + + const response = await GET({ url: mockURL, request } as any); + expect(response.status).toBe(200); + + const data = await response.json(); + expect(data.status).toBe('complete'); + expect(data.customer_email).toBe('test@example.com'); + expect(data.payment_intent).toBeUndefined(); + }); + + it('should test create-checkout-session basic functionality', async () => { + // Mock Stripe + const mockStripe = { + checkout: { + sessions: { + create: vi.fn().mockResolvedValue({ + id: 'cs_test_session123', + client_secret: 'cs_test_secret456', + }), + }, + }, + }; + + vi.doMock('stripe', () => ({ + default: vi.fn().mockImplementation(() => mockStripe), + })); + + // Mock UUID + vi.doMock('uuid', () => ({ + v4: vi.fn().mockReturnValue('12345678-1234-1234-1234-123456789012'), + })); + + const { POST } = await import('../../pages/api/create-checkout-session'); + + const mockRequest = { + json: () => Promise.resolve({ + kitType: 'build', + theme: 'corporate', + organization: 'Test Org', + contactName: 'John Doe', + email: 'test@example.com', + }), + url: 'http://localhost:4321/api/create-checkout-session' + }; + + const response = await POST({ request: mockRequest } as any); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.purchaseCode).toMatch(/^ESC-[0-9A-F]{8}$/); + expect(data.clientSecret).toBe('cs_test_secret456'); + }); +}); diff --git a/astro/src/test/api/create-checkout-session.test.ts b/astro/src/test/api/create-checkout-session.test.ts new file mode 100644 index 00000000..5f5dfbe0 --- /dev/null +++ b/astro/src/test/api/create-checkout-session.test.ts @@ -0,0 +1,227 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +// Mock Stripe +const mockStripe = { + checkout: { + sessions: { + create: vi.fn(), + }, + }, +}; + +vi.mock('stripe', () => { + return { + default: vi.fn().mockImplementation(() => mockStripe), + }; +}); + +// Mock UUID +vi.mock('uuid', () => ({ + v4: vi.fn().mockReturnValue('12345678-1234-1234-1234-123456789012'), +})); + +describe('create-checkout-session API', () => { + beforeEach(() => { + vi.clearAllMocks(); + + // Reset Stripe mock with fresh implementation + mockStripe.checkout.sessions.create.mockReset(); + mockStripe.checkout.sessions.create.mockResolvedValue({ + id: 'cs_test_session123', + client_secret: 'cs_test_secret456', + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should create a checkout session successfully with valid data', async () => { + const { POST } = await import('../../pages/api/create-checkout-session'); + + const mockRequest = { + json: () => Promise.resolve({ + kitType: 'build', + theme: 'corporate', + organization: 'Test Org', + contactName: 'John Doe', + email: 'test@example.com', + }), + url: 'http://localhost:4321/api/create-checkout-session' + }; + + const response = await POST({ request: mockRequest } as any); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.purchaseCode).toMatch(/^ESC-[0-9A-F]{8}$/); + expect(data.clientSecret).toBe('cs_test_secret456'); + }); + + it('should return 400 for missing required fields', async () => { + const { POST } = await import('../../pages/api/create-checkout-session'); + + const mockRequest = { + json: () => Promise.resolve({ + kitType: 'build', + // Missing other required fields + }), + }; + + const response = await POST({ request: mockRequest } as any); + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data.error).toBe('Missing required fields'); + }); + + it('should correctly calculate prices for different kit types', async () => { + const { POST } = await import('../../pages/api/create-checkout-session'); + + // Test build kit + const buildKitData = { + kitType: 'build', + theme: 'corporate', + organization: 'Test Org', + contactName: 'John Doe', + email: 'test@example.com', + }; + + const mockRequest1 = new Request('http://localhost:4321/api/create-checkout-session', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(buildKitData) + }); + + const response1 = await POST({ request: mockRequest1 } as any); + console.log('Build kit response status:', response1.status); + + if (response1.status !== 200) { + const errorData = await response1.json(); + console.log('Build kit error:', errorData); + } + + expect(mockStripe.checkout.sessions.create).toHaveBeenCalledWith( + expect.objectContaining({ + line_items: expect.arrayContaining([ + expect.objectContaining({ + price_data: expect.objectContaining({ + unit_amount: 50000, + }), + }), + ]), + }) + ); + }); + + it('should map theme names correctly', async () => { + const { POST } = await import('../../pages/api/create-checkout-session'); + + const themes = [ + { input: 'corporate', expected: 'Corporate Conundrum' }, + { input: 'baking', expected: 'Baking Bonanza' }, + { input: 'picnic', expected: 'Puzzling Picnic' }, + { input: 'casino', expected: 'Cryptic Casino' }, + ]; + + for (const theme of themes) { + const requestData = { + kitType: 'build', + theme: theme.input, + organization: 'Test Org', + contactName: 'John Doe', + email: 'test@example.com', + }; + + const mockRequest = new Request('http://localhost:4321/api/create-checkout-session', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestData) + }); + + await POST({ request: mockRequest } as any); + + expect(mockStripe.checkout.sessions.create).toHaveBeenCalledWith( + expect.objectContaining({ + line_items: expect.arrayContaining([ + expect.objectContaining({ + price_data: expect.objectContaining({ + product_data: expect.objectContaining({ + name: expect.stringContaining(theme.expected), + }), + }), + }), + ]), + }) + ); + + // Clear for next iteration + mockStripe.checkout.sessions.create.mockClear(); + } + }); + + it('should include all metadata in the session', async () => { + const { POST } = await import('../../pages/api/create-checkout-session'); + + const requestData = { + kitType: 'build', + theme: 'corporate', + organization: 'Acme Corp', + contactName: 'Jane Smith', + email: 'jane@acme.com', + specialRequirements: 'Need wheelchair access', + }; + + const mockRequest = new Request('http://localhost:4321/api/create-checkout-session', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestData) + }); + + const response = await POST({ request: mockRequest } as any); + const responseData = await response.json(); + + expect(mockStripe.checkout.sessions.create).toHaveBeenCalledWith( + expect.objectContaining({ + customer_email: 'jane@acme.com', + metadata: expect.objectContaining({ + purchaseCode: responseData.purchaseCode, + kitType: 'build', + theme: 'corporate', + organization: 'Acme Corp', + contactName: 'Jane Smith', + email: 'jane@acme.com', + specialRequirements: 'Need wheelchair access', + }), + }) + ); + }); + + it('should handle Stripe API errors gracefully', async () => { + const { POST } = await import('../../pages/api/create-checkout-session'); + + mockStripe.checkout.sessions.create.mockRejectedValue( + new Error('Stripe API Error') + ); + + const requestData = { + kitType: 'build', + theme: 'corporate', + organization: 'Test Org', + contactName: 'John Doe', + email: 'test@example.com', + }; + + const mockRequest = new Request('http://localhost:4321/api/create-checkout-session', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestData) + }); + + const response = await POST({ request: mockRequest } as any); + const data = await response.json(); + + expect(response.status).toBe(500); + expect(data.error).toBe('Failed to create checkout session'); + }); +}); diff --git a/astro/src/test/api/digital-content.test.ts b/astro/src/test/api/digital-content.test.ts new file mode 100644 index 00000000..5a2facdc --- /dev/null +++ b/astro/src/test/api/digital-content.test.ts @@ -0,0 +1,316 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +// Mock the sessions from verify-purchase +const mockSessions = new Map(); + +vi.mock('./verify-purchase', () => ({ + sessions: mockSessions, +})); + +describe('digital-content API', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + mockSessions.clear(); + + // Setup valid session + const sessionData = { + purchaseCode: 'ESC-12345678', + email: 'test@example.com', + theme: 'corporate', + kitType: 'build', + organization: 'Test Corp', + createdAt: Date.now(), + expiresAt: Date.now() + (30 * 60 * 1000), + browserFingerprint: 'valid-fingerprint', + ipAddress: '127.0.0.1', + }; + + mockSessions.set('valid-session-id', sessionData); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it('should return digital content page for valid session', async () => { + const { GET } = await import('../../pages/api/digital-content'); + + const mockURL = new URL('http://localhost:4321/api/digital-content?session=valid-session-id'); + + const mockRequest = { + headers: new Map([ + ['cookie', 'session=valid-session-id'], + ['user-agent', 'Mozilla/5.0 Test Browser'], + ['accept-language', 'en-US,en;q=0.9'], + ['accept-encoding', 'gzip, deflate, br'], + ]), + }; + + const mockFingerprint = Buffer.from('Mozilla/5.0 Test Browser:en-US,en;q=0.9:gzip, deflate, br').toString('base64'); + mockSessions.set('valid-session-id', { + ...mockSessions.get('valid-session-id'), + browserFingerprint: mockFingerprint, + }); + + const response = await GET({ + request: mockRequest, + url: mockURL, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(200); + expect(response.headers.get('Content-Type')).toBe('text/html'); + expect(response.headers.get('Cache-Control')).toContain('private, no-cache'); + + const html = await response.text(); + expect(html).toContain('Test Corp'); // Organization name should be in content + expect(html).toContain('ESC-12345678'); // Purchase code should be in content + }); + + it('should return 401 for missing session parameter', async () => { + const { GET } = await import('../../pages/api/digital-content'); + + const mockURL = new URL('http://localhost:4321/api/digital-content'); + const mockRequest = { headers: new Map() }; + + const response = await GET({ + request: mockRequest, + url: mockURL, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(401); + + const responseText = await response.text(); + expect(responseText).toBe('Unauthorized'); + }); + + it('should return 401 for session cookie mismatch', async () => { + const { GET } = await import('../../pages/api/digital-content'); + + const mockURL = new URL('http://localhost:4321/api/digital-content?session=valid-session-id'); + + const mockRequest = { + headers: new Map([ + ['cookie', 'session=different-session-id'], // Mismatched cookie + ]), + }; + + const response = await GET({ + request: mockRequest, + url: mockURL, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(401); + + const responseText = await response.text(); + expect(responseText).toBe('Unauthorized - Session mismatch'); + }); + + it('should return 401 for invalid session ID', async () => { + const { GET } = await import('../../pages/api/digital-content'); + + const mockURL = new URL('http://localhost:4321/api/digital-content?session=invalid-session-id'); + + const mockRequest = { + headers: new Map([ + ['cookie', 'session=invalid-session-id'], + ]), + }; + + const response = await GET({ + request: mockRequest, + url: mockURL, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(401); + + const responseText = await response.text(); + expect(responseText).toBe('Session expired'); + }); + + it('should return 401 for expired session', async () => { + const { GET } = await import('../../pages/api/digital-content'); + + // Set session as expired + mockSessions.set('expired-session-id', { + purchaseCode: 'ESC-12345678', + email: 'test@example.com', + theme: 'corporate', + kitType: 'build', + organization: 'Test Corp', + createdAt: Date.now() - (60 * 60 * 1000), // 1 hour ago + expiresAt: Date.now() - (30 * 60 * 1000), // Expired 30 minutes ago + browserFingerprint: 'valid-fingerprint', + ipAddress: '127.0.0.1', + }); + + const mockURL = new URL('http://localhost:4321/api/digital-content?session=expired-session-id'); + + const mockRequest = { + headers: new Map([ + ['cookie', 'session=expired-session-id'], + ]), + }; + + const response = await GET({ + request: mockRequest, + url: mockURL, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(401); + + const responseText = await response.text(); + expect(responseText).toBe('Session expired'); + }); + + it('should return 401 for browser fingerprint mismatch', async () => { + const { GET } = await import('../../pages/api/digital-content'); + + const mockURL = new URL('http://localhost:4321/api/digital-content?session=valid-session-id'); + + const mockRequest = { + headers: new Map([ + ['cookie', 'session=valid-session-id'], + ['user-agent', 'Different Browser'], + ['accept-language', 'fr-FR,fr;q=0.9'], + ['accept-encoding', 'gzip'], + ]), + }; + + const response = await GET({ + request: mockRequest, + url: mockURL, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(401); + + const responseText = await response.text(); + expect(responseText).toBe('Unauthorized - Browser mismatch'); + }); + + it('should handle missing cookie header gracefully', async () => { + const { GET } = await import('../../pages/api/digital-content'); + + const mockURL = new URL('http://localhost:4321/api/digital-content?session=valid-session-id'); + + const mockRequest = { + headers: new Map([ + // No cookie header + ['user-agent', 'Mozilla/5.0 Test Browser'], + ]), + }; + + const response = await GET({ + request: mockRequest, + url: mockURL, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(401); + + const responseText = await response.text(); + expect(responseText).toBe('Unauthorized - Session mismatch'); + }); + + it('should generate different content for different themes', async () => { + const { GET } = await import('../../pages/api/digital-content'); + + const themes = ['corporate', 'baking', 'picnic', 'casino']; + + for (const theme of themes) { + const sessionId = `session-${theme}`; + const mockFingerprint = Buffer.from('Mozilla/5.0 Test Browser:en-US,en;q=0.9:gzip, deflate, br').toString('base64'); + + mockSessions.set(sessionId, { + purchaseCode: 'ESC-12345678', + email: 'test@example.com', + theme: theme, + kitType: 'build', + organization: 'Test Corp', + createdAt: Date.now(), + expiresAt: Date.now() + (30 * 60 * 1000), + browserFingerprint: mockFingerprint, + ipAddress: '127.0.0.1', + }); + + const mockURL = new URL(`http://localhost:4321/api/digital-content?session=${sessionId}`); + + const mockRequest = { + headers: new Map([ + ['cookie', `session=${sessionId}`], + ['user-agent', 'Mozilla/5.0 Test Browser'], + ['accept-language', 'en-US,en;q=0.9'], + ['accept-encoding', 'gzip, deflate, br'], + ]), + }; + + const response = await GET({ + request: mockRequest, + url: mockURL, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(200); + + const html = await response.text(); + expect(html).toContain(theme); // Theme should be reflected in content + } + }); + + it('should generate different content for different kit types', async () => { + const { GET } = await import('../../pages/api/digital-content'); + + const kitTypes = ['build', 'ready']; + + for (const kitType of kitTypes) { + const sessionId = `session-${kitType}`; + const mockFingerprint = Buffer.from('Mozilla/5.0 Test Browser:en-US,en;q=0.9:gzip, deflate, br').toString('base64'); + + mockSessions.set(sessionId, { + purchaseCode: 'ESC-12345678', + email: 'test@example.com', + theme: 'corporate', + kitType: kitType, + organization: 'Test Corp', + createdAt: Date.now(), + expiresAt: Date.now() + (30 * 60 * 1000), + browserFingerprint: mockFingerprint, + ipAddress: '127.0.0.1', + }); + + const mockURL = new URL(`http://localhost:4321/api/digital-content?session=${sessionId}`); + + const mockRequest = { + headers: new Map([ + ['cookie', `session=${sessionId}`], + ['user-agent', 'Mozilla/5.0 Test Browser'], + ['accept-language', 'en-US,en;q=0.9'], + ['accept-encoding', 'gzip, deflate, br'], + ]), + }; + + const response = await GET({ + request: mockRequest, + url: mockURL, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(200); + + const html = await response.text(); + + if (kitType === 'build') { + expect(html).toContain('Build-your-own'); + } else { + expect(html).toContain('Ready-made'); + } + } + }); +}); diff --git a/astro/src/test/api/download-material.test.ts b/astro/src/test/api/download-material.test.ts new file mode 100644 index 00000000..a34ad205 --- /dev/null +++ b/astro/src/test/api/download-material.test.ts @@ -0,0 +1,358 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import fs from 'fs/promises'; + +// Mock fs module +vi.mock('fs/promises', () => ({ + default: { + access: vi.fn(), + readFile: vi.fn(), + }, +})); + +// Mock the sessions from verify-purchase +const mockSessions = new Map(); + +// Mock the downloadTokens from generate-download-token +const mockDownloadTokens = new Map(); + +vi.mock('./verify-purchase', () => ({ + sessions: mockSessions, +})); + +vi.mock('./generate-download-token', () => ({ + downloadTokens: mockDownloadTokens, +})); + +describe('download-material API', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + mockSessions.clear(); + mockDownloadTokens.clear(); + + // Setup valid session and token + const sessionData = { + purchaseCode: 'ESC-12345678', + email: 'test@example.com', + theme: 'corporate', + kitType: 'build', + organization: 'Test Corp', + createdAt: Date.now(), + expiresAt: Date.now() + (30 * 60 * 1000), + browserFingerprint: 'valid-fingerprint', + ipAddress: '127.0.0.1', + }; + + const tokenData = { + sessionId: 'valid-session-id', + materialType: 'setup-guide', + createdAt: Date.now(), + expiresAt: Date.now() + (5 * 60 * 1000), + used: false, + }; + + mockSessions.set('valid-session-id', sessionData); + mockDownloadTokens.set('valid-token', tokenData); + + // Mock file system + (fs.access as any).mockResolvedValue(undefined); + (fs.readFile as any).mockResolvedValue(Buffer.from('PDF content')); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it('should download material with valid token', async () => { + const { GET } = await import('../../pages/api/download-material'); + + const mockURL = new URL('http://localhost:4321/api/download-material?token=valid-token'); + + const mockRequest = { + headers: new Map([ + ['user-agent', 'Mozilla/5.0 Test Browser'], + ['accept-language', 'en-US,en;q=0.9'], + ['accept-encoding', 'gzip, deflate, br'], + ]), + }; + + const mockFingerprint = Buffer.from('Mozilla/5.0 Test Browser:en-US,en;q=0.9:gzip, deflate, br').toString('base64'); + mockSessions.set('valid-session-id', { + ...mockSessions.get('valid-session-id'), + browserFingerprint: mockFingerprint, + }); + + const response = await GET({ + request: mockRequest, + url: mockURL, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(200); + expect(response.headers.get('Content-Type')).toBe('application/pdf'); + expect(response.headers.get('Content-Disposition')).toContain('attachment'); + + // Token should be marked as used + expect(mockDownloadTokens.get('valid-token').used).toBe(true); + }); + + it('should return 401 for missing download token', async () => { + const { GET } = await import('../../pages/api/download-material'); + + const mockURL = new URL('http://localhost:4321/api/download-material'); + const mockRequest = { headers: new Map() }; + + const response = await GET({ + request: mockRequest, + url: mockURL, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(401); + + const responseText = await response.text(); + expect(responseText).toBe('Missing download token'); + }); + + it('should return 401 for invalid download token', async () => { + const { GET } = await import('../../pages/api/download-material'); + + const mockURL = new URL('http://localhost:4321/api/download-material?token=invalid-token'); + const mockRequest = { headers: new Map() }; + + const response = await GET({ + request: mockRequest, + url: mockURL, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(401); + + const responseText = await response.text(); + expect(responseText).toBe('Invalid or expired download token'); + }); + + it('should return 401 for expired download token', async () => { + const { GET } = await import('../../pages/api/download-material'); + + // Create expired token + const expiredTokenData = { + sessionId: 'valid-session-id', + materialType: 'setup-guide', + createdAt: Date.now() - (10 * 60 * 1000), + expiresAt: Date.now() - (5 * 60 * 1000), // Expired 5 minutes ago + used: false, + }; + + mockDownloadTokens.set('expired-token', expiredTokenData); + + const mockURL = new URL('http://localhost:4321/api/download-material?token=expired-token'); + const mockRequest = { headers: new Map() }; + + const response = await GET({ + request: mockRequest, + url: mockURL, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(401); + + const responseText = await response.text(); + expect(responseText).toBe('Invalid or expired download token'); + }); + + it('should return 401 for already used token', async () => { + const { GET } = await import('../../pages/api/download-material'); + + // Mark token as used + mockDownloadTokens.get('valid-token').used = true; + + const mockURL = new URL('http://localhost:4321/api/download-material?token=valid-token'); + const mockRequest = { headers: new Map() }; + + const response = await GET({ + request: mockRequest, + url: mockURL, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(401); + + const responseText = await response.text(); + expect(responseText).toBe('Invalid or expired download token'); + }); + + it('should return 401 for expired session', async () => { + const { GET } = await import('../../pages/api/download-material'); + + // Set session as expired + mockSessions.set('valid-session-id', { + ...mockSessions.get('valid-session-id'), + expiresAt: Date.now() - (5 * 60 * 1000), // Expired 5 minutes ago + }); + + const mockURL = new URL('http://localhost:4321/api/download-material?token=valid-token'); + const mockRequest = { headers: new Map() }; + + const response = await GET({ + request: mockRequest, + url: mockURL, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(401); + + const responseText = await response.text(); + expect(responseText).toBe('Session expired'); + }); + + it('should return 401 for browser fingerprint mismatch', async () => { + const { GET } = await import('../../pages/api/download-material'); + + const mockURL = new URL('http://localhost:4321/api/download-material?token=valid-token'); + + const mockRequest = { + headers: new Map([ + ['user-agent', 'Different Browser'], + ['accept-language', 'fr-FR,fr;q=0.9'], + ['accept-encoding', 'gzip'], + ]), + }; + + const response = await GET({ + request: mockRequest, + url: mockURL, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(401); + + const responseText = await response.text(); + expect(responseText).toBe('Unauthorized - Browser mismatch'); + }); + + it('should return 404 for invalid material type', async () => { + const { GET } = await import('../../pages/api/download-material'); + + // Create token with invalid material type + const invalidTokenData = { + sessionId: 'valid-session-id', + materialType: 'invalid-material', + createdAt: Date.now(), + expiresAt: Date.now() + (5 * 60 * 1000), + used: false, + }; + + mockDownloadTokens.set('invalid-material-token', invalidTokenData); + + const mockURL = new URL('http://localhost:4321/api/download-material?token=invalid-material-token'); + + const mockRequest = { + headers: new Map([ + ['user-agent', 'Mozilla/5.0 Test Browser'], + ['accept-language', 'en-US,en;q=0.9'], + ['accept-encoding', 'gzip, deflate, br'], + ]), + }; + + const mockFingerprint = Buffer.from('Mozilla/5.0 Test Browser:en-US,en;q=0.9:gzip, deflate, br').toString('base64'); + mockSessions.set('valid-session-id', { + ...mockSessions.get('valid-session-id'), + browserFingerprint: mockFingerprint, + }); + + const response = await GET({ + request: mockRequest, + url: mockURL, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(404); + + const responseText = await response.text(); + expect(responseText).toBe('Not Found'); + }); + + it('should return 404 for missing file', async () => { + const { GET } = await import('../../pages/api/download-material'); + + (fs.access as any).mockRejectedValue(new Error('ENOENT: no such file or directory')); + + const mockURL = new URL('http://localhost:4321/api/download-material?token=valid-token'); + + const mockRequest = { + headers: new Map([ + ['user-agent', 'Mozilla/5.0 Test Browser'], + ['accept-language', 'en-US,en;q=0.9'], + ['accept-encoding', 'gzip, deflate, br'], + ]), + }; + + const mockFingerprint = Buffer.from('Mozilla/5.0 Test Browser:en-US,en;q=0.9:gzip, deflate, br').toString('base64'); + mockSessions.set('valid-session-id', { + ...mockSessions.get('valid-session-id'), + browserFingerprint: mockFingerprint, + }); + + const response = await GET({ + request: mockRequest, + url: mockURL, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(404); + + const responseText = await response.text(); + expect(responseText).toBe('File not found'); + }); + + it('should set correct headers for different material types', async () => { + const { GET } = await import('../../pages/api/download-material'); + + const materialTypes = [ + { type: 'setup-guide', expectedFilename: 'setup-guide-corporate.pdf' }, + { type: 'accessibility-guide', expectedFilename: 'accessibility-guide-corporate.pdf' }, + { type: 'prop-templates', expectedFilename: 'prop-templates-corporate.pdf' }, + { type: 'assembly-instructions', expectedFilename: 'assembly-instructions-corporate.pdf' }, + { type: 'shopping-list', expectedFilename: 'shopping-list-corporate.pdf' }, + ]; + + for (const material of materialTypes) { + const tokenData = { + sessionId: 'valid-session-id', + materialType: material.type, + createdAt: Date.now(), + expiresAt: Date.now() + (5 * 60 * 1000), + used: false, + }; + + mockDownloadTokens.set(`token-${material.type}`, tokenData); + + const mockURL = new URL(`http://localhost:4321/api/download-material?token=token-${material.type}`); + + const mockRequest = { + headers: new Map([ + ['user-agent', 'Mozilla/5.0 Test Browser'], + ['accept-language', 'en-US,en;q=0.9'], + ['accept-encoding', 'gzip, deflate, br'], + ]), + }; + + const mockFingerprint = Buffer.from('Mozilla/5.0 Test Browser:en-US,en;q=0.9:gzip, deflate, br').toString('base64'); + mockSessions.set('valid-session-id', { + ...mockSessions.get('valid-session-id'), + browserFingerprint: mockFingerprint, + }); + + const response = await GET({ + request: mockRequest, + url: mockURL, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(200); + expect(response.headers.get('Content-Disposition')).toContain(material.expectedFilename); + } + }); +}); diff --git a/astro/src/test/api/generate-download-token.test.ts b/astro/src/test/api/generate-download-token.test.ts new file mode 100644 index 00000000..4b936dd4 --- /dev/null +++ b/astro/src/test/api/generate-download-token.test.ts @@ -0,0 +1,250 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +// Mock UUID +vi.mock('uuid', () => ({ + v4: vi.fn().mockReturnValue('test-token-id-1234'), +})); + +// Mock the sessions from verify-purchase +const mockSessions = new Map(); + +vi.mock('./verify-purchase', () => ({ + sessions: mockSessions, +})); + +describe('generate-download-token API', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + mockSessions.clear(); + + // Setup a valid session + mockSessions.set('valid-session-id', { + purchaseCode: 'ESC-12345678', + email: 'test@example.com', + theme: 'corporate', + kitType: 'build', + organization: 'Test Corp', + createdAt: Date.now(), + expiresAt: Date.now() + (30 * 60 * 1000), // 30 minutes from now + browserFingerprint: 'valid-fingerprint', + ipAddress: '127.0.0.1', + }); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it('should generate a download token for valid session and material type', async () => { + const { POST } = await import('../../pages/api/generate-download-token'); + + const mockRequest = { + json: () => Promise.resolve({ + sessionId: 'valid-session-id', + materialType: 'setup-guide', + }), + headers: new Map([ + ['user-agent', 'Mozilla/5.0 Test Browser'], + ['accept-language', 'en-US,en;q=0.9'], + ['accept-encoding', 'gzip, deflate, br'], + ]), + }; + + const mockFingerprint = Buffer.from('Mozilla/5.0 Test Browser:en-US,en;q=0.9:gzip, deflate, br').toString('base64'); + mockSessions.set('valid-session-id', { + ...mockSessions.get('valid-session-id'), + browserFingerprint: mockFingerprint, + }); + + const response = await POST({ + request: mockRequest, + clientAddress: '127.0.0.1' + } as any); + + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.success).toBe(true); + expect(data.downloadToken).toBe('test-token-id-1234'); + expect(data.expiresAt).toBeDefined(); + }); + + it('should return 400 for missing parameters', async () => { + const { POST } = await import('../../pages/api/generate-download-token'); + + const testCases = [ + { sessionId: null, materialType: 'setup-guide' }, + { sessionId: 'valid-session-id', materialType: null }, + { sessionId: null, materialType: null }, + ]; + + for (const testCase of testCases) { + const mockRequest = { + json: () => Promise.resolve(testCase), + headers: new Map(), + }; + + const response = await POST({ + request: mockRequest, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(400); + + const responseText = await response.text(); + expect(responseText).toBe('Missing parameters'); + } + }); + + it('should return 401 for invalid session ID', async () => { + const { POST } = await import('../../pages/api/generate-download-token'); + + const mockRequest = { + json: () => Promise.resolve({ + sessionId: 'invalid-session-id', + materialType: 'setup-guide', + }), + headers: new Map(), + }; + + const response = await POST({ + request: mockRequest, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(401); + + const responseText = await response.text(); + expect(responseText).toBe('Invalid session'); + }); + + it('should return 401 for expired session', async () => { + const { POST } = await import('../../pages/api/generate-download-token'); + + // Set expired session + mockSessions.set('expired-session-id', { + purchaseCode: 'ESC-12345678', + email: 'test@example.com', + theme: 'corporate', + kitType: 'build', + organization: 'Test Corp', + createdAt: Date.now() - (60 * 60 * 1000), // 1 hour ago + expiresAt: Date.now() - (30 * 60 * 1000), // Expired 30 minutes ago + browserFingerprint: 'valid-fingerprint', + ipAddress: '127.0.0.1', + }); + + const mockRequest = { + json: () => Promise.resolve({ + sessionId: 'expired-session-id', + materialType: 'setup-guide', + }), + headers: new Map(), + }; + + const response = await POST({ + request: mockRequest, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(401); + + const responseText = await response.text(); + expect(responseText).toBe('Invalid session'); + }); + + it('should return 401 for browser fingerprint mismatch', async () => { + const { POST } = await import('../../pages/api/generate-download-token'); + + const mockRequest = { + json: () => Promise.resolve({ + sessionId: 'valid-session-id', + materialType: 'setup-guide', + }), + headers: new Map([ + ['user-agent', 'Different Browser'], + ['accept-language', 'fr-FR,fr;q=0.9'], + ['accept-encoding', 'gzip'], + ]), + }; + + const response = await POST({ + request: mockRequest, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(401); + + const responseText = await response.text(); + expect(responseText).toBe('Unauthorized'); + }); + + it('should create token with correct expiration time', async () => { + const { POST } = await import('../../pages/api/generate-download-token'); + + const currentTime = Date.now(); + vi.setSystemTime(currentTime); + + const mockRequest = { + json: () => Promise.resolve({ + sessionId: 'valid-session-id', + materialType: 'setup-guide', + }), + headers: new Map([ + ['user-agent', 'Mozilla/5.0 Test Browser'], + ['accept-language', 'en-US,en;q=0.9'], + ['accept-encoding', 'gzip, deflate, br'], + ]), + }; + + const mockFingerprint = Buffer.from('Mozilla/5.0 Test Browser:en-US,en;q=0.9:gzip, deflate, br').toString('base64'); + mockSessions.set('valid-session-id', { + ...mockSessions.get('valid-session-id'), + browserFingerprint: mockFingerprint, + }); + + const response = await POST({ + request: mockRequest, + clientAddress: '127.0.0.1' + } as any); + + const data = await response.json(); + + expect(response.status).toBe(200); + + // Token should expire in 5 minutes (300,000 ms) + const expectedExpiration = currentTime + (5 * 60 * 1000); + expect(data.expiresAt).toBe(expectedExpiration); + }); + + it('should handle missing headers gracefully', async () => { + const { POST } = await import('../../pages/api/generate-download-token'); + + const mockRequest = { + json: () => Promise.resolve({ + sessionId: 'valid-session-id', + materialType: 'setup-guide', + }), + headers: new Map(), // No headers provided + }; + + // Set session with empty fingerprint to match + const emptyFingerprint = Buffer.from('::').toString('base64'); + mockSessions.set('valid-session-id', { + ...mockSessions.get('valid-session-id'), + browserFingerprint: emptyFingerprint, + }); + + const response = await POST({ + request: mockRequest, + clientAddress: '127.0.0.1' + } as any); + + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.success).toBe(true); + }); +}); diff --git a/astro/src/test/api/save-purchase-data.test.ts b/astro/src/test/api/save-purchase-data.test.ts new file mode 100644 index 00000000..6b592adf --- /dev/null +++ b/astro/src/test/api/save-purchase-data.test.ts @@ -0,0 +1,175 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +// Mock the stripe-webhook module +const mockHandleSuccessfulPayment = vi.fn(); + +vi.mock('../../pages/api/stripe-webhook', () => ({ + handleSuccessfulPayment: mockHandleSuccessfulPayment, +})); + +// Mock Stripe +const mockStripe = { + checkout: { + sessions: { + retrieve: vi.fn(), + }, + }, +}; + +vi.mock('stripe', () => ({ + default: vi.fn().mockImplementation(() => mockStripe), +})); + +describe('save-purchase-data API', () => { + beforeEach(() => { + vi.clearAllMocks(); + // Reset the mock for each test + mockHandleSuccessfulPayment.mockResolvedValue(undefined); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should save purchase data for a valid paid session', async () => { + const { POST } = await import('../../pages/api/save-purchase-data'); + + const mockSession = { + id: 'cs_test_session123', + payment_status: 'paid', + metadata: { + purchaseCode: 'ESC-12345678', + }, + }; + + mockStripe.checkout.sessions.retrieve.mockResolvedValue(mockSession); + + const mockRequest = { + json: () => Promise.resolve({ + sessionId: 'cs_test_session123', + }), + }; + + const response = await POST({ request: mockRequest } as any); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.success).toBe(true); + expect(mockHandleSuccessfulPayment).toHaveBeenCalledWith(mockSession); + }); + + it('should return 400 for missing session ID', async () => { + const { POST } = await import('../../pages/api/save-purchase-data'); + + const mockRequest = { + json: () => Promise.resolve({}), + }; + + const response = await POST({ request: mockRequest } as any); + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data.error).toBe('Session ID required'); + }); + + it('should return 400 for unpaid session', async () => { + const { POST } = await import('../../pages/api/save-purchase-data'); + + const mockSession = { + id: 'cs_test_session123', + payment_status: 'unpaid', + }; + + mockStripe.checkout.sessions.retrieve.mockResolvedValue(mockSession); + + const mockRequest = { + json: () => Promise.resolve({ + sessionId: 'cs_test_session123', + }), + }; + + const response = await POST({ request: mockRequest } as any); + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data.error).toBe('Payment not completed'); + }); + + it('should handle Stripe API errors', async () => { + const { POST } = await import('../../pages/api/save-purchase-data'); + + mockStripe.checkout.sessions.retrieve.mockRejectedValue( + new Error('No such checkout session') + ); + + const mockRequest = { + json: () => Promise.resolve({ + sessionId: 'cs_invalid', + }), + }; + + const response = await POST({ request: mockRequest } as any); + const data = await response.json(); + + expect(response.status).toBe(500); + expect(data.error).toBe('Failed to process purchase'); + }); + + it('should handle webhook handler errors', async () => { + const { POST } = await import('../../pages/api/save-purchase-data'); + + const mockSession = { + id: 'cs_test_session123', + payment_status: 'paid', + }; + + mockStripe.checkout.sessions.retrieve.mockResolvedValue(mockSession); + mockHandleSuccessfulPayment.mockRejectedValue(new Error('Failed to save data')); + + const mockRequest = { + json: () => Promise.resolve({ + sessionId: 'cs_test_session123', + }), + }; + + const response = await POST({ request: mockRequest } as any); + const data = await response.json(); + + expect(response.status).toBe(500); + expect(data.error).toBe('Failed to process purchase'); + }); + + it('should validate payment status correctly', async () => { + const { POST } = await import('../../pages/api/save-purchase-data'); + + const testCases = [ + { payment_status: 'paid', expectedStatus: 200 }, + { payment_status: 'unpaid', expectedStatus: 400 }, + { payment_status: 'no_payment_required', expectedStatus: 400 }, + ]; + + for (const testCase of testCases) { + const mockSession = { + id: 'cs_test_session123', + payment_status: testCase.payment_status, + }; + + mockStripe.checkout.sessions.retrieve.mockResolvedValue(mockSession); + + // Reset the mock for successful cases + if (testCase.payment_status === 'paid') { + mockHandleSuccessfulPayment.mockResolvedValue(undefined); + } + + const mockRequest = { + json: () => Promise.resolve({ + sessionId: 'cs_test_session123', + }), + }; + + const response = await POST({ request: mockRequest } as any); + + expect(response.status).toBe(testCase.expectedStatus); + } + }); +}); diff --git a/astro/src/test/api/session-status.test.ts b/astro/src/test/api/session-status.test.ts new file mode 100644 index 00000000..3d07c6ec --- /dev/null +++ b/astro/src/test/api/session-status.test.ts @@ -0,0 +1,146 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +// Mock Stripe +const mockStripe = { + checkout: { + sessions: { + retrieve: vi.fn(), + }, + }, +}; + +vi.mock('stripe', () => ({ + default: vi.fn().mockImplementation(() => mockStripe), +})); + +describe('session-status API', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should retrieve and filter Stripe session data correctly', async () => { + const { GET } = await import('../../pages/api/session-status'); + + const mockStripeSession = { + id: 'cs_test_session123', + status: 'complete', + customer_email: 'test@example.com', + metadata: { + purchaseCode: 'ESC-12345678', + kitType: 'build', + theme: 'corporate', + }, + payment_intent: 'pi_secret_dont_expose', + client_secret: 'cs_secret_dont_expose', + }; + + mockStripe.checkout.sessions.retrieve.mockResolvedValue(mockStripeSession); + + const mockURL = new URL('http://localhost:4321/api/session-status?session_id=cs_test_session123'); + const request = { url: mockURL.toString() }; + + const response = await GET({ url: mockURL, request } as any); + expect(response.status).toBe(200); + + const data = await response.json(); + + // Should only return safe, non-sensitive data + expect(data).toEqual({ + status: 'complete', + metadata: { + purchaseCode: 'ESC-12345678', + kitType: 'build', + theme: 'corporate', + }, + customer_email: 'test@example.com', + }); + + // Should NOT include sensitive fields + expect(data.payment_intent).toBeUndefined(); + expect(data.client_secret).toBeUndefined(); + }); + + it('should return 400 for missing session_id', async () => { + const { GET } = await import('../../pages/api/session-status'); + + const mockURL = new URL('http://localhost:4321/api/session-status'); + const request = { url: mockURL.toString() }; + + const response = await GET({ url: mockURL, request } as any); + expect(response.status).toBe(400); + + const data = await response.json(); + expect(data.error).toBe('Session ID is required'); + }); + + it('should handle Stripe retrieval errors', async () => { + const { GET } = await import('../../pages/api/session-status'); + + mockStripe.checkout.sessions.retrieve.mockRejectedValue( + new Error('No such checkout session: cs_invalid') + ); + + const mockURL = new URL('http://localhost:4321/api/session-status?session_id=cs_invalid'); + const request = { url: mockURL.toString() }; + + const response = await GET({ url: mockURL, request } as any); + expect(response.status).toBe(500); + + const data = await response.json(); + expect(data.error).toBe('Failed to retrieve session'); + }); + + it('should handle sessions with null metadata', async () => { + const { GET } = await import('../../pages/api/session-status'); + + const mockStripeSession = { + id: 'cs_test_session123', + status: 'incomplete', + customer_email: null, + metadata: null, + }; + + mockStripe.checkout.sessions.retrieve.mockResolvedValue(mockStripeSession); + + const mockURL = new URL('http://localhost:4321/api/session-status?session_id=cs_test_session123'); + const request = { url: mockURL.toString() }; + + const response = await GET({ url: mockURL, request } as any); + expect(response.status).toBe(200); + + const data = await response.json(); + expect(data).toEqual({ + status: 'incomplete', + metadata: null, + customer_email: null, + }); + }); + + it('should handle sessions with incomplete status', async () => { + const { GET } = await import('../../pages/api/session-status'); + + const mockStripeSession = { + id: 'cs_test_session123', + status: 'expired', + customer_email: 'test@example.com', + metadata: { + purchaseCode: 'ESC-12345678', + }, + }; + + mockStripe.checkout.sessions.retrieve.mockResolvedValue(mockStripeSession); + + const mockURL = new URL('http://localhost:4321/api/session-status?session_id=cs_test_session123'); + const request = { url: mockURL.toString() }; + + const response = await GET({ url: mockURL, request } as any); + expect(response.status).toBe(200); + + const data = await response.json(); + expect(data.status).toBe('expired'); + }); +}); diff --git a/astro/src/test/api/stripe-webhook.test.ts b/astro/src/test/api/stripe-webhook.test.ts new file mode 100644 index 00000000..b2988752 --- /dev/null +++ b/astro/src/test/api/stripe-webhook.test.ts @@ -0,0 +1,393 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import fs from 'fs/promises'; + +// Mock fs module +vi.mock('fs/promises', () => ({ + default: { + mkdir: vi.fn(), + writeFile: vi.fn(), + }, +})); + +// Mock Stripe +const mockStripe = { + webhooks: { + constructEvent: vi.fn(), + }, +}; + +vi.mock('stripe', () => ({ + default: vi.fn().mockImplementation(() => mockStripe), +})); + +// Mock fetch for email sending +global.fetch = vi.fn(); + +describe('stripe-webhook API', () => { + beforeEach(() => { + vi.clearAllMocks(); + + // Mock successful file operations + (fs.mkdir as any).mockResolvedValue(undefined); + (fs.writeFile as any).mockResolvedValue(undefined); + + // Mock successful email sending + (global.fetch as any).mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ success: true }), + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should handle checkout.session.completed event successfully', async () => { + const { POST } = await import('../../pages/api/stripe-webhook'); + + const mockEvent = { + type: 'checkout.session.completed', + data: { + object: { + id: 'cs_test_session123', + metadata: { + purchaseCode: 'ESC-12345678', + kitType: 'build', + theme: 'corporate', + organization: 'Test Corp', + contactName: 'John Doe', + email: 'test@example.com', + specialRequirements: 'Wheelchair access', + }, + amount_total: 50000, + currency: 'usd', + payment_status: 'paid', + }, + }, + }; + + mockStripe.webhooks.constructEvent.mockReturnValue(mockEvent); + + const mockRequest = { + text: () => Promise.resolve(JSON.stringify(mockEvent)), + headers: new Map([ + ['stripe-signature', 'test_signature'], + ]), + }; + + const response = await POST({ request: mockRequest } as any); + + expect(response.status).toBe(200); + + const responseText = await response.text(); + expect(responseText).toBe('Webhook handled successfully'); + + // Verify purchase data was stored + expect(fs.writeFile).toHaveBeenCalledWith( + expect.stringMatching(/ESC-12345678\.json$/), + expect.stringContaining('"purchaseCode":"ESC-12345678"') + ); + }); + + it('should handle webhook without signature verification when secret not configured', async () => { + const { POST } = await import('../../pages/api/stripe-webhook'); + + const mockEvent = { + type: 'checkout.session.completed', + data: { + object: { + id: 'cs_test_session123', + metadata: { + purchaseCode: 'ESC-12345678', + kitType: 'build', + theme: 'corporate', + organization: 'Test Corp', + contactName: 'John Doe', + email: 'test@example.com', + specialRequirements: '', + }, + amount_total: 50000, + currency: 'usd', + payment_status: 'paid', + }, + }, + }; + + // Simulate missing webhook secret + Object.defineProperty(import.meta, 'env', { + value: { + STRIPE_SECRET_KEY: 'sk_test_fake_stripe_key', + STRIPE_WEBHOOK_SECRET: undefined, // No webhook secret + }, + writable: true, + }); + + const mockRequest = { + text: () => Promise.resolve(JSON.stringify(mockEvent)), + headers: new Map([ + ['stripe-signature', 'test_signature'], + ]), + }; + + const response = await POST({ request: mockRequest } as any); + + expect(response.status).toBe(200); + expect(mockStripe.webhooks.constructEvent).not.toHaveBeenCalled(); + }); + + it('should return 400 for invalid webhook signature', async () => { + const { POST } = await import('../../pages/api/stripe-webhook'); + + mockStripe.webhooks.constructEvent.mockImplementation(() => { + throw new Error('Invalid signature'); + }); + + const mockRequest = { + text: () => Promise.resolve('invalid event body'), + headers: new Map([ + ['stripe-signature', 'invalid_signature'], + ]), + }; + + const response = await POST({ request: mockRequest } as any); + + expect(response.status).toBe(400); + + const responseText = await response.text(); + expect(responseText).toBe('Webhook signature verification failed'); + }); + + it('should ignore unhandled event types', async () => { + const { POST } = await import('../../pages/api/stripe-webhook'); + + const mockEvent = { + type: 'payment_intent.succeeded', // Different event type + data: { + object: { + id: 'pi_test_payment123', + }, + }, + }; + + mockStripe.webhooks.constructEvent.mockReturnValue(mockEvent); + + const mockRequest = { + text: () => Promise.resolve(JSON.stringify(mockEvent)), + headers: new Map([ + ['stripe-signature', 'test_signature'], + ]), + }; + + const response = await POST({ request: mockRequest } as any); + + expect(response.status).toBe(200); + + const responseText = await response.text(); + expect(responseText).toBe('Webhook handled successfully'); + + // Should not store any purchase data for unhandled events + expect(fs.writeFile).not.toHaveBeenCalled(); + }); + + it('should handle file system errors gracefully', async () => { + const { POST } = await import('../../pages/api/stripe-webhook'); + + (fs.mkdir as any).mockRejectedValue(new Error('Permission denied')); + + const mockEvent = { + type: 'checkout.session.completed', + data: { + object: { + id: 'cs_test_session123', + metadata: { + purchaseCode: 'ESC-12345678', + kitType: 'build', + theme: 'corporate', + organization: 'Test Corp', + contactName: 'John Doe', + email: 'test@example.com', + specialRequirements: '', + }, + amount_total: 50000, + currency: 'usd', + payment_status: 'paid', + }, + }, + }; + + mockStripe.webhooks.constructEvent.mockReturnValue(mockEvent); + + const mockRequest = { + text: () => Promise.resolve(JSON.stringify(mockEvent)), + headers: new Map([ + ['stripe-signature', 'test_signature'], + ]), + }; + + const response = await POST({ request: mockRequest } as any); + + expect(response.status).toBe(500); + + const responseText = await response.text(); + expect(responseText).toBe('Webhook handler failed'); + }); + + it('should handle email sending failures gracefully', async () => { + const { POST } = await import('../../pages/api/stripe-webhook'); + + // Mock email sending failure + (global.fetch as any).mockResolvedValue({ + ok: false, + status: 500, + }); + + const mockEvent = { + type: 'checkout.session.completed', + data: { + object: { + id: 'cs_test_session123', + metadata: { + purchaseCode: 'ESC-12345678', + kitType: 'build', + theme: 'corporate', + organization: 'Test Corp', + contactName: 'John Doe', + email: 'test@example.com', + specialRequirements: '', + }, + amount_total: 50000, + currency: 'usd', + payment_status: 'paid', + }, + }, + }; + + mockStripe.webhooks.constructEvent.mockReturnValue(mockEvent); + + const mockRequest = { + text: () => Promise.resolve(JSON.stringify(mockEvent)), + headers: new Map([ + ['stripe-signature', 'test_signature'], + ]), + }; + + const response = await POST({ request: mockRequest } as any); + + // Should still succeed even if email fails + expect(response.status).toBe(200); + + // Purchase data should still be stored + expect(fs.writeFile).toHaveBeenCalled(); + }); + + it('should store complete purchase data with all metadata', async () => { + const { POST } = await import('../../pages/api/stripe-webhook'); + + const mockEvent = { + type: 'checkout.session.completed', + data: { + object: { + id: 'cs_test_session123', + metadata: { + purchaseCode: 'ESC-12345678', + kitType: 'ready', + theme: 'baking', + organization: 'Bakery Corp', + contactName: 'Jane Baker', + email: 'jane@bakery.com', + specialRequirements: 'Gluten-free options', + }, + amount_total: 350000, + currency: 'usd', + payment_status: 'paid', + }, + }, + }; + + mockStripe.webhooks.constructEvent.mockReturnValue(mockEvent); + + const mockRequest = { + text: () => Promise.resolve(JSON.stringify(mockEvent)), + headers: new Map([ + ['stripe-signature', 'test_signature'], + ]), + }; + + await POST({ request: mockRequest } as any); + + // Verify all data is included + expect(fs.writeFile).toHaveBeenCalledWith( + expect.stringMatching(/ESC-12345678\.json$/), + expect.stringMatching(/"purchaseCode":"ESC-12345678"/) + ); + + const writeCall = (fs.writeFile as any).mock.calls[0]; + const savedData = JSON.parse(writeCall[1]); + + expect(savedData).toMatchObject({ + sessionId: 'cs_test_session123', + purchaseCode: 'ESC-12345678', + kitType: 'ready', + theme: 'baking', + organization: 'Bakery Corp', + contactName: 'Jane Baker', + email: 'jane@bakery.com', + specialRequirements: 'Gluten-free options', + amountPaid: 350000, + currency: 'usd', + paymentStatus: 'paid', + }); + + expect(savedData.createdAt).toBeDefined(); + }); + + it('should send confirmation email with correct data', async () => { + const { POST } = await import('../../pages/api/stripe-webhook'); + + const mockEvent = { + type: 'checkout.session.completed', + data: { + object: { + id: 'cs_test_session123', + metadata: { + purchaseCode: 'ESC-12345678', + kitType: 'build', + theme: 'corporate', + organization: 'Test Corp', + contactName: 'John Doe', + email: 'test@example.com', + specialRequirements: '', + }, + amount_total: 50000, + currency: 'usd', + payment_status: 'paid', + }, + }, + }; + + mockStripe.webhooks.constructEvent.mockReturnValue(mockEvent); + + const mockRequest = { + text: () => Promise.resolve(JSON.stringify(mockEvent)), + headers: new Map([ + ['stripe-signature', 'test_signature'], + ]), + }; + + await POST({ request: mockRequest } as any); + + expect(global.fetch).toHaveBeenCalledWith('/api/send-confirmation-email', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email: 'test@example.com', + purchaseCode: 'ESC-12345678', + organization: 'Test Corp', + kitType: 'build', + theme: 'corporate', + }), + }); + }); +}); diff --git a/astro/src/test/api/verify-purchase.test.ts b/astro/src/test/api/verify-purchase.test.ts new file mode 100644 index 00000000..e97f9ec7 --- /dev/null +++ b/astro/src/test/api/verify-purchase.test.ts @@ -0,0 +1,265 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import fs from 'fs/promises'; + +// Mock fs module +vi.mock('fs/promises', () => ({ + default: { + readFile: vi.fn(), + }, +})); + +// Mock UUID +const mockUuid = vi.fn().mockReturnValue('test-session-id-1234'); + +vi.mock('uuid', () => ({ + v4: mockUuid, +})); + +describe('verify-purchase API', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + + // Reset UUID mock + mockUuid.mockReturnValue('test-session-id-1234'); + + // Mock successful file read + const mockPurchaseData = { + purchaseCode: 'ESC-12345678', + email: 'test@example.com', + theme: 'corporate', + kitType: 'build', + organization: 'Test Corp', + createdAt: Date.now(), + }; + + (fs.readFile as any).mockResolvedValue(JSON.stringify(mockPurchaseData)); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it('should verify a valid purchase successfully', async () => { + const { POST } = await import('../../pages/api/verify-purchase'); + + const mockRequest = { + json: () => Promise.resolve({ + purchaseCode: 'ESC-12345678', + email: 'test@example.com', + }), + headers: { + get: (key: string) => { + const headers: Record = { + 'user-agent': 'Mozilla/5.0 Test Browser', + 'accept-language': 'en-US,en;q=0.9', + 'accept-encoding': 'gzip, deflate, br', + }; + return headers[key] || ''; + }, + }, + }; + + const response = await POST({ + request: mockRequest, + clientAddress: '127.0.0.1' + } as any); + + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.valid).toBe(true); + expect(data.sessionId).toBe('test-session-id-1234'); + expect(data.kitType).toBe('build'); + expect(data.theme).toBe('corporate'); + expect(data.organization).toBe('Test Corp'); + }); + + it('should return 400 for missing purchase code', async () => { + const { POST } = await import('../../pages/api/verify-purchase'); + + const mockRequest = { + json: () => Promise.resolve({ + email: 'test@example.com', + }), + headers: { + get: () => '', + }, + }; + + const response = await POST({ + request: mockRequest, + clientAddress: '127.0.0.1' + } as any); + + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data.error).toBe('Invalid purchase code or email address'); + }); + + it('should return 400 for missing email', async () => { + const { POST } = await import('../../pages/api/verify-purchase'); + + const mockRequest = { + json: () => Promise.resolve({ + purchaseCode: 'ESC-12345678', + }), + headers: { + get: () => '', + }, + }; + + const response = await POST({ + request: mockRequest, + clientAddress: '127.0.0.1' + } as any); + + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data.error).toBe('Invalid purchase code or email address'); + }); + + it('should return 404 for non-existent purchase code', async () => { + const { POST } = await import('../../pages/api/verify-purchase'); + + // Mock file read error (file not found) + (fs.readFile as any).mockRejectedValue(new Error('ENOENT: no such file or directory')); + + const mockRequest = { + json: () => Promise.resolve({ + purchaseCode: 'ESC-INVALID', + email: 'test@example.com', + }), + headers: { + get: () => '', + }, + }; + + const response = await POST({ + request: mockRequest, + clientAddress: '127.0.0.1' + } as any); + + const data = await response.json(); + + expect(response.status).toBe(404); + expect(data.error).toBe('Invalid purchase code or email address'); + }); + + it('should return 403 for email mismatch', async () => { + const { POST } = await import('../../pages/api/verify-purchase'); + + const mockRequest = { + json: () => Promise.resolve({ + purchaseCode: 'ESC-12345678', + email: 'wrong@example.com', + }), + headers: { + get: () => '', + }, + }; + + const response = await POST({ + request: mockRequest, + clientAddress: '127.0.0.1' + } as any); + + const data = await response.json(); + + expect(response.status).toBe(403); + expect(data.error).toBe('Invalid purchase code or email address'); + }); + + it('should handle case-insensitive email comparison', async () => { + const { POST } = await import('../../pages/api/verify-purchase'); + + const mockRequest = { + json: () => Promise.resolve({ + purchaseCode: 'ESC-12345678', + email: 'TEST@EXAMPLE.COM', // Uppercase email + }), + headers: { + get: (key: string) => { + const headers: Record = { + 'user-agent': 'Mozilla/5.0 Test Browser', + 'accept-language': 'en-US,en;q=0.9', + 'accept-encoding': 'gzip, deflate, br', + }; + return headers[key] || ''; + }, + }, + }; + + const response = await POST({ + request: mockRequest, + clientAddress: '127.0.0.1' + } as any); + + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.valid).toBe(true); + }); + + it('should create a secure session with proper expiration', async () => { + const { POST } = await import('../../pages/api/verify-purchase'); + + const currentTime = Date.now(); + vi.setSystemTime(currentTime); + + const mockRequest = { + json: () => Promise.resolve({ + purchaseCode: 'ESC-12345678', + email: 'test@example.com', + }), + headers: { + get: (key: string) => { + const headers: Record = { + 'user-agent': 'Mozilla/5.0 Test Browser', + 'accept-language': 'en-US,en;q=0.9', + 'accept-encoding': 'gzip, deflate, br', + }; + return headers[key] || ''; + }, + }, + }; + + const response = await POST({ + request: mockRequest, + clientAddress: '127.0.0.1' + } as any); + + expect(response.status).toBe(200); + + // Check that session cookie is set + const cookies = response.headers.get('Set-Cookie'); + expect(cookies).toContain('session=test-session-id-1234'); + expect(cookies).toContain('HttpOnly'); + expect(cookies).toContain('Secure'); + expect(cookies).toContain('SameSite=Strict'); + }); + + it('should handle JSON parsing errors gracefully', async () => { + const { POST } = await import('../../pages/api/verify-purchase'); + + const mockRequest = { + json: () => Promise.reject(new Error('Invalid JSON')), + headers: { + get: () => '', + }, + }; + + const response = await POST({ + request: mockRequest, + clientAddress: '127.0.0.1' + } as any); + + const data = await response.json(); + + expect(response.status).toBe(500); + expect(data.error).toBe('Invalid purchase code or email address'); + }); +}); diff --git a/astro/src/test/setup.ts b/astro/src/test/setup.ts new file mode 100644 index 00000000..f73f148d --- /dev/null +++ b/astro/src/test/setup.ts @@ -0,0 +1,19 @@ +// Test setup file +import { vi } from 'vitest'; + +// Mock environment variables +Object.defineProperty(import.meta, 'env', { + value: { + STRIPE_SECRET_KEY: 'sk_test_fake_stripe_key', + STRIPE_WEBHOOK_SECRET: 'whsec_fake_webhook_secret', + }, + writable: true, +}); + +// Mock console methods to reduce noise during tests +global.console = { + ...console, + log: vi.fn(), + error: vi.fn(), + warn: vi.fn(), +}; diff --git a/astro/vitest.config.ts b/astro/vitest.config.ts new file mode 100644 index 00000000..53375af2 --- /dev/null +++ b/astro/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config'; +import path from 'path'; + +export default defineConfig({ + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./src/test/setup.ts'], + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +}); From f6d14d6a91752b290183a4d834b04f8dbbafda4b Mon Sep 17 00:00:00 2001 From: gummdev Date: Thu, 9 Oct 2025 14:43:01 -0400 Subject: [PATCH 09/27] Remove merge conflict marker in icon list --- astro/astro.config.mjs | 3 --- 1 file changed, 3 deletions(-) diff --git a/astro/astro.config.mjs b/astro/astro.config.mjs index f00569c4..3c0bfdc5 100644 --- a/astro/astro.config.mjs +++ b/astro/astro.config.mjs @@ -74,13 +74,10 @@ export default defineConfig({ // Social Media 'facebook', 'instagram', 'linkedin', 'rss-fill', 'tiktok', 'youtube','globe', 'mastodon', 'twitter', // Descriptive -<<<<<<< HEAD 'gift-fill', 'pencil-fill', 'people-fill', 'person-fill', // Additional icons 'check-circle-fill', 'exclamation-triangle-fill', 'file-text-fill', 'display-fill', 'puzzle-fill', 'tools', -======= 'gift-fill', 'pencil-fill', 'people-fill', 'person-fill', 'puzzle-fill', 'stopwatch-fill', 'tools', ->>>>>>> origin/main ], // CoreUI Brands cib: [ From 091ec9c398d7d242fdd49cc7f13299334d867474 Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Thu, 9 Oct 2025 15:22:25 -0400 Subject: [PATCH 10/27] Set output option to 'server' in Astro configuration --- astro/astro.config.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/astro/astro.config.mjs b/astro/astro.config.mjs index 3c0bfdc5..283a7f18 100644 --- a/astro/astro.config.mjs +++ b/astro/astro.config.mjs @@ -33,6 +33,7 @@ const botsToDisallow = [ export default defineConfig({ site: "https://accessiblecommunity.org", + output: 'server', adapter: netlify(), server: { From a5a92efe9912ec89f8d13da14f1858da17091740 Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Thu, 16 Oct 2025 13:05:57 -0400 Subject: [PATCH 11/27] Rework session mocking in tests and add access control middleware tests --- astro/src/test/api/digital-content.test.ts | 9 +- astro/src/test/api/download-material.test.ts | 9 +- .../test/api/generate-download-token.test.ts | 10 ++- astro/src/test/api/verify-purchase.test.ts | 4 +- .../test/middleware/access-control.test.ts | 88 +++++++++++++++++++ 5 files changed, 106 insertions(+), 14 deletions(-) create mode 100644 astro/src/test/middleware/access-control.test.ts diff --git a/astro/src/test/api/digital-content.test.ts b/astro/src/test/api/digital-content.test.ts index 5a2facdc..4144a739 100644 --- a/astro/src/test/api/digital-content.test.ts +++ b/astro/src/test/api/digital-content.test.ts @@ -1,10 +1,11 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -// Mock the sessions from verify-purchase -const mockSessions = new Map(); +// Mock the session store +const mockSessions = new Map(); -vi.mock('./verify-purchase', () => ({ - sessions: mockSessions, +vi.mock('src/lib/session-store', () => ({ + getSession: (sessionId: string) => mockSessions.get(sessionId), + invalidateSession: (sessionId: string) => mockSessions.delete(sessionId), })); describe('digital-content API', () => { diff --git a/astro/src/test/api/download-material.test.ts b/astro/src/test/api/download-material.test.ts index a34ad205..908b5f55 100644 --- a/astro/src/test/api/download-material.test.ts +++ b/astro/src/test/api/download-material.test.ts @@ -9,14 +9,15 @@ vi.mock('fs/promises', () => ({ }, })); -// Mock the sessions from verify-purchase -const mockSessions = new Map(); +// Mock the session store +const mockSessions = new Map(); // Mock the downloadTokens from generate-download-token const mockDownloadTokens = new Map(); -vi.mock('./verify-purchase', () => ({ - sessions: mockSessions, +vi.mock('src/lib/session-store', () => ({ + getSession: (sessionId: string) => mockSessions.get(sessionId), + invalidateSession: (sessionId: string) => mockSessions.delete(sessionId), })); vi.mock('./generate-download-token', () => ({ diff --git a/astro/src/test/api/generate-download-token.test.ts b/astro/src/test/api/generate-download-token.test.ts index 4b936dd4..1856b2d4 100644 --- a/astro/src/test/api/generate-download-token.test.ts +++ b/astro/src/test/api/generate-download-token.test.ts @@ -5,11 +5,13 @@ vi.mock('uuid', () => ({ v4: vi.fn().mockReturnValue('test-token-id-1234'), })); -// Mock the sessions from verify-purchase -const mockSessions = new Map(); +// Mock the session store module +const mockSessions = new Map(); -vi.mock('./verify-purchase', () => ({ - sessions: mockSessions, +vi.mock('src/lib/session-store', () => ({ + getSession: (sessionId: string) => mockSessions.get(sessionId), + upsertSession: (session: any) => mockSessions.set(session.sessionId, session), + clearSessions: () => mockSessions.clear(), })); describe('generate-download-token API', () => { diff --git a/astro/src/test/api/verify-purchase.test.ts b/astro/src/test/api/verify-purchase.test.ts index e97f9ec7..466d1f3c 100644 --- a/astro/src/test/api/verify-purchase.test.ts +++ b/astro/src/test/api/verify-purchase.test.ts @@ -126,11 +126,11 @@ describe('verify-purchase API', () => { const { POST } = await import('../../pages/api/verify-purchase'); // Mock file read error (file not found) - (fs.readFile as any).mockRejectedValue(new Error('ENOENT: no such file or directory')); + (fs.readFile as any).mockRejectedValue(Object.assign(new Error('ENOENT: no such file or directory'), { code: 'ENOENT' })); const mockRequest = { json: () => Promise.resolve({ - purchaseCode: 'ESC-INVALID', + purchaseCode: 'ESC-87654321', email: 'test@example.com', }), headers: { diff --git a/astro/src/test/middleware/access-control.test.ts b/astro/src/test/middleware/access-control.test.ts new file mode 100644 index 00000000..b22e6c0a --- /dev/null +++ b/astro/src/test/middleware/access-control.test.ts @@ -0,0 +1,88 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { onRequest } from '../../middleware'; +import { upsertSession, clearSessions, type PurchaseSession } from '../../lib/session-store'; + +const TEST_HEADERS = { + 'user-agent': 'Mozilla/5.0 Test Browser', + 'accept-language': 'en-US,en;q=0.9', + 'accept-encoding': 'gzip, deflate, br', +}; + +function createRequest(path: string, options: { cookie?: string; headers?: Record } = {}) { + const headers = new Headers(); + const merged = { ...TEST_HEADERS, ...(options.headers ?? {}) }; + for (const [key, value] of Object.entries(merged)) { + headers.set(key, value); + } + if (options.cookie) { + headers.set('cookie', options.cookie); + } + return new Request(`http://localhost:4321${path}`, { headers }); +} + +describe('middleware access control', () => { + beforeEach(() => { + clearSessions(); + }); + + afterEach(() => { + clearSessions(); + vi.unstubAllGlobals(); + }); + + it('returns 404 for protected route without session', async () => { + const request = createRequest('/services/escape-room/content/corporate'); + const context: any = { + url: new URL(request.url), + request, + locals: {}, + }; + + const next = vi.fn().mockResolvedValue(new Response('next')); + + const response = await onRequest(context, next); + + expect(next).not.toHaveBeenCalled(); + expect(response).toBeInstanceOf(Response); + expect((response as Response).status).toBe(404); + }); + + it('allows protected route with valid session', async () => { + const session: PurchaseSession = { + sessionId: 'valid-session', + purchaseCode: 'ESC-12345678', + email: 'test@example.com', + theme: 'corporate', + kitType: 'build', + organization: 'Test Corp', + createdAt: Date.now(), + expiresAt: Date.now() + 1_800_000, + browserFingerprint: Buffer.from( + `${TEST_HEADERS['user-agent']}:${TEST_HEADERS['accept-language']}:${TEST_HEADERS['accept-encoding']}` + ).toString('base64'), + ipAddress: '127.0.0.1', + }; + + upsertSession(session); + + const request = createRequest('/services/escape-room/content/corporate', { + cookie: `session=${session.sessionId}`, + }); + + const context: any = { + url: new URL(request.url), + request, + locals: {}, + }; + + const next = vi.fn().mockResolvedValue(new Response('ok')); + + const response = await onRequest(context, next); + + expect(next).toHaveBeenCalledOnce(); + expect(response).toBeInstanceOf(Response); + expect((response as Response).status).toBe(200); + expect(await (response as Response).text()).toBe('ok'); + expect(context.locals.session?.sessionId).toBe(session.sessionId); + }); +}); From 883f5c3d522329a3788e8de630464d98cdafa081 Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Thu, 16 Oct 2025 13:11:06 -0400 Subject: [PATCH 12/27] Change session handling to use session-store module in download material and verify purchase APIs --- astro/src/pages/api/download-material.ts | 4 +- .../src/pages/api/generate-download-token.ts | 10 +- astro/src/pages/api/verify-purchase.ts | 296 +++++++++++------- 3 files changed, 195 insertions(+), 115 deletions(-) diff --git a/astro/src/pages/api/download-material.ts b/astro/src/pages/api/download-material.ts index 7df506f8..3cf7a1ea 100644 --- a/astro/src/pages/api/download-material.ts +++ b/astro/src/pages/api/download-material.ts @@ -1,7 +1,7 @@ import type { APIRoute } from 'astro'; import fs from 'fs/promises'; import path from 'path'; -import { sessions } from './verify-purchase'; +import { getSession } from 'src/lib/session-store'; import { downloadTokens } from './generate-download-token'; export const GET: APIRoute = async ({ request, url, clientAddress }) => { @@ -27,7 +27,7 @@ export const GET: APIRoute = async ({ request, url, clientAddress }) => { tokenData.used = true; // Verify the associated session still exists - const sessionData = sessions.get(tokenData.sessionId); + const sessionData = getSession(tokenData.sessionId); if (!sessionData || Date.now() > sessionData.expiresAt) { downloadTokens.delete(downloadToken); diff --git a/astro/src/pages/api/generate-download-token.ts b/astro/src/pages/api/generate-download-token.ts index 840bf972..05eb32a3 100644 --- a/astro/src/pages/api/generate-download-token.ts +++ b/astro/src/pages/api/generate-download-token.ts @@ -1,5 +1,5 @@ import type { APIRoute } from 'astro'; -import { sessions } from './verify-purchase'; +import { getSession } from 'src/lib/session-store'; import { v4 as uuidv4 } from 'uuid'; // Store for one-time download tokens @@ -31,7 +31,7 @@ export const POST: APIRoute = async ({ request, clientAddress }) => { } // Verify session exists and is valid - const sessionData = sessions.get(sessionId); + const sessionData = getSession(sessionId); if (!sessionData || Date.now() > sessionData.expiresAt) { return new Response('Invalid session', { status: 401 }); @@ -49,16 +49,18 @@ export const POST: APIRoute = async ({ request, clientAddress }) => { // Create one-time download token (expires in 5 minutes) const downloadToken = uuidv4(); + const expiresAt = Date.now() + (5 * 60 * 1000); + downloadTokens.set(downloadToken, { sessionId, materialType, createdAt: Date.now(), - expiresAt: Date.now() + (5 * 60 * 1000), // 5 minutes + expiresAt, used: false }); return new Response( - JSON.stringify({ downloadToken }), + JSON.stringify({ success: true, downloadToken, expiresAt }), { status: 200, headers: { 'Content-Type': 'application/json' } diff --git a/astro/src/pages/api/verify-purchase.ts b/astro/src/pages/api/verify-purchase.ts index c68f07f7..31f05d3a 100644 --- a/astro/src/pages/api/verify-purchase.ts +++ b/astro/src/pages/api/verify-purchase.ts @@ -1,138 +1,216 @@ import type { APIRoute } from 'astro'; import fs from 'fs/promises'; import path from 'path'; -import { v4 as uuidv4 } from 'uuid'; +import { createSession, getCookieHeader, getAllSessions } from 'src/lib/session-store'; +import type { PurchaseSession } from 'src/lib/session-store'; -// In-memory session store (in production, use Redis or database) -const sessions = new Map(); - -// Clean up expired sessions every hour -setInterval(() => { - const now = Date.now(); - for (const [sessionId, session] of sessions.entries()) { - if (now > session.expiresAt) { - sessions.delete(sessionId); + theme?: string; + kitType?: string; + organization?: string; + [key: string]: unknown; +} + +const GENERIC_ERROR_MESSAGE = 'Invalid purchase code or email address'; + +function jsonResponse(data: unknown, status = 200, headers?: HeadersInit) { + return new Response(JSON.stringify(data), { + status, + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', + ...(headers ?? {}) + } + }); +} + + +function normalisePurchaseCode(input: string) { + return input.trim().toUpperCase(); +} + +function normaliseEmail(input: string) { + return input.trim().toLowerCase(); +} + +async function readBody(request: Request): Promise<{ purchaseCode?: string; email?: string }> { + const contentType = request.headers.get('content-type') ?? ''; + + if (!contentType || contentType.includes('application/json')) { + try { + const json = await request.json(); + if (json && typeof json === 'object') { + return json as any; + } + } catch (error) { + console.error('[verify-purchase] Failed to parse JSON body', error); + throw new Error('BODY_PARSE_ERROR'); } } -}, 60 * 60 * 1000); -export const POST: APIRoute = async ({ request, clientAddress }) => { - try { - const body = await request.json(); - const { purchaseCode, email } = body; - - // Add debugging - console.log('verify-purchase request:', { purchaseCode, email }); - - if (!purchaseCode || !email) { - console.log('Missing required fields'); - return new Response( - JSON.stringify({ error: 'Invalid purchase code or email address' }), - { status: 400, headers: { 'Content-Type': 'application/json' } } - ); + if (contentType.includes('application/x-www-form-urlencoded') && typeof request.formData === 'function') { + try { + const form = await request.formData(); + return { + purchaseCode: form.get('purchaseCode')?.toString(), + email: form.get('email')?.toString() + }; + } catch (error) { + console.error('[verify-purchase] Failed to parse form body', error); + throw new Error('BODY_PARSE_ERROR'); } + } - // Look up purchase data - const purchaseData = await getPurchaseData(purchaseCode); + return {}; +} - if (!purchaseData) { - console.log('Purchase data not found for code:', purchaseCode); - return new Response( - JSON.stringify({ error: 'Invalid purchase code or email address' }), - { status: 404, headers: { 'Content-Type': 'application/json' } } - ); +function getCandidatePurchaseDirs(): string[] { + const env = import.meta.env as Record; + const candidates = [ + env.PURCHASES_DATA_DIR, + process.env.PURCHASES_DATA_DIR, + path.resolve(process.cwd(), '..', 'local-dev', 'purchases'), + path.resolve(process.cwd(), 'local-dev', 'purchases') + ]; + return [...new Set(candidates.filter(Boolean) as string[])]; +} + +async function loadPurchase(purchaseCode: string): Promise { + const candidates = getCandidatePurchaseDirs(); + const filename = `${purchaseCode}.json`; + + for (const dir of candidates) { + const filePath = path.join(dir, filename); + try { + const fileContents = await fs.readFile(filePath, 'utf-8'); + const parsed = JSON.parse(fileContents) as PurchaseRecord; + return parsed; + } catch (error: any) { + if (error?.code === 'ENOENT') { + continue; + } + + console.error('[verify-purchase] Failed to read purchase file', { filePath, error }); + throw error; } + } + + return null; +} + +async function verifyAndCreateSession(options: { + purchaseCode?: string | null; + email?: string | null; + request: Request; + clientAddress?: string; + method: string; +}) { + const { purchaseCode, email, request, clientAddress, method } = options; + + if (!purchaseCode || !email) { + return jsonResponse({ error: GENERIC_ERROR_MESSAGE }, 400); + } - // Verify email matches - if (purchaseData.email.toLowerCase() !== email.toLowerCase()) { - return new Response( - JSON.stringify({ error: 'Invalid purchase code or email address' }), - { status: 403, headers: { 'Content-Type': 'application/json' } } - ); + const normalisedCode = normalisePurchaseCode(purchaseCode); + const normalisedEmail = normaliseEmail(email); + + if (!/^ESC-[A-Z0-9]{8}$/.test(normalisedCode) || !/.+@.+\..+/.test(normalisedEmail)) { + return jsonResponse({ error: GENERIC_ERROR_MESSAGE }, 400); + } + + try { + const purchase = await loadPurchase(normalisedCode); + + if (!purchase) { + return jsonResponse({ error: GENERIC_ERROR_MESSAGE }, 404); } - // Create browser fingerprint from headers - const userAgent = request.headers.get('user-agent') || ''; - const acceptLanguage = request.headers.get('accept-language') || ''; - const acceptEncoding = request.headers.get('accept-encoding') || ''; - const browserFingerprint = Buffer.from(`${userAgent}:${acceptLanguage}:${acceptEncoding}`).toString('base64'); + if (!purchase.email || normaliseEmail(purchase.email) !== normalisedEmail) { + return jsonResponse({ error: GENERIC_ERROR_MESSAGE }, 403); + } - // Create secure session - const sessionId = uuidv4(); - const sessionData = { - purchaseCode, - email: purchaseData.email, - theme: purchaseData.theme, - kitType: purchaseData.kitType, - organization: purchaseData.organization, - createdAt: Date.now(), - expiresAt: Date.now() + (30 * 60 * 1000), // Reduced to 30 minutes - browserFingerprint, - ipAddress: clientAddress || 'unknown' - }; - - sessions.set(sessionId, sessionData); - - // Return session ID and purchase details with security token - const response = new Response( - JSON.stringify({ + const session: PurchaseSession = createSession({ + purchaseCode: purchase.purchaseCode ?? normalisedCode, + email: purchase.email, + theme: purchase.theme, + kitType: purchase.kitType, + organization: purchase.organization, + headers: request.headers, + ipAddress: clientAddress, + }); + + const response = jsonResponse( + { valid: true, - sessionId, - kitType: purchaseData.kitType, - theme: purchaseData.theme, - organization: purchaseData.organization, - purchaseDate: purchaseData.createdAt, - }), - { - status: 200, - headers: { - 'Content-Type': 'application/json', - // Set HttpOnly cookie for additional security - 'Set-Cookie': `session=${sessionId}; HttpOnly; Secure; SameSite=Strict; Max-Age=1800; Path=/api/` - } - } + sessionId: session.sessionId, + expiresAt: session.expiresAt, + purchaseCode: session.purchaseCode, + email: purchase.email, + kitType: purchase.kitType, + theme: purchase.theme, + organization: purchase.organization + }, + 200, + { 'Set-Cookie': getCookieHeader(session) } ); - return response; + console.log(`[verify-purchase] ${method} success`, { + purchaseCode: session.purchaseCode, + sessionId: session.sessionId, + expiresAt: session.expiresAt, + // note: sessions will persist in-memory until replaced w/ Azure/Netlify store + }); + return response; } catch (error) { - console.error('Error verifying purchase:', error); - return new Response( - JSON.stringify({ error: 'Invalid purchase code or email address' }), - { status: 500, headers: { 'Content-Type': 'application/json' } } - ); + console.error('[verify-purchase] Verification error', error); + return jsonResponse({ error: GENERIC_ERROR_MESSAGE }, 500); + } +} + +export const POST: APIRoute = async ({ request, clientAddress }) => { + try { + const body = await readBody(request); + return await verifyAndCreateSession({ + purchaseCode: body.purchaseCode, + email: body.email, + request, + clientAddress, + method: 'POST' + }); + } catch (error: any) { + if (error?.message === 'BODY_PARSE_ERROR') { + return jsonResponse({ error: GENERIC_ERROR_MESSAGE }, 500); + } + + console.error('[verify-purchase] Unexpected POST error', error); + return jsonResponse({ error: GENERIC_ERROR_MESSAGE }, 500); } }; -async function getPurchaseData(purchaseCode: string) { +export const GET: APIRoute = async ({ request, url, clientAddress }) => { try { - // Go up one level from astro/ to repo root, then into local-dev/purchases - const storageDir = path.join(process.cwd(), '..', 'local-dev', 'purchases'); - const filename = `${purchaseCode}.json`; - const filepath = path.join(storageDir, filename); - - // Add debugging - console.log('Looking for purchase file:', filepath); - - const data = await fs.readFile(filepath, 'utf8'); - console.log('Found purchase data for:', purchaseCode); - return JSON.parse(data); + const purchaseCode = url.searchParams.get('purchaseCode'); + const email = url.searchParams.get('email'); + + if (!purchaseCode && !email) { + return jsonResponse({ status: 'ok', route: '/api/verify-purchase', ts: Date.now() }); + } + + return await verifyAndCreateSession({ + purchaseCode, + email, + request, + clientAddress, + method: 'GET' + }); } catch (error) { - // File doesn't exist or other error - console.log('Error reading purchase file:', error.message); - return null; + console.error('[verify-purchase] Unexpected GET error', error); + return jsonResponse({ error: GENERIC_ERROR_MESSAGE }, 500); } -} +}; + +export { getAllSessions as sessions }; // TODO: Remove once downstream modules migrate to session-store directly. -// Export sessions for use in other APIs -export { sessions }; From 22c03cf99867baa277563d9794a7d567c288925c Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Thu, 16 Oct 2025 13:20:48 -0400 Subject: [PATCH 13/27] Add session handling to middleware and update type definitions --- astro/src/env.d.ts | 8 ++++++++ astro/src/middleware.ts | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/astro/src/env.d.ts b/astro/src/env.d.ts index acef35f1..749cf61e 100644 --- a/astro/src/env.d.ts +++ b/astro/src/env.d.ts @@ -1,2 +1,10 @@ /// /// + +import type { PurchaseSession } from './lib/session-store'; + +declare namespace App { + interface Locals { + session?: PurchaseSession; + } +} diff --git a/astro/src/middleware.ts b/astro/src/middleware.ts index af8ddfc6..cf66c5b9 100644 --- a/astro/src/middleware.ts +++ b/astro/src/middleware.ts @@ -1,6 +1,9 @@ import type { MiddlewareHandler } from 'astro'; +import { getSessionFromRequest } from './lib/session-store'; -export const onRequest: MiddlewareHandler = ({ url }, next) => { +const PROTECTED_ESCAPE_ROOM_PREFIX = '/services/escape-room/content/'; + +export const onRequest: MiddlewareHandler = async ({ url, request, locals }, next) => { // Block direct access to protected materials if (url.pathname.startsWith('/materials/premium/') || url.pathname.startsWith('/protected-materials/')) { @@ -13,6 +16,16 @@ export const onRequest: MiddlewareHandler = ({ url }, next) => { }); } + if (url.pathname.startsWith(PROTECTED_ESCAPE_ROOM_PREFIX)) { + const session = await getSessionFromRequest(request); + + if (!session) { + return new Response(null, { status: 404 }); + } + + locals.session = session; + } + // Continue to the next middleware or route return next(); }; From cc3bfae5530245afe9e5a3b4509a89fc02549c04 Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Thu, 16 Oct 2025 13:21:09 -0400 Subject: [PATCH 14/27] Change data extraction in escape room theme page and enable prerendering --- astro/src/pages/services/escape-room/themes/[id].astro | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/astro/src/pages/services/escape-room/themes/[id].astro b/astro/src/pages/services/escape-room/themes/[id].astro index c300c74a..849296e5 100644 --- a/astro/src/pages/services/escape-room/themes/[id].astro +++ b/astro/src/pages/services/escape-room/themes/[id].astro @@ -4,6 +4,8 @@ import { getOpenGraphImageData } from "src/lib/og-image"; import type { Breadcrumbs, PageMetadata } from "src/lib/types"; import components from "src/lib/mdx"; +export const prerender = true; + export async function getStaticPaths() { const crumbs: Breadcrumbs = [ { @@ -40,8 +42,11 @@ interface Props { } const { room, crumbs, metadata } = Astro.props; -const { about, title, page } = room.data; -const { theme, image } = page; +const title = room.data.title; +const about = room.data.about ?? {}; +const page = room.data.page as Record | undefined; +const theme = (page?.theme ?? "brand") as string; +const image = page?.image; const { Content } = await render(room); const themeVar = `var(--bs-${theme})`; const textVar = `var(--bs-${theme}-text-emphasis)`; From 0a16d16832f5cbbbd3f28644930a69a91356f2c5 Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Thu, 16 Oct 2025 13:35:37 -0400 Subject: [PATCH 15/27] Rework escape room access and content handling - Removed the digital content API endpoint and its associated logic. - Simplified the escape room access page, enhancing the form for purchase code and email verification. - Added error handling for missing content in the new corporate escape room content page. - Introduced a new layout for displaying escape room content based on user sessions. --- astro/package-lock.json | 2012 ++++++++++++++++- astro/package.json | 7 +- astro/src/components/Section.astro | 12 + .../content/escape-room-kits/corporate.mdx | 218 ++ .../src/layouts/EscapeRoomContentLayout.astro | 271 +++ astro/src/lib/session-store.ts | 140 ++ astro/src/pages/api/digital-content.ts | 482 ---- .../pages/services/escape-room/access.astro | 761 ++----- .../escape-room/content/corporate.astro | 27 + 9 files changed, 2878 insertions(+), 1052 deletions(-) create mode 100644 astro/src/components/Section.astro create mode 100644 astro/src/content/escape-room-kits/corporate.mdx create mode 100644 astro/src/layouts/EscapeRoomContentLayout.astro create mode 100644 astro/src/lib/session-store.ts delete mode 100644 astro/src/pages/api/digital-content.ts create mode 100644 astro/src/pages/services/escape-room/content/corporate.astro diff --git a/astro/package-lock.json b/astro/package-lock.json index 843d6030..0517e417 100644 --- a/astro/package-lock.json +++ b/astro/package-lock.json @@ -27,8 +27,10 @@ "sass": "^1.71.1" }, "devDependencies": { + "jsdom": "^27.0.0", "prettier": "^3.2.5", - "prettier-plugin-astro": "^0.14.0" + "prettier-plugin-astro": "^0.14.0", + "vitest": "^2.1.4" } }, "node_modules/@antfu/install-pkg": { @@ -51,6 +53,82 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz", + "integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.1" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.6.2.tgz", + "integrity": "sha512-+AG0jN9HTwfDLBhjhX1FKi6zlIAc/YGgEHlN/OMaHD1pOPFsC5CpYQpLkPX0aFjyaVmoq9330cQDCU4qnSL1qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.2" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@astrojs/compiler": { "version": "2.12.2", "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.12.2.tgz", @@ -275,6 +353,144 @@ "node": ">=0.1.90" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.14.tgz", + "integrity": "sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@dabh/diagnostics": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", @@ -3154,6 +3370,92 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@vue/compiler-core": { "version": "3.5.22", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.22.tgz", @@ -3560,6 +3862,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-module-types": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ast-module-types/-/ast-module-types-6.0.1.tgz", @@ -3871,6 +4183,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -4027,6 +4349,16 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -4076,6 +4408,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", @@ -4124,6 +4473,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/cheerio": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", @@ -4698,6 +5057,42 @@ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==" }, + "node_modules/cssstyle": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.1.tgz", + "integrity": "sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.0.3", + "@csstools/css-syntax-patches-for-csstree": "^1.0.14", + "css-tree": "^3.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssstyle/node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -4707,32 +5102,90 @@ "node": ">= 12" } }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=20" } }, - "node_modules/decache": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/decache/-/decache-4.6.2.tgz", - "integrity": "sha512-2LPqkLeu8XWHU8qNCS3kcF6sCcb5zIzvWaAHYSvPfwhdd7mHuah29NssMzrTYyHN4F5oFy2ko9OBYxegtU0FEw==", + "node_modules/data-urls/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decache": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/decache/-/decache-4.6.2.tgz", + "integrity": "sha512-2LPqkLeu8XWHU8qNCS3kcF6sCcb5zIzvWaAHYSvPfwhdd7mHuah29NssMzrTYyHN4F5oFy2ko9OBYxegtU0FEw==", "license": "MIT", "dependencies": { "callsite": "^1.0.0" } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/decode-named-character-reference": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", @@ -4759,6 +5212,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -5566,6 +6029,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -6452,6 +6925,19 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-escaper": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", @@ -6490,6 +6976,20 @@ "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "license": "BSD-2-Clause" }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/http-shutdown": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/http-shutdown/-/http-shutdown-1.2.2.tgz", @@ -6913,6 +7413,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-stream": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", @@ -7054,6 +7561,83 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.0.tgz", + "integrity": "sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/dom-selector": "^6.5.4", + "cssstyle": "^5.3.0", + "data-urls": "^6.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^7.3.0", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0", + "ws": "^8.18.2", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -7368,6 +7952,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -9140,11 +9731,12 @@ "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" }, "node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", "dependencies": { - "entities": "^4.4.0" + "entities": "^6.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -9173,6 +9765,18 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-exists": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", @@ -9242,6 +9846,16 @@ "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==" }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -9469,6 +10083,16 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -10016,6 +10640,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -10133,6 +10764,19 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -10224,6 +10868,13 @@ "@types/hast": "^3.0.4" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -10377,6 +11028,13 @@ "node": "*" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/std-env": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", @@ -10614,6 +11272,13 @@ "url": "https://opencollective.com/svgo" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/system-architecture": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz", @@ -10686,6 +11351,13 @@ "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", @@ -10736,6 +11408,56 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.17.tgz", + "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.17" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz", + "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", + "dev": true, + "license": "MIT" + }, "node_modules/tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", @@ -10777,6 +11499,19 @@ "integrity": "sha512-2Ulkc8T7mXJ2l0W476YC/A209PR38Nw8PuaCNtk9uI3t1zzFdGQeWYGQvmj2PZkVvRC/Yoi4xQKMRnWc/N29tQ==", "license": "MIT" }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -11457,52 +12192,1161 @@ } } }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=12.0.0" + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "bin": { + "vite-node": "vite-node.mjs" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "license": "MIT", "engines": { - "node": ">=12" + "node": "^18.0.0 || >=20.0.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://opencollective.com/vitest" } }, - "node_modules/vitefu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", - "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", - "workspaces": [ - "tests/deps/*", - "tests/projects/*", - "tests/projects/workspace/packages/*" + "optional": true, + "os": [ + "aix" ], - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "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" + } + }, + "node_modules/vite-node/node_modules/vite": { + "version": "5.4.20", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", + "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitefu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "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" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "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" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "5.4.20", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", + "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" } }, "node_modules/web-namespaces": { @@ -11580,6 +13424,23 @@ "node": ">=4" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/widest-line": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", @@ -11778,6 +13639,45 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/xss": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", diff --git a/astro/package.json b/astro/package.json index a3f27201..6caee0de 100644 --- a/astro/package.json +++ b/astro/package.json @@ -7,7 +7,8 @@ "start": "astro dev", "build": "astro build", "preview": "astro preview", - "astro": "astro" + "astro": "astro", + "test": "vitest run" }, "dependencies": { "@astrojs/mdx": "^4.3.6", @@ -29,7 +30,9 @@ "sass": "^1.71.1" }, "devDependencies": { + "jsdom": "^27.0.0", "prettier": "^3.2.5", - "prettier-plugin-astro": "^0.14.0" + "prettier-plugin-astro": "^0.14.0", + "vitest": "^2.1.4" } } diff --git a/astro/src/components/Section.astro b/astro/src/components/Section.astro new file mode 100644 index 00000000..320a4e56 --- /dev/null +++ b/astro/src/components/Section.astro @@ -0,0 +1,12 @@ +--- +// Wrapper that forwards attributes and ensures the layout sees `target-slot` +// Usage in MD/MDX: +//
...
+const props = Astro.props as Record; +const targetSlot = props['target-slot']; +const { slot: _ignoredSlot, ...rest } = props; +const dataAttrs = targetSlot ? { 'data-slot-target': String(targetSlot) } : {}; +--- +
+ +
diff --git a/astro/src/content/escape-room-kits/corporate.mdx b/astro/src/content/escape-room-kits/corporate.mdx new file mode 100644 index 00000000..bd045378 --- /dev/null +++ b/astro/src/content/escape-room-kits/corporate.mdx @@ -0,0 +1,218 @@ +--- +title: A Corporate Conundrum +image: ../../images/escape-room/aleksandar-andreev-k2gRTJM9BPw-unsplash.jpg +--- + +
+## Overview + +### Scenario + +You are our best corporate espionage team. Our newest prototype went missing. We've located the person who took it but we need you to enter a room and find the technology. + +You have 30 minutes to get the tech and get out before you risk detection. Leave the room clean and as close to what it looks like when you enter as possible so the thief doesn't immediately know someone has been there. + +Good luck. + +### Puzzles + +![The alternative text](src/images/escape-room/braille-blocks.png) + +- **Computer**: A computer presents a screen with 4 passcodes. Each passcode can be figured out using clues given by other puzzles in the room. (Jester, Baltimore, Spot, Violet) +- **Codename**: Photos and Seeing AI. Seeing AI will read the name only on the correct photo. Letter is signed by J narrows the guesses down if they do not figure this out. +- **City**: clue provided by Braille card in book + - Finding the Braille decipher card is required (if no one reads braille) + - Braille is redundant on the other puzzles but the ability to read braille is a “superpower” on this puzzle. +- **Pet Name:** The riddle on the video presents definitions of Spot. The first letter of each phrase (capitalized in captions) also spells out Spot. +- **Color**: + - Directional Lock: The letter provides the directional combination within the text. + - Box with Braille letters: The letters when organized provides the color +- **Computer**: Email – final clue for final puzzle, images have numbers + - Answer is 35678 +- **Numeric lock finds the prototype:** EZ Button + +
+
+ +## Materials + +To build the escape room, you'll need to acquire the following items. They do not need to be from Amazon, but we've linked to the items on Amazon to make it easier to find. + +### Required items + +- Wooden alphabet tiles with letters and braille. We have previously used the [PlanToys alphabet tiles](https://www.amazon.com/dp/B00I3VWSIA?ref_=ppx_hzsearch_conn_dt_b_fed_asin_title_2) but they are unavailable on Amazon. [This alternative from Feifeiya](https://www.amazon.com/Feifeiya-Alphabet-Punctuation-Educational-Visually/dp/B0DSFKLRF5/ref=sr_1_16?dib=eyJ2IjoiMSJ9.zfNJP4gK5mZvMbykUiIxamwTOdWUmJIwhs2wmsPjGwF8cAQN3ua1LRsdjLczpzzHbmU5fOLM1W10eB1HyITrf6ZNJpcAHzV5u7j4t7PNXYbb8-FB7oXZPpO6neJ1qQioKWBWCLn377r4I-mUNXwrt-s5o0sWa1aLicc6ip1cYYNrz6-mlHnPif_MK1kMT36hzLVreN1_hQqEJ0udsGsNfnDQrQgRDP2IOuFRU0z68tdbhC1gZeO9YXWHc4l1Lj32fPFVnfMeHi5apIAJkkjjRKUI2SCzYpmA9-iED-7aCOM.IQ-9gWRxjvpTmFGt-7pOq_AJ9eXja16wEI1sc1qC6ZM&dib_tag=se&qid=1744921793&sr=8-16&srs=121042749011) should also work, but if you are able to find the alphabet tiles, those are ideal. The key is that the letter needs to be large and high contrast and there should be some tactile indicator to clearly define the direction the braille should be read. + ![The alternative text](src/images/escape-room/braille-blocks.png) +- A [directional lock](https://www.amazon.com/Master-Lock-Locker-Directional-Combination/dp/B0845Q2SBV/ref=sr_1_2?crid=GCG9HDU0DSZ2&dib=eyJ2IjoiMSJ9.Sy7kTuIJ0JDdCvVN77SfssTlnc93OEyjEYFcqfugEX85Ld4FRqnk8Bv_8Nge_JDUSFD_183M52H9OrmcDw1xxwi5aSpZeIGBSGpVMelvDLxz4utifSjR31mVsPtIcTwkJ4s3cM1VTsQ2715X6DR6HyTwqrIQfWlksZviDhr_3h7IFW7quq4Iw1g-QskpKuJUumW9qkDrCBsIsdB42lL7g1rGOrwe3CnBoYpyRhRSDACOqdHz6K9Rvpyff7gy0UXq37eKCkYpOD4U6ncJS00pTrNFpAxUj-2eOX0lIL_xbSw.IOfw2L77PCqlEIW6ujOzk3N4ZoJACIt6jfE2FBEmIv8&dib_tag=se&keywords=directional+lock&qid=1744924500&sprefix=directional+lock%2Caps%2C234&sr=8-2) - These are adjustable and can be set to work with the room. + - [MasterLock 1500ID Directional Lock](https://www.masterlock.com/products/product/1500iD) +- A [push number lock](https://www.amazon.com/Combination-Push-Button-Anti-Rust-Weatherproof-Padlock/dp/B07F9M2CX1/ref=sr_1_58?crid=1QASMHP23HEFU&dib=eyJ2IjoiMSJ9.qSIegJfIjHpLklNFlRncKSbbxSQibsjZTvbKYhfEzEgOhP8s2iqGnNsdfM0lgkxOorrSjp0Zzjp_lT8M4KHB2rPyCYIMT8l-gC3FpPWpzXrq91ufng6BurRLKj5MfwrTUTRIYVEe-oGlIWa4SRmuxW5CdeY-LXrs2n4_zSbZEV3nGMj0zOMAZz8WXU_-P41BdVr3g7mswcPGs4bxTN5s8oKXCV8X5TU25yQsaQHfWH-3JzpoH3yzmZBKC_ezsDOPy1L1GhB_Dz7PW0PBjDWuOoDEuRqsPWHHeBEeNRj__YQ.PGBl2rmh0x_MBUVhtp50LTPgZFJ0j07_yMjfIN-Fzx4&dib_tag=se&keywords=number+lock&qid=1744924532&sprefix=number+lock%2Caps%2C200&sr=8-58) - These are not adjustable. + - Please send us the number codes when you get the locks. +- Braille alphabet card - You can buy a [sturdy alphabet card](https://www.amazon.com/EliteSign-Braille-Alphabet-Learning-Beginners/dp/B0DHGF1N7Z/ref=sr_1_12_sspa?crid=36XN52UK1UKAX&dib=eyJ2IjoiMSJ9.oAbas7bOgncFAsYHpF892mTQ0LiR6ivhQt6C89iwx71U8TVsWctwRHCSLooMEmzOEKY1gP16TLCbG5yONQNHpnj_tWX6RpJ9bFHN6tmxH5KK7oU-ggLI0GUEo0HOzeYeSeiCCBDWhqxfgsVzHihzBgqS0trS0_lZnr3lVaztHQi1wf8dzqDTX2EhQwik81QQzH9epJqoBRYF-a7LUh5vLywCLP7rORFyCyq07w0S1SV8rNSwrxK3dwWPtg7SgASkMZ8AzeaMw2kGen0DAQqGPHKTVWWqcCHWFOIjoizbSgY.NpSwZhwNxnvxdXM2NlgqIcyt1VhzHzu4z-1DKSMffFM&dib_tag=se&keywords=braille%2Balphabet%2Bcard&qid=1744922571&sprefix=braille%2Balphabet%2Bcard%2Caps%2C198&sr=8-12-spons&sp_csd=d2lkZ2V0TmFtZT1zcF9tdGY&th=1) or if you have a cardboard one already, you can use that. +- [Recordable button](https://www.amazon.com/dp/B08HRQN931/ref=sspa_dk_detail_2?pd_rd_i=B07CXG4ZFF&pd_rd_w=CQsli&content-id=amzn1.sym.f2f1cf8f-cab4-44dc-82ba-0ca811fb90cc&pf_rd_p=f2f1cf8f-cab4-44dc-82ba-0ca811fb90cc&pf_rd_r=33CBX1K76YMPBGT6SY5E&pd_rd_wg=RSN2w&pd_rd_r=39addf21-c0c2-43ab-b22f-dcaecae35c7e&s=office-products&sp_csd=d2lkZ2V0TmFtZT1zcF9kZXRhaWxfdGhlbWF0aWM&th=1) - You will need to add the recording. +- Two boxes of some type that will support a normal-sized padlock. We often build our own as it can be difficult to find ones that fit the locks. Below are options: + - [Stainless steel Storage box](https://www.amazon.com/ToolUSA-Stainless-Steel-Security-Box/dp/B0017QFP7Y/ref=sr_1_4?crid=X1O62B7HLJ2C&dib=eyJ2IjoiMSJ9.PfDfS5NfvduWrccFNoIS0LRhKBgVGeXjp2DT0EuL9ulqobcsmYN7F7ZAw3c8k8CnBlbbiJC7VenAoXRaoqXLfDW5_88aX_s-u0sUUi8uWao1-A40IGR9THrWSG3W1ka9pz7LLhLGVRi2Im-qIl6I24c-bKbrP61sxT1IOxLb1VQdv9UUYe6AZ_oIZ1UC0EMueeZdLBcyqWbEsv6fZFCU2Zu74vfAMFgsKhZA_Ymuv_OyuEcCVGTLRSQCe5MnL5hOZwq5quBwQPtqPIZSzONaiuBY7h4SC8jK0lVsOp3xBfs.WDBWWctHD2uPCeqnSDkrY3CJBZZBCVlFXtAp0YlcF1E&dib_tag=se&keywords=boxes%2Bthat%2Bwill%2Btake%2Ba%2Bpadlock&qid=1744926521&sprefix=boxes%2Bthat%2Bwill%2Btake%2Ba%2Bpadlock%2Caps%2C167&sr=8-4&th=1) - this box will support the push number lock but not the directional lock. + - [Charity collection box](https://www.amazon.com/MCB-Donation-Collection-Mounting-Included/dp/B00VGR295U/ref=sr_1_16?crid=X1O62B7HLJ2C&dib=eyJ2IjoiMSJ9.PfDfS5NfvduWrccFNoIS0LRhKBgVGeXjp2DT0EuL9ulqobcsmYN7F7ZAw3c8k8CnBlbbiJC7VenAoXRaoqXLfDW5_88aX_s-u0sUUi8uWao1-A40IGR9THrWSG3W1ka9pz7LLhLGVRi2Im-qIl6I24c-bKbrP61sxT1IOxLb1VQdv9UUYe6AZ_oIZ1UC0EMueeZdLBcyqWbEsv6fZFCU2Zu74vfAMFgsKhZA_Ymuv_OyuEcCVGTLRSQCe5MnL5hOZwq5quBwQPtqPIZSzONaiuBY7h4SC8jK0lVsOp3xBfs.WDBWWctHD2uPCeqnSDkrY3CJBZZBCVlFXtAp0YlcF1E&dib_tag=se&keywords=boxes%2Bthat%2Bwill%2Btake%2Ba%2Bpadlock&qid=1744926521&sprefix=boxes%2Bthat%2Bwill%2Btake%2Ba%2Bpadlock%2Caps%2C167&sr=8-16&th=1) - I haven't tried this but it looks like it will work. + - Find a decorative briefcase or suitcase that can fit a padlock. + - Build your own - Both [Michaels](https://www.michaels.com/) or [Hobby Lobby](https://www.hobbylobby.com/) have decorative boxes like this [treasure box](https://www.amazon.com/dp/B092PZJ4NF/ref=sbl_dpx_decor-boxes_B084MMTZZF_00?keywords=decorative%2Bboxes%2Bthat%2Bwill%2Btake%2Ba%2Bpadlock&th=1) with fairly thick sides. You can purchase [hasps](https://www.amazon.com/stores/page/D05F7A9B-3981-4B5D-8818-ED8EBDFFD834/search?lp_asin=B000BP7M96&ref_=ast_bln&store_ref=bl_ast_dp_brandLogo_sto&terms=hasp) that will blend with the box's look and size. Find the shortest screws that will work with the hasp... we suggest 1/4 inch wood screws with a flat head, as these typically do not require drilling. + +#### Packing Instructions + +- Place the easy button and written version into the =box. Add packing material to keep items from moving around and lock it with the number lock. +- Place puzzle box, tactile letters, pictures, alphabet card, and directional lock into the other box. + +- A computer (desktop, laptop or tablet with a keyboard) that can display the provided web page. +- Tablet or phone that can display a video from the internet in a loop. + +### Recommended items + +- A leaderboard to track the best results. +- A printout of orientation or electronic device to read it from. +- An extra or two of each type of lock, so they can be demonstrated and played with. + - It is often helpful to have a second type of each lock to allow people to try them out before they start the escape room. +- Extra braille cards and extra batteries/cords. + - In their excitement, people tend to be rough with puzzle room equipment, so having backups becomes important. +- Thematic items to hide some of the clues in. + - [Book Box](https://www.amazon.com/Vintiquewise-QI003691-B-Decorative-Vintage-Box-Blue/dp/B084MMTZZF/ref=sr_1_17?crid=13YC6EJ3DIPZ9&dib=eyJ2IjoiMSJ9.UX6YWw73lmENX_azzhPy0Pg1qLoJGBKNY4_vG6Qr6HCFQ-jIIcMPOY08gFfn6Qhe2gZFzfSwzz1EtpZ9jXcPoHwFIXTTA-V1AsU_BImZSr8lrXV7nBvOl6_rwUoIEHgMk9b0J3uBc-454Vaot4Q0uaWSDDVhDjiVGxsfaEzW_Otc4mLTKG9ryDaXp2nzLbTqVw6oleIgO9nejzFsMi6qmXYJVt108LGPa3oaeyHWP4nfrC1sbg5nm21p2Lmc0eNVZl5lcnCzqmDsFOvFNF-cG5VBkEXdwwWA1MMUhuYnuuE.N7V31DbUWo8waHQ5bsKRWST4elMAS-rBVA0A88KcC4c&dib_tag=se&keywords=decorative%2Bboxes%2Bthat%2Bwill%2Btake%2Ba%2Bpadlock&qid=1744926814&sprefix=decorative%2Bboxes%2Bthat%2Bwill%2Btake%2Ba%2Bpadlock%2Caps%2C172&sr=8-17&th=1) - A simple item that is fun for players to find things in. We typically add it to a stack of real books. + - A trash can, preferably short and definitely clean. + +Other decorations to help establish the theme and get your participants in the mood. This includes a briefcase, pads of paper or notebooks, books (to use with the book box), pens/pencils, a fake plant or two, etc. If hosting in an office or conference room, use random office supplies or decorations from the surrounding area. + +
+
+ +## Assembly & Setup + +Remember to charge all equipment the night before. The computer should be plugged in during the escape room. Other items should be plugged in during or between sessions. + +### Computer + +1. Plug in the computer. Turn it on and log in, if necessary. +2. Install NVDA or JAWS. +3. Load the given web page to get to the Login Screen with name and password fields. +4. If the computer runs Windows, press F11 to hide the browser. + - If you have a way to put the computer on kiosk mode, this will help you run the room. + - If you have an apple product, use guided access (see instructions under Video) and voiceover + +### Prototype EZ Button and Numeric Lock + +1. Ensure the batteries are fresh. +2. Record the following: + > Congratulations! You have found the Accessibility Easy Button. With just one click everything within five feet will be made accessible. Available on April 1st. +3. Place the EZ button in the box and lock it with the numeric lock. + +### Photographs + +1. Spread the photos around in an area or hide the photos in a file cabinet. + +### iPhone + +1. Install the SeeingAI application onto the phone. +2. Configure SeeingAI to recognize the correct photograph + 1. Open SeeingAI + 2. Turn it onto Person mode. + 3. Click the person list button on the left of the camera button. + 4. Add person. + 5. Take 3 photos of the Jester photo + 6. Name the person Jester. + 7. Double check that when seeing AI is on person mode it now says “Jester” +3. During the orientation, demonstrate Seeing AI on another participant and some text. +4. Hand it to the group or leave it with the photos. + +### Braille Cityname Card + +1. Place the braille card into a book box, drawer or other slightly hidden place in the room. + +### Letter Scramble and Directional Lock + +1. [Configure the directional lock](https://www.masterlock.com/support/instructions/Speed_Dial_1500iD_Instructions) to have the following combination: + `Up, Left, Left, Up.` +2. Mixup the puzzle pieces randomly and place them in one of the lockable boxes. +3. Place the directional lock on the lockbox and lock it. + +### Letter + +1. Place the hint letter in an envelope and place it on a desk or table (to speed things up) or hidden in the room (to slow things down). + +### Braille Translation Card + +1. If you have an additional hidden place, put the braille alphabet guide in there (trash can, desk drawer or other place). If not, display it on a desk or table, or consider having on the floor in a corner that's relatively easy to get to. + +### Video (TV, iPad or Tablet) + +1. The link for the video is [https://youtu.be/a7nH3ZC7Vho](https://youtu.be/a7nH3ZC7Vho). +2. If you are playing the video on the iPad, put it into “Guided Access” mode to restrict it to the YouTube app (or Safari, if you're accessing YouTube from a browser). + 1. Enable Guided Access under Settings > Accessibility > Guided Access + 2. Open Passcode Settings and set a passcode. If you are locking multiple devices, we recommend using the same passcode for each and write it down on these instructions. + 3. Open the application (YouTube or Safari) and load the video link above. + 4. Triple click the home/side button on the iphone. It will prompt you about Guided Access. Click Start. + 5. To end, triple click the home/side button again and then enter the passcode. + +
+
+ +## Preparation + +### Room layout suggestions + +As you are laying out the escape room, consider the following: + +- Ensure that there is plenty of room and open space. Remember that multiple people will need to gather around each item (or set of items). + - Conference or folding tables work well, and items can be placed at each end of the table(s). + - For items needing power (laptop, video/tv), do your best to ensure that the power cords will not be a tripping hazard for your audience. +- How you lay out the room affects how long it takes people to complete it. + - If you need to extend the experience, separate related clues in the room. The amount of clues you hide (in a trash can, desk drawer, file cabinet, etc) will also lengthen the time it typically takes to solve. + - Place related clues closer together if you need to reduce the amount of time the puzzle takes. +- Make the room feel like it's a work space (or hotel room) to lean into the theme. + - Consider leaving some items on a work surface, such as the hat & bow tie and pictures. + - If there is a file cabinet or a desk or shelf with drawers, consider using that to hide some of the clues. +- If you can, have some seats available. Not only will this help cement your theme of an office (or hotel room), but it will provide those who need it a chance to rest. + +### Scheduling the day + +- Hold a trial run or two will help you gauge how long to schedule each session. + - Some groups will go really fast, and some will get stuck and go slow. The groups that go fast will provide you with a break in-between the groups. + - This escape room is designed to run between 20 and 30 minutes. You can adjust the time by separating parts of a puzzle. For example, if the letter with directions is next to the directional lock, teams will figure it out much faster than if they are in separate parts of the room. +- Remember to include time for the orientation and wrap up with each group. + - If you have a separate space (and help) where these discussions can be held, this will allow you to handle more groups. + - If you do not have a separate space, add at least 10 minutes between groups. + +### Giving an orientation for participants + +The goals of the orientation are the following: + +- Provide the overview (slides are provided) +- If the participants do not know each other, provide a time for introductions. +- Orient all participants to any bathroom and any other accommodations you have available. + +As far as the escape room, going over some details will help things go more smoothly. + +- Explain how the two locks work: + - To reset the directional lock, strongly press the lock into the silver arch twice, then start the directions. + - To reset the number lock, push all the numbers up, strongly press the lock into the silver arch, then press the desired numbers down and hit the switch on the bottom. +- Introduce them to Seeing AI. +- State that each clue is only used on time. + +### Providing hints for participants + +To help move things along, sometimes your participants will need a hint. Without getting too specific, here are a couple of good general ones to use: + +- Remember that each item is only used once – What haven't you used yet? +- Read the letter out loud again. +- Why don't you explore [location that they missed] or go back to [item that they missed]? + +### Providing a wrap up (What your participants learned) + +1. Overall + - With thought and planning, activities can be made accessible. + - Alternatives such as braille, captions, audio descriptions, sound, and lights can add to the experience and allow more people to participate + - Technological assistance such as SeeingAI can provide accommodations and allow more participants to play. Sometimes it can even be part of a puzzle. + - Escape room themes don't need to cause anxiety. They can be fun and still challenging enough for adults. +2. Photographs + - Color isn't required to identify people. When designing, remember to use shapes and other distinguishing features to convey information. +3. Word Scramble + - Contrast and tactile alternatives are an important design consideration for individuals with low vision +4. Video + - Captions and audio descriptions add value to multimedia content +5. Email + - Alternative text supports individuals who are blind and can add value for others was well + +
diff --git a/astro/src/layouts/EscapeRoomContentLayout.astro b/astro/src/layouts/EscapeRoomContentLayout.astro new file mode 100644 index 00000000..9e42b930 --- /dev/null +++ b/astro/src/layouts/EscapeRoomContentLayout.astro @@ -0,0 +1,271 @@ +--- +import "../styles/bootstrap.scss"; +import Metadata from "../components/Metadata.astro"; +import HeaderER from "../components/HeaderER.astro"; +import { type PageMetadata } from "../lib/types"; + +interface Props { + title: string; + metadata?: PageMetadata; + theme?: string; + organization?: string; + kitType?: "build" | "ready" | string; +} + +const { metadata, title, theme = "", organization = "", kitType = "" } = Astro.props as Props; +--- + + + + + + + + + + {metadata && ( + + + + )} + + + + + + + + +
+
+

Accessible Escape Room Digital Content

+
+ {title} +
+
+ + +
+ + +
+
+

Instructions

+ +
+ +
+

Elements

+ +
+ +
+

Room Layout

+ +
+ +
+

Leaderboard & Results

+
+ + + + + + + + + + + + + +
RankTeam NameCompletion Time
Leaderboard will be populated programmatically.
+
+
+ + +
+
+ + + + diff --git a/astro/src/lib/session-store.ts b/astro/src/lib/session-store.ts new file mode 100644 index 00000000..cd0fb884 --- /dev/null +++ b/astro/src/lib/session-store.ts @@ -0,0 +1,140 @@ +import { v4 as uuidv4 } from 'uuid'; + +export interface PurchaseSession { + sessionId: string; + purchaseCode: string; + email: string; + theme?: string; + kitType?: string; + organization?: string; + createdAt?: string; + createdAt: number; + expiresAt: number; + browserFingerprint: string; + ipAddress?: string; +} + +const SESSION_DURATION_MS = 30 * 60 * 1000; + +const sessions = new Map(); + +function cleanupExpiredSessions() { + const now = Date.now(); + for (const [sessionId, session] of sessions.entries()) { + if (session.expiresAt <= now) { + sessions.delete(sessionId); + } + } +} + +function createFingerprint(headers: Headers) { + const userAgent = headers.get('user-agent') ?? ''; + const acceptLanguage = headers.get('accept-language') ?? ''; + const acceptEncoding = headers.get('accept-encoding') ?? ''; + return Buffer.from(`${userAgent}:${acceptLanguage}:${acceptEncoding}`).toString('base64'); +} + +export function createSession(data: { + purchaseCode: string; + email: string; + theme?: string; + kitType?: string; + organization?: string; + createdAt?: string; + headers: Headers; + ipAddress?: string; +}): PurchaseSession { + cleanupExpiredSessions(); + + const sessionId = uuidv4(); + const createdAt = Date.now(); + const expiresAt = createdAt + SESSION_DURATION_MS; + const browserFingerprint = createFingerprint(data.headers); + + const session: PurchaseSession = { + sessionId, + purchaseCode: data.purchaseCode, + email: data.email, + theme: data.theme, + kitType: data.kitType, + organization: data.organization, + createdAt: data.createdAt, + createdAt, + expiresAt, + browserFingerprint, + ipAddress: data.ipAddress, + }; + + sessions.set(sessionId, session); + return session; +} + +export async function getSessionFromRequest(request: Request) { + cleanupExpiredSessions(); + + const cookieHeader = request.headers.get('cookie') ?? ''; + const cookies = Object.fromEntries( + cookieHeader + .split(';') + .map((part) => part.trim()) + .filter(Boolean) + .map((part) => { + const [name, ...rest] = part.split('='); + return [name, rest.join('=')]; + }), + ); + + const sessionId = cookies.session; + if (!sessionId) return null; + + const session = sessions.get(sessionId); + if (!session) return null; + + if (session.expiresAt <= Date.now()) { + sessions.delete(sessionId); + return null; + } + + const fingerprint = createFingerprint(request.headers); + if (fingerprint !== session.browserFingerprint) { + return null; + } + + return session; +} + +export function getSession(sessionId: string) { + cleanupExpiredSessions(); + return sessions.get(sessionId) ?? null; +} + +export function invalidateSession(sessionId: string) { + sessions.delete(sessionId); +} + +export function getCookieHeader(session: PurchaseSession) { + const maxAge = Math.floor((session.expiresAt - Date.now()) / 1000); + return [ + `session=${session.sessionId}`, + 'Path=/', + `Max-Age=${Math.max(maxAge, 0)}`, + 'HttpOnly', + 'Secure', + 'SameSite=Strict', + ].join('; '); +} + +export function getAllSessions() { + cleanupExpiredSessions(); + return sessions; +} + +export function upsertSession(session: PurchaseSession) { + sessions.set(session.sessionId, session); +} + +export function clearSessions() { + sessions.clear(); +} + +// TODO: Persist sessions via Azure Table Storage or Netlify Blobs for multi-instance resilience. diff --git a/astro/src/pages/api/digital-content.ts b/astro/src/pages/api/digital-content.ts deleted file mode 100644 index 2808583f..00000000 --- a/astro/src/pages/api/digital-content.ts +++ /dev/null @@ -1,482 +0,0 @@ -import type { APIRoute } from 'astro'; -import { sessions } from './verify-purchase'; - -export const GET: APIRoute = async ({ request, url, clientAddress }) => { - try { - const searchParams = url.searchParams; - const sessionId = searchParams.get('session'); - - if (!sessionId) { - return new Response('Unauthorized', { status: 401 }); - } - - // Also check for session cookie - const cookieHeader = request.headers.get('cookie'); - const sessionFromCookie = cookieHeader?.split(';') - .find(c => c.trim().startsWith('session=')) - ?.split('=')[1]; - - // Session must match both URL parameter and cookie - if (sessionFromCookie !== sessionId) { - return new Response('Unauthorized - Session mismatch', { status: 401 }); - } - - // Verify session - const sessionData = sessions.get(sessionId); - - if (!sessionData || Date.now() > sessionData.expiresAt) { - if (sessionData) { - sessions.delete(sessionId); // Clean up expired session - } - return new Response('Session expired', { status: 401 }); - } - - // Verify browser fingerprint - const userAgent = request.headers.get('user-agent') || ''; - const acceptLanguage = request.headers.get('accept-language') || ''; - const acceptEncoding = request.headers.get('accept-encoding') || ''; - const currentFingerprint = Buffer.from(`${userAgent}:${acceptLanguage}:${acceptEncoding}`).toString('base64'); - - if (currentFingerprint !== sessionData.browserFingerprint) { - sessions.delete(sessionId); // Delete session on fingerprint mismatch - return new Response('Unauthorized - Browser mismatch', { status: 401 }); - } - - // Generate secure access page for digital content - const digitalContentHtml = generateDigitalContentPage(sessionData, sessionId); - - return new Response(digitalContentHtml, { - status: 200, - headers: { - 'Content-Type': 'text/html', - 'Cache-Control': 'private, no-cache, no-store, must-revalidate', - 'Pragma': 'no-cache', - 'Expires': '0', - 'X-Robots-Tag': 'noindex, nofollow' - } - }); - - } catch (error) { - console.error('Error serving digital content:', error); - return new Response('Internal Server Error', { status: 500 }); - } -}; - -function generateDigitalContentPage(sessionData: any, sessionId: string): string { - const { theme, organization, kitType } = sessionData; - - return ` - - - - - Digital Content - ${theme} - - - - - - - - -
-
-
-
-

Digital Content

-

Kit: ${theme}

-

Purchased by: ${organization}

-

Kit Type: ${kitType === 'build' ? 'Build-your-own Kit' : 'Ready-made Kit'}

-
-
-
-
- - - - -
-
- -
-
-
- -
-
-
-

Setup Instructions

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.

- -

Video Guide

-
-

[Video Player Placeholder]

- Content Type: MP4 Video with Captions -
- -

Step-by-Step Guide

-
    -
  1. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
  2. -
  3. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
  4. -
  5. Ut enim ad minim veniam, quis nostrud exercitation ullamco.
  6. -
  7. Duis aute irure dolor in reprehenderit in voluptate velit esse.
  8. -
-
-
-

Audio Descriptions

-
-

[Audio Player Placeholder]

- Content Type: MP3 Audio -
-
-
-
- - -
-

Interactive Elements

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut enim ad minim veniam, quis nostrud exercitation.

- -
-
-
-
-
Puzzle Element 1
-

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

- -
-
-
-
-
-
-
Puzzle Element 2
-

Sed do eiusmod tempor incididunt ut labore et dolore.

- -
-
-
-
-
-
-
Final Challenge
-

Ut enim ad minim veniam, quis nostrud exercitation.

- -
-
-
-
-
- - -
-

Room Layout & Setup

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

- -
-
-
-

[Room Layout Diagram Placeholder]

- Interactive room layout would appear here -
- -

Setup Requirements

-
    -
  • Lorem ipsum dolor sit amet - minimum 10x10 feet
  • -
  • Consectetur adipiscing elit - adequate lighting
  • -
  • Sed do eiusmod tempor - 4-6 participants recommended
  • -
  • Incididunt ut labore - tables and chairs for setup
  • -
-
- -
-
- - -
-

Leaderboard & Results

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Track your team's progress and compare with others.

- -
-
-

Current Rankings

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
RankTeam NameCompletion Time
1Lorem Ipsum Team24:35
2Consectetur Elit27:12
3Sed Do Eiusmod31:48
4Tempor Incididunt35:22
-
-
-
-

Your Stats

-
-
-
Team Progress
-

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

-
    -
  • Best Time: Not yet completed
  • -
  • Attempts: 0
  • -
  • Current Rank: Unranked
  • -
-
-
-
-
-
-
-
- - -
- - - - -`; -} diff --git a/astro/src/pages/services/escape-room/access.astro b/astro/src/pages/services/escape-room/access.astro index 9eb3c4fe..9f774739 100644 --- a/astro/src/pages/services/escape-room/access.astro +++ b/astro/src/pages/services/escape-room/access.astro @@ -1,539 +1,276 @@ --- -import { getOpenGraphImageData } from "@lib/og-image"; -import type { Breadcrumbs, PageMetadata } from "@lib/types"; - -const title = "Access Your Kit - The Accessible Escape Room"; -const metadata: PageMetadata = { - title, - description: "Access your purchased escape room kit materials", - image: getOpenGraphImageData(Astro.site, "pages", "kit-access"), -}; -const crumbs: Breadcrumbs = [ - { name: "Home", href: "/" }, - { name: "Services", href: "/services" }, - { name: "Escape Room", href: "/services/escape-room" }, -]; - -import Branding from "@components/Branding.astro"; import Layout from "src/layouts/Layout.astro"; -import ThemedSection from "@components/ThemedSection.astro"; -import { Icon } from "astro-icon/components"; +const title = 'Kit Access'; --- - -
-
-
-
-

- Access Your Escape Room Kit -

-

- Enter your purchase code and email to access your kit materials -

-
-
-
-
- - - -
-
-
-
-

- - Verify Your Purchase -

-
-
-
- - -
- - -
- Enter the purchase code from your confirmation email -
- -
- -
- - - -
- -
- -
- - -
- - -
-
-
-
-
- - - - - - -
-

Need Help?

-
-
-
-
- -
Lost Your Code?
-

Check your email confirmation or contact support

- - Email Support - -
-
-
-
-
-
- -
Setup Help
-

Get assistance setting up your escape room

- - Get Setup Help - -
-
-
-
-
-
- -
Purchase More
-

Want another theme or kit type?

- - View Kits - -
-
-
-
-
-
- - + +
+
+ + + +
+
+ + + +
+ +
+

+ + - - + }); + +
diff --git a/astro/src/pages/services/escape-room/content/corporate.astro b/astro/src/pages/services/escape-room/content/corporate.astro new file mode 100644 index 00000000..dce94b44 --- /dev/null +++ b/astro/src/pages/services/escape-room/content/corporate.astro @@ -0,0 +1,27 @@ +--- +import { getEntry, render } from "astro:content"; +import EscapeRoomContentLayout from "src/layouts/EscapeRoomContentLayout.astro"; +import Section from "@components/Section.astro"; +import type { PurchaseSession } from "src/lib/session-store"; + +// Ensure that 'session' exists on Astro.locals, or use the correct property name if different +const session = (Astro.locals as { session?: PurchaseSession }).session; + +if (!session) { + throw new Response(null, { status: 404 }); +} + +const themeSlug = (session.theme ?? "corporate").toLowerCase(); +const entry = await getEntry("escapeRoomKits", themeSlug); + +if (!entry) { + return Astro.redirect('/services/escape-room/access?error=content-missing'); +} + +const { Content } = await render(entry); +const title = entry.data.title ?? "A Corporate Conundrum"; +--- + + + + From 298c779ecc360123cb27212bda7a4d24750b5e22 Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Thu, 30 Oct 2025 14:51:56 -0400 Subject: [PATCH 16/27] Remove unnecessary createdAt field from PurchaseSession interface and createSession function --- astro/src/lib/session-store.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/astro/src/lib/session-store.ts b/astro/src/lib/session-store.ts index cd0fb884..8849bbdc 100644 --- a/astro/src/lib/session-store.ts +++ b/astro/src/lib/session-store.ts @@ -7,7 +7,6 @@ export interface PurchaseSession { theme?: string; kitType?: string; organization?: string; - createdAt?: string; createdAt: number; expiresAt: number; browserFingerprint: string; @@ -40,7 +39,6 @@ export function createSession(data: { theme?: string; kitType?: string; organization?: string; - createdAt?: string; headers: Headers; ipAddress?: string; }): PurchaseSession { @@ -58,7 +56,6 @@ export function createSession(data: { theme: data.theme, kitType: data.kitType, organization: data.organization, - createdAt: data.createdAt, createdAt, expiresAt, browserFingerprint, From 963db3ac0f8c9fbfbc8ba5899417bb9012f979a8 Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Fri, 14 Nov 2025 13:17:46 -0500 Subject: [PATCH 17/27] Refactor robots.txt rules into astro config --- astro/astro.config.mjs | 16 +++++++++++----- astro/public/robots.txt | 14 -------------- 2 files changed, 11 insertions(+), 19 deletions(-) delete mode 100644 astro/public/robots.txt diff --git a/astro/astro.config.mjs b/astro/astro.config.mjs index 283a7f18..7fd1cdee 100644 --- a/astro/astro.config.mjs +++ b/astro/astro.config.mjs @@ -95,17 +95,23 @@ export default defineConfig({ robotsTxt({ sitemap: true, policy: [ + // Block specified bots entirely ...botsToDisallow.map((userAgent) => ({ userAgent, disallow: ['/'], })), + // General user-agent rules { userAgent: '*', - disallow: ['/fixable/'], - }, - { - userAgent: '*', - allow: ['/'], + allow: ['/', '/materials/basic/'], + disallow: [ + '/fixable/', + '/materials/premium/', + '/protected-materials/', + '/api/download-material', + '/api/digital-content', + '/api/verify-purchase', + ], }, ] }) diff --git a/astro/public/robots.txt b/astro/public/robots.txt deleted file mode 100644 index 9dc69c47..00000000 --- a/astro/public/robots.txt +++ /dev/null @@ -1,14 +0,0 @@ -User-agent: * -Allow: / - -# Protect premium materials from crawling -Disallow: /materials/premium/ -Disallow: /protected-materials/ -Disallow: /api/download-material -Disallow: /api/digital-content -Disallow: /api/verify-purchase - -# Allow basic materials -Allow: /materials/basic/ - -Sitemap: https://accessiblecommunity.github.io/sitemap.xml From e4c9bd3bcdf629079a54881594f0dc7942fa36ec Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Fri, 14 Nov 2025 13:21:42 -0500 Subject: [PATCH 18/27] Update sitemap filter to exclude newer escape room content path --- astro/astro.config.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/astro/astro.config.mjs b/astro/astro.config.mjs index 7fd1cdee..41b927cc 100644 --- a/astro/astro.config.mjs +++ b/astro/astro.config.mjs @@ -90,7 +90,10 @@ export default defineConfig({ } }), sitemap({ - filter: (page) => !page.endsWith('/commitment-form/') && !page.endsWith('fixable/'), + filter: (page) => + !page.endsWith('/commitment-form/') && + !page.includes('/fixable/') && + !page.includes('/services/escape-room/content/'), }), robotsTxt({ sitemap: true, @@ -111,6 +114,7 @@ export default defineConfig({ '/api/download-material', '/api/digital-content', '/api/verify-purchase', + '/services/escape-room/content/', ], }, ] From e7f1658cc57168d0cb9cbe135a21de3cbd6c65b1 Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Fri, 14 Nov 2025 13:35:44 -0500 Subject: [PATCH 19/27] Remove kitType from API and UI, update pricing logic, and adjust confirmation email content --- .../src/pages/api/create-checkout-session.ts | 12 ++-- .../src/pages/api/send-confirmation-email.ts | 6 +- .../escape-room/purchase-success.astro | 3 +- .../services/escape-room/purchase/index.astro | 62 +------------------ astro/src/test/api/digital-content.test.ts | 52 ---------------- 5 files changed, 10 insertions(+), 125 deletions(-) diff --git a/astro/src/pages/api/create-checkout-session.ts b/astro/src/pages/api/create-checkout-session.ts index 626a81b4..4a82a0e9 100644 --- a/astro/src/pages/api/create-checkout-session.ts +++ b/astro/src/pages/api/create-checkout-session.ts @@ -7,10 +7,10 @@ const stripe = new Stripe(import.meta.env.STRIPE_SECRET_KEY); export const POST: APIRoute = async ({ request }) => { try { const body = await request.json(); - const { kitType, theme, organization, contactName, email, specialRequirements } = body; + const { theme, organization, contactName, email, specialRequirements } = body; // Validate required fields - if (!kitType || !theme || !organization || !contactName || !email) { + if (!theme || !organization || !contactName || !email) { return new Response( JSON.stringify({ error: 'Missing required fields' }), { status: 400, headers: { 'Content-Type': 'application/json' } } @@ -20,9 +20,8 @@ export const POST: APIRoute = async ({ request }) => { // Generate unique purchase code const purchaseCode = `ESC-${uuidv4().split('-')[0].toUpperCase()}`; - // Determine price based on kit type - const price = kitType === 'build' ? 50000 : 350000; // Stripe uses cents - const kitTypeName = kitType === 'build' ? 'Build-your-own Kit' : 'Ready-made Kit'; + // Set price for build-your-own kit + const price = 50000; // $500 in cents for Stripe // Map theme values to display names const themeNames: Record = { @@ -42,7 +41,7 @@ export const POST: APIRoute = async ({ request }) => { price_data: { currency: 'usd', product_data: { - name: `${kitTypeName} - ${themeName}`, + name: `Build-your-own Escape Room Kit - ${themeName}`, description: `Accessible Escape Room Kit for ${organization}`, }, unit_amount: price, @@ -55,7 +54,6 @@ export const POST: APIRoute = async ({ request }) => { customer_email: email, metadata: { purchaseCode, - kitType, theme, organization, contactName, diff --git a/astro/src/pages/api/send-confirmation-email.ts b/astro/src/pages/api/send-confirmation-email.ts index e628ea65..784394de 100644 --- a/astro/src/pages/api/send-confirmation-email.ts +++ b/astro/src/pages/api/send-confirmation-email.ts @@ -3,7 +3,7 @@ import type { APIRoute } from 'astro'; export const POST: APIRoute = async ({ request }) => { try { const body = await request.json(); - const { email, purchaseCode, organization, kitType, theme } = body; + const { email, purchaseCode, organization, theme } = body; // In a production environment, you would use a service like: // - SendGrid @@ -21,7 +21,7 @@ Dear ${organization}, Thank you for purchasing an Accessible Escape Room Kit! Your Purchase Details: -- Kit Type: ${kitType === 'build' ? 'Build-your-own Kit' : 'Ready-made Kit'} +- Kit Type: Build-your-own Kit - Theme: ${theme} - Organization: ${organization} @@ -45,7 +45,7 @@ The Accessible Community Team // to: email, // subject: 'Your Accessible Escape Room Kit Purchase Confirmation', // text: emailContent, - // html: generateHtmlEmail(purchaseCode, organization, kitType, theme) + // html: generateHtmlEmail(purchaseCode, organization, theme) // }); return new Response( diff --git a/astro/src/pages/services/escape-room/purchase-success.astro b/astro/src/pages/services/escape-room/purchase-success.astro index 9a3f5ef4..48c82005 100644 --- a/astro/src/pages/services/escape-room/purchase-success.astro +++ b/astro/src/pages/services/escape-room/purchase-success.astro @@ -368,13 +368,12 @@ import ThemedSection from "@components/ThemedSection.astro"; // Kit Details const kitDetailsBody = document.getElementById('kit-details-body'); if (kitDetailsBody) { - const kitType = sessionData.metadata?.kitType === 'build' ? 'Build-your-own Kit' : 'Ready-made Kit'; const theme = sessionData.metadata?.theme || 'Not specified'; kitDetailsBody.innerHTML = ` - ${kitType}
+ Build-your-own Kit
Theme: ${theme} diff --git a/astro/src/pages/services/escape-room/purchase/index.astro b/astro/src/pages/services/escape-room/purchase/index.astro index 8fcca8a5..ea442a57 100644 --- a/astro/src/pages/services/escape-room/purchase/index.astro +++ b/astro/src/pages/services/escape-room/purchase/index.astro @@ -44,7 +44,7 @@ import { Icon } from "astro-icon/components";

Purchase Options

- Select your kit type and theme below + Select your kit theme below

@@ -55,25 +55,6 @@ import { Icon } from "astro-icon/components";
- - -
- Kit Type * -
- - -
-
- - -
- -
-
Choose Your Theme * @@ -172,40 +153,6 @@ import { Icon } from "astro-icon/components";
- - -
-

Ready-Made Kits

-

Our pre-assembled escape rooms will be available later this year

- -
-
-
-
-

Coming Soon

-

- Pre-assembled escape rooms with higher technology and automation. - Perfect for organizations wanting a ready-to-go solution. -

-
$3,500
-

Available later this year

- - Get Notified - -
- Email us to be notified when ready-made kits become available -
-
-
-
-
-
-
- @@ -320,13 +267,6 @@ import { Icon } from "astro-icon/components"; el.classList.remove('is-invalid'); }); - // Kit type validation - const kitType = document.querySelector('input[name="kitType"]:checked'); - if (!kitType) { - showFieldError('kitType-error', 'Please select a kit type'); - isValid = false; - } - // Theme validation const theme = document.querySelector('input[name="theme"]:checked'); if (!theme) { diff --git a/astro/src/test/api/digital-content.test.ts b/astro/src/test/api/digital-content.test.ts index 4144a739..025aefae 100644 --- a/astro/src/test/api/digital-content.test.ts +++ b/astro/src/test/api/digital-content.test.ts @@ -19,7 +19,6 @@ describe('digital-content API', () => { purchaseCode: 'ESC-12345678', email: 'test@example.com', theme: 'corporate', - kitType: 'build', organization: 'Test Corp', createdAt: Date.now(), expiresAt: Date.now() + (30 * 60 * 1000), @@ -142,7 +141,6 @@ describe('digital-content API', () => { purchaseCode: 'ESC-12345678', email: 'test@example.com', theme: 'corporate', - kitType: 'build', organization: 'Test Corp', createdAt: Date.now() - (60 * 60 * 1000), // 1 hour ago expiresAt: Date.now() - (30 * 60 * 1000), // Expired 30 minutes ago @@ -233,7 +231,6 @@ describe('digital-content API', () => { purchaseCode: 'ESC-12345678', email: 'test@example.com', theme: theme, - kitType: 'build', organization: 'Test Corp', createdAt: Date.now(), expiresAt: Date.now() + (30 * 60 * 1000), @@ -265,53 +262,4 @@ describe('digital-content API', () => { } }); - it('should generate different content for different kit types', async () => { - const { GET } = await import('../../pages/api/digital-content'); - - const kitTypes = ['build', 'ready']; - - for (const kitType of kitTypes) { - const sessionId = `session-${kitType}`; - const mockFingerprint = Buffer.from('Mozilla/5.0 Test Browser:en-US,en;q=0.9:gzip, deflate, br').toString('base64'); - - mockSessions.set(sessionId, { - purchaseCode: 'ESC-12345678', - email: 'test@example.com', - theme: 'corporate', - kitType: kitType, - organization: 'Test Corp', - createdAt: Date.now(), - expiresAt: Date.now() + (30 * 60 * 1000), - browserFingerprint: mockFingerprint, - ipAddress: '127.0.0.1', - }); - - const mockURL = new URL(`http://localhost:4321/api/digital-content?session=${sessionId}`); - - const mockRequest = { - headers: new Map([ - ['cookie', `session=${sessionId}`], - ['user-agent', 'Mozilla/5.0 Test Browser'], - ['accept-language', 'en-US,en;q=0.9'], - ['accept-encoding', 'gzip, deflate, br'], - ]), - }; - - const response = await GET({ - request: mockRequest, - url: mockURL, - clientAddress: '127.0.0.1' - } as any); - - expect(response.status).toBe(200); - - const html = await response.text(); - - if (kitType === 'build') { - expect(html).toContain('Build-your-own'); - } else { - expect(html).toContain('Ready-made'); - } - } - }); }); From b796c285c785789d2576b8c9d0921aec21584b8a Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Fri, 14 Nov 2025 14:04:56 -0500 Subject: [PATCH 20/27] Fix theme mapping for baking kit in checkout session --- astro/src/pages/api/create-checkout-session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astro/src/pages/api/create-checkout-session.ts b/astro/src/pages/api/create-checkout-session.ts index 4a82a0e9..56505f88 100644 --- a/astro/src/pages/api/create-checkout-session.ts +++ b/astro/src/pages/api/create-checkout-session.ts @@ -26,7 +26,7 @@ export const POST: APIRoute = async ({ request }) => { // Map theme values to display names const themeNames: Record = { 'corporate': 'Corporate Conundrum', - 'baking': 'Baking Bonanza', + 'kitchen': 'Baking Bonanza', 'picnic': 'Puzzling Picnic', 'casino': 'Cryptic Casino' }; From 2b83cf11caee9340428f3ad68e21f24b0cd81307 Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Fri, 14 Nov 2025 14:05:14 -0500 Subject: [PATCH 21/27] Update theme selection to dynamically render options from escape room themes collection --- .../services/escape-room/purchase/index.astro | 47 +++++-------------- 1 file changed, 12 insertions(+), 35 deletions(-) diff --git a/astro/src/pages/services/escape-room/purchase/index.astro b/astro/src/pages/services/escape-room/purchase/index.astro index ea442a57..53ecf385 100644 --- a/astro/src/pages/services/escape-room/purchase/index.astro +++ b/astro/src/pages/services/escape-room/purchase/index.astro @@ -1,5 +1,6 @@ --- import { getOpenGraphImageData } from "@lib/og-image"; +import { getCollection } from "astro:content"; import type { Breadcrumbs, PageMetadata } from "@lib/types"; const title = "Purchase - The Accessible Escape Room"; @@ -19,6 +20,7 @@ import Layout from "src/layouts/Layout.astro"; import ThemedBox from "@components/ThemedBox.astro"; import ThemedSection from "@components/ThemedSection.astro"; import { Icon } from "astro-icon/components"; +const themes = await getCollection('escapeRoomThemes'); --- @@ -59,42 +61,17 @@ import { Icon } from "astro-icon/components";
Choose Your Theme *
-
-
- - + {themes.map((entry) => ( +
+
+ + +
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
+ ))}
From 80480c2b2b1c2880f816d449ce283808f5c44cf1 Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Fri, 14 Nov 2025 14:06:05 -0500 Subject: [PATCH 22/27] Componentize FooterER to simplify EscapeRoomLayout --- astro/src/components/FooterER.astro | 93 +++++++++++++ astro/src/layouts/EscapeRoomLayout.astro | 163 ++--------------------- 2 files changed, 105 insertions(+), 151 deletions(-) create mode 100644 astro/src/components/FooterER.astro diff --git a/astro/src/components/FooterER.astro b/astro/src/components/FooterER.astro new file mode 100644 index 00000000..d9506cc6 --- /dev/null +++ b/astro/src/components/FooterER.astro @@ -0,0 +1,93 @@ +--- +import Branding from "../components/Branding.astro"; + +const navLinks = [ + { name: "Build-Your-Own Rooms", href: "/services/escape-room/themes" }, + { name: "How it Works", href: "/services/escape-room/how-it-works" }, + { name: "Our Story", href: "/services/escape-room/our-story" }, +]; +--- + + + + diff --git a/astro/src/layouts/EscapeRoomLayout.astro b/astro/src/layouts/EscapeRoomLayout.astro index 31c09ee3..803de7aa 100644 --- a/astro/src/layouts/EscapeRoomLayout.astro +++ b/astro/src/layouts/EscapeRoomLayout.astro @@ -1,162 +1,23 @@ --- -import "../styles/bootstrap.scss"; - -import Metadata from "../components/Metadata.astro"; -import Branding from "../components/Branding.astro"; +import Layout from "./Layout.astro"; import HeaderER from "../components/HeaderER.astro"; -import { type PageMetadata } from "../lib/types"; - -// Single these from the Header. -const navLinks = [{ - name: "Build-Your-Own Rooms", - href: "/services/escape-room/themes", -}, { - name: "How it Works", - href: "/services/escape-room/how-it-works", -}, { - name: "Our Story", - href: "/services/escape-room/our-story", -}]; +import FooterER from "../components/FooterER.astro"; +import type { Breadcrumbs, PageMetadata } from "../lib/types"; interface Props { title: string; metadata?: PageMetadata; + crumbs?: Breadcrumbs; } -const { metadata, title } = Astro.props; +const { title, metadata, crumbs } = Astro.props as Props; --- - - - - - - - - - { - metadata && ( - - - - ) - } - - - - - - - - - -
-
- - -
- - - - - - { - import.meta.env.PROD && ( - From 786458882cd14b27e91b0cab5cf8effb4f09dd04 Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Thu, 20 Nov 2025 14:33:46 -0500 Subject: [PATCH 24/27] Add escape room dynamic theme rendering --- .../escape-room/content/{corporate.astro => [theme].astro} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename astro/src/pages/services/escape-room/content/{corporate.astro => [theme].astro} (84%) diff --git a/astro/src/pages/services/escape-room/content/corporate.astro b/astro/src/pages/services/escape-room/content/[theme].astro similarity index 84% rename from astro/src/pages/services/escape-room/content/corporate.astro rename to astro/src/pages/services/escape-room/content/[theme].astro index dce94b44..ccf901e9 100644 --- a/astro/src/pages/services/escape-room/content/corporate.astro +++ b/astro/src/pages/services/escape-room/content/[theme].astro @@ -4,7 +4,7 @@ import EscapeRoomContentLayout from "src/layouts/EscapeRoomContentLayout.astro"; import Section from "@components/Section.astro"; import type { PurchaseSession } from "src/lib/session-store"; -// Ensure that 'session' exists on Astro.locals, or use the correct property name if different +// Ensure that 'session' exists on Astro.locals (protected by middleware) const session = (Astro.locals as { session?: PurchaseSession }).session; if (!session) { @@ -19,7 +19,7 @@ if (!entry) { } const { Content } = await render(entry); -const title = entry.data.title ?? "A Corporate Conundrum"; +const title = entry.data.title ?? "Escape Room Kit"; --- From 95fe042e88ebe928a171410003eb7ca6d64cf415 Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Thu, 20 Nov 2025 14:34:36 -0500 Subject: [PATCH 25/27] Make email confirmation functionality as function and store purchase data test rework --- astro/src/lib/email.ts | 56 ++++++ astro/src/lib/purchase-storage.ts | 77 ++++++++ .../src/pages/api/send-confirmation-email.ts | 72 ------- astro/src/test/lib/purchase-storage.test.ts | 187 ++++++++++++++++++ 4 files changed, 320 insertions(+), 72 deletions(-) create mode 100644 astro/src/lib/email.ts create mode 100644 astro/src/lib/purchase-storage.ts delete mode 100644 astro/src/pages/api/send-confirmation-email.ts create mode 100644 astro/src/test/lib/purchase-storage.test.ts diff --git a/astro/src/lib/email.ts b/astro/src/lib/email.ts new file mode 100644 index 00000000..76d88226 --- /dev/null +++ b/astro/src/lib/email.ts @@ -0,0 +1,56 @@ +export interface EmailOptions { + email: string; + purchaseCode: string; + organization: string; + theme: string; +} + +export async function sendConfirmationEmail(options: EmailOptions): Promise { + const { email, purchaseCode, organization, theme } = options; + + // In a production environment, you would use a service like: + // - SendGrid + // - Nodemailer with SMTP + // - AWS SES + // - Mailgun + // etc. + + // For now, we'll just log the email content + const emailContent = ` +Subject: Your Accessible Escape Room Kit Purchase Confirmation + +Dear ${organization}, + +Thank you for purchasing an Accessible Escape Room Kit! + +Your Purchase Details: +- Kit Type: Build-your-own Kit +- Theme: ${theme} +- Organization: ${organization} + +Your Secure Access Code: ${purchaseCode} + +To access your kit materials, visit: +https://accessiblecommunity.org/services/escape-room/access + +Enter your access code and the email address used for this purchase. + +Keep this code secure - it's your proof of purchase and gateway to your materials. + +If you need any assistance, please contact us at escaperoom@accessiblecommunity.org + +Best regards, +The Accessible Community Team + `; + + // In production, replace this with actual email sending + // await sendEmail({ + // to: email, + // subject: 'Your Accessible Escape Room Kit Purchase Confirmation', + // text: emailContent, + // html: generateHtmlEmail(purchaseCode, organization, theme) + // }); + + console.log('Confirmation email prepared for:', email); + console.log(emailContent); +} diff --git a/astro/src/lib/purchase-storage.ts b/astro/src/lib/purchase-storage.ts new file mode 100644 index 00000000..7bc375da --- /dev/null +++ b/astro/src/lib/purchase-storage.ts @@ -0,0 +1,77 @@ +import fs from 'fs/promises'; +import path from 'path'; + +export interface PurchaseData { + sessionId: string; + purchaseCode: string; + kitType?: string; + theme: string; + organization: string; + contactName: string; + email: string; + specialRequirements?: string; + amountPaid?: number | null; + currency?: string | null; + paymentStatus?: string | null; + createdAt: string; +} + +export async function storePurchaseData(purchaseData: PurchaseData): Promise { + try { + // TODO: For user accounts/login system, also store purchase by email: + // - Link purchase codes to customer email addresses + // - Store in database: { email, purchaseCode, productId, purchaseDate } + // - Enable "view all my purchases" functionality + + // TODO?: For Google OAuth integration: + // - Add optional account linking after purchase + // - Store OAuth user ID with purchase data + // - Allow login to see purchase history + + // Create local storage directory if it doesn't exist + const storageDir = path.join(process.cwd(), '..', 'local-dev', 'purchases'); + await fs.mkdir(storageDir, { recursive: true }); + + // Store purchase data as JSON file + const filename = `${purchaseData.purchaseCode}.json`; + const filepath = path.join(storageDir, filename); + + await fs.writeFile(filepath, JSON.stringify(purchaseData, null, 2)); + // Purchase data stored successfully + } catch (error) { + console.error('Error storing purchase data:', error); + throw error; + } +} + +function getCandidatePurchaseDirs(): string[] { + const env = import.meta.env as Record; + const candidates = [ + env.PURCHASES_DATA_DIR, + process.env.PURCHASES_DATA_DIR, + path.resolve(process.cwd(), '..', 'local-dev', 'purchases'), + path.resolve(process.cwd(), 'local-dev', 'purchases') + ]; + return [...new Set(candidates.filter(Boolean) as string[])]; +} + +export async function getPurchaseData(purchaseCode: string): Promise { + const candidates = getCandidatePurchaseDirs(); + const filename = `${purchaseCode}.json`; + + for (const dir of candidates) { + const filepath = path.join(dir, filename); + try { + const data = await fs.readFile(filepath, 'utf-8'); + return JSON.parse(data); + } catch (error: any) { + if (error?.code === 'ENOENT') { + continue; + } + console.error('Error reading purchase data:', { filepath, error }); + throw error; + } + } + + return null; +} diff --git a/astro/src/pages/api/send-confirmation-email.ts b/astro/src/pages/api/send-confirmation-email.ts deleted file mode 100644 index 784394de..00000000 --- a/astro/src/pages/api/send-confirmation-email.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { APIRoute } from 'astro'; - -export const POST: APIRoute = async ({ request }) => { - try { - const body = await request.json(); - const { email, purchaseCode, organization, theme } = body; - - // In a production environment, you would use a service like: - // - SendGrid - // - Nodemailer with SMTP - // - AWS SES - // - Mailgun - // etc. - - // For now, we'll just log the email content - const emailContent = ` -Subject: Your Accessible Escape Room Kit Purchase Confirmation - -Dear ${organization}, - -Thank you for purchasing an Accessible Escape Room Kit! - -Your Purchase Details: -- Kit Type: Build-your-own Kit -- Theme: ${theme} -- Organization: ${organization} - -Your Secure Access Code: ${purchaseCode} - -To access your kit materials, visit: -https://accessiblecommunity.org/services/escape-room/access - -Enter your access code and the email address used for this purchase. - -Keep this code secure - it's your proof of purchase and gateway to your materials. - -If you need any assistance, please contact us at escaperoom@accessiblecommunity.org - -Best regards, -The Accessible Community Team - `; - - // In production, replace this with actual email sending - // await sendEmail({ - // to: email, - // subject: 'Your Accessible Escape Room Kit Purchase Confirmation', - // text: emailContent, - // html: generateHtmlEmail(purchaseCode, organization, theme) - // }); - - return new Response( - JSON.stringify({ - success: true, - message: 'Confirmation email sent successfully' - }), - { - status: 200, - headers: { 'Content-Type': 'application/json' } - } - ); - - } catch (error) { - console.error('Error sending confirmation email:', error); - return new Response( - JSON.stringify({ - success: false, - error: 'Failed to send confirmation email' - }), - { status: 500, headers: { 'Content-Type': 'application/json' } } - ); - } -}; diff --git a/astro/src/test/lib/purchase-storage.test.ts b/astro/src/test/lib/purchase-storage.test.ts new file mode 100644 index 00000000..fc951fbf --- /dev/null +++ b/astro/src/test/lib/purchase-storage.test.ts @@ -0,0 +1,187 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import fs from 'fs/promises'; +import { storePurchaseData, getPurchaseData } from '@lib/purchase-storage'; +import type { PurchaseData } from '@lib/purchase-storage'; + +// Mock fs module +vi.mock('fs/promises', () => ({ + default: { + mkdir: vi.fn(), + writeFile: vi.fn(), + readFile: vi.fn(), + }, +})); + +describe('purchase-storage', () => { + beforeEach(() => { + vi.clearAllMocks(); + + // Mock successful file operations + (fs.mkdir as any).mockResolvedValue(undefined); + (fs.writeFile as any).mockResolvedValue(undefined); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('storePurchaseData', () => { + it('should store purchase data to JSON file', async () => { + const purchaseData: PurchaseData = { + sessionId: 'cs_test_session123', + purchaseCode: 'ESC-12345678', + theme: 'corporate', + organization: 'Test Corp', + contactName: 'John Doe', + email: 'test@example.com', + createdAt: new Date().toISOString(), + }; + + await storePurchaseData(purchaseData); + + expect(fs.mkdir).toHaveBeenCalledWith( + expect.stringMatching(/local-dev[\/\\]purchases$/), + { recursive: true } + ); + + expect(fs.writeFile).toHaveBeenCalledWith( + expect.stringMatching(/ESC-12345678\.json$/), + expect.stringContaining('"purchaseCode":"ESC-12345678"') + ); + }); + + it('should store complete purchase data with all fields', async () => { + const purchaseData: PurchaseData = { + sessionId: 'cs_test_session123', + purchaseCode: 'ESC-87654321', + kitType: 'build', + theme: 'kitchen', + organization: 'Bakery Inc', + contactName: 'Jane Baker', + email: 'jane@bakery.com', + specialRequirements: 'Gluten-free options', + amountPaid: 50000, + currency: 'usd', + paymentStatus: 'paid', + createdAt: new Date().toISOString(), + }; + + await storePurchaseData(purchaseData); + + const writeCall = (fs.writeFile as any).mock.calls[0]; + const savedData = JSON.parse(writeCall[1]); + + expect(savedData).toMatchObject({ + sessionId: 'cs_test_session123', + purchaseCode: 'ESC-87654321', + kitType: 'build', + theme: 'kitchen', + organization: 'Bakery Inc', + contactName: 'Jane Baker', + email: 'jane@bakery.com', + specialRequirements: 'Gluten-free options', + amountPaid: 50000, + currency: 'usd', + paymentStatus: 'paid', + }); + }); + + it('should handle file system errors', async () => { + (fs.mkdir as any).mockRejectedValue(new Error('Permission denied')); + + const purchaseData: PurchaseData = { + sessionId: 'cs_test_session123', + purchaseCode: 'ESC-12345678', + theme: 'corporate', + organization: 'Test Corp', + contactName: 'John Doe', + email: 'test@example.com', + createdAt: new Date().toISOString(), + }; + + await expect(storePurchaseData(purchaseData)).rejects.toThrow('Permission denied'); + }); + + it('should create directory if it does not exist', async () => { + const purchaseData: PurchaseData = { + sessionId: 'cs_test_session123', + purchaseCode: 'ESC-12345678', + theme: 'corporate', + organization: 'Test Corp', + contactName: 'John Doe', + email: 'test@example.com', + createdAt: new Date().toISOString(), + }; + + await storePurchaseData(purchaseData); + + expect(fs.mkdir).toHaveBeenCalledWith( + expect.any(String), + { recursive: true } + ); + }); + }); + + describe('getPurchaseData', () => { + it('should retrieve purchase data by code', async () => { + const mockData: PurchaseData = { + sessionId: 'cs_test_session123', + purchaseCode: 'ESC-12345678', + theme: 'corporate', + organization: 'Test Corp', + contactName: 'John Doe', + email: 'test@example.com', + createdAt: '2025-11-20T00:00:00.000Z', + }; + + (fs.readFile as any).mockResolvedValue(JSON.stringify(mockData)); + + const result = await getPurchaseData('ESC-12345678'); + + expect(result).toEqual(mockData); + expect(fs.readFile).toHaveBeenCalledWith( + expect.stringMatching(/ESC-12345678\.json$/), + 'utf-8' + ); + }); + + it('should return null for non-existent purchase code', async () => { + const error: NodeJS.ErrnoException = new Error('File not found'); + error.code = 'ENOENT'; + (fs.readFile as any).mockRejectedValue(error); + + const result = await getPurchaseData('ESC-NOTFOUND'); + + expect(result).toBeNull(); + }); + + it('should throw error for other file system errors', async () => { + (fs.readFile as any).mockRejectedValue(new Error('Permission denied')); + + await expect(getPurchaseData('ESC-12345678')).rejects.toThrow('Permission denied'); + }); + + it('should parse JSON data correctly', async () => { + const mockData: PurchaseData = { + sessionId: 'cs_test_session123', + purchaseCode: 'ESC-99999999', + kitType: 'build', + theme: 'kitchen', + organization: 'Test Org', + contactName: 'Test User', + email: 'test@test.com', + specialRequirements: 'Test requirements', + amountPaid: 50000, + currency: 'usd', + paymentStatus: 'paid', + createdAt: '2025-11-20T00:00:00.000Z', + }; + + (fs.readFile as any).mockResolvedValue(JSON.stringify(mockData, null, 2)); + + const result = await getPurchaseData('ESC-99999999'); + + expect(result).toEqual(mockData); + }); + }); +}); From 107cd919c7cf2ee7fca0fd11233ae46a33e913e2 Mon Sep 17 00:00:00 2001 From: Nathan Race Date: Thu, 20 Nov 2025 14:34:43 -0500 Subject: [PATCH 26/27] Refactor Stripe webhook to use dedicated email and purchase storage functions --- astro/src/pages/api/stripe-webhook.ts | 56 ++++------------------- astro/src/pages/api/verify-purchase.ts | 48 +------------------ astro/src/test/api/stripe-webhook.test.ts | 40 ++++++++-------- 3 files changed, 28 insertions(+), 116 deletions(-) diff --git a/astro/src/pages/api/stripe-webhook.ts b/astro/src/pages/api/stripe-webhook.ts index e84796c7..188963c1 100644 --- a/astro/src/pages/api/stripe-webhook.ts +++ b/astro/src/pages/api/stripe-webhook.ts @@ -1,7 +1,7 @@ import type { APIRoute } from 'astro'; import Stripe from 'stripe'; -import fs from 'fs/promises'; -import path from 'path'; +import { storePurchaseData } from '@lib/purchase-storage'; +import { sendConfirmationEmail } from '@lib/email'; const stripe = new Stripe(import.meta.env.STRIPE_SECRET_KEY); const endpointSecret = import.meta.env.STRIPE_WEBHOOK_SECRET; @@ -64,25 +64,13 @@ async function handleSuccessfulPayment(session: Stripe.Checkout.Session) { // Send confirmation email with secure purchase code try { - const emailResponse = await fetch('/api/send-confirmation-email', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - email: metadata.email, - purchaseCode: metadata.purchaseCode, - organization: metadata.organization, - kitType: metadata.kitType, - theme: metadata.theme, - }), + await sendConfirmationEmail({ + email: metadata.email, + purchaseCode: metadata.purchaseCode, + organization: metadata.organization, + theme: metadata.theme, }); - - if (!emailResponse.ok) { - console.error('Failed to send confirmation email'); - } else { - console.log('Confirmation email sent successfully'); - } + console.log('Confirmation email sent successfully'); } catch (error) { console.error('Error sending confirmation email:', error); } @@ -90,31 +78,3 @@ async function handleSuccessfulPayment(session: Stripe.Checkout.Session) { // Export for use in save-purchase-data export { handleSuccessfulPayment }; - -async function storePurchaseData(purchaseData: any) { - try { - // TODO: For user accounts/login system, also store purchase by email: - // - Link purchase codes to customer email addresses - // - Store in database: { email, purchaseCode, productId, purchaseDate } - // - Enable "view all my purchases" functionality - - // TODO?: For Google OAuth integration: - // - Add optional account linking after purchase - // - Store OAuth user ID with purchase data - // - Allow login to see purchase history - - // Create local storage directory if it doesn't exist - const storageDir = path.join(process.cwd(), '..', 'local-dev', 'purchases'); - await fs.mkdir(storageDir, { recursive: true }); - - // Store purchase data as JSON file - const filename = `${purchaseData.purchaseCode}.json`; - const filepath = path.join(storageDir, filename); - - await fs.writeFile(filepath, JSON.stringify(purchaseData, null, 2)); - // Purchase data stored successfully - } catch (error) { - console.error('Error storing purchase data:', error); - throw error; - } -} diff --git a/astro/src/pages/api/verify-purchase.ts b/astro/src/pages/api/verify-purchase.ts index 31f05d3a..f81b85e5 100644 --- a/astro/src/pages/api/verify-purchase.ts +++ b/astro/src/pages/api/verify-purchase.ts @@ -1,17 +1,7 @@ import type { APIRoute } from 'astro'; -import fs from 'fs/promises'; -import path from 'path'; import { createSession, getCookieHeader, getAllSessions } from 'src/lib/session-store'; import type { PurchaseSession } from 'src/lib/session-store'; - -interface PurchaseRecord { - purchaseCode: string; - email: string; - theme?: string; - kitType?: string; - organization?: string; - [key: string]: unknown; -} +import { getPurchaseData } from '@lib/purchase-storage'; const GENERIC_ERROR_MESSAGE = 'Invalid purchase code or email address'; @@ -66,40 +56,6 @@ async function readBody(request: Request): Promise<{ purchaseCode?: string; emai return {}; } -function getCandidatePurchaseDirs(): string[] { - const env = import.meta.env as Record; - const candidates = [ - env.PURCHASES_DATA_DIR, - process.env.PURCHASES_DATA_DIR, - path.resolve(process.cwd(), '..', 'local-dev', 'purchases'), - path.resolve(process.cwd(), 'local-dev', 'purchases') - ]; - return [...new Set(candidates.filter(Boolean) as string[])]; -} - -async function loadPurchase(purchaseCode: string): Promise { - const candidates = getCandidatePurchaseDirs(); - const filename = `${purchaseCode}.json`; - - for (const dir of candidates) { - const filePath = path.join(dir, filename); - try { - const fileContents = await fs.readFile(filePath, 'utf-8'); - const parsed = JSON.parse(fileContents) as PurchaseRecord; - return parsed; - } catch (error: any) { - if (error?.code === 'ENOENT') { - continue; - } - - console.error('[verify-purchase] Failed to read purchase file', { filePath, error }); - throw error; - } - } - - return null; -} - async function verifyAndCreateSession(options: { purchaseCode?: string | null; email?: string | null; @@ -121,7 +77,7 @@ async function verifyAndCreateSession(options: { } try { - const purchase = await loadPurchase(normalisedCode); + const purchase = await getPurchaseData(normalisedCode); if (!purchase) { return jsonResponse({ error: GENERIC_ERROR_MESSAGE }, 404); diff --git a/astro/src/test/api/stripe-webhook.test.ts b/astro/src/test/api/stripe-webhook.test.ts index b2988752..94c37f4b 100644 --- a/astro/src/test/api/stripe-webhook.test.ts +++ b/astro/src/test/api/stripe-webhook.test.ts @@ -1,12 +1,9 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import fs from 'fs/promises'; +import { storePurchaseData } from '@lib/purchase-storage'; -// Mock fs module -vi.mock('fs/promises', () => ({ - default: { - mkdir: vi.fn(), - writeFile: vi.fn(), - }, +// Mock purchase storage +vi.mock('@lib/purchase-storage', () => ({ + storePurchaseData: vi.fn().mockResolvedValue(undefined), })); // Mock Stripe @@ -27,10 +24,6 @@ describe('stripe-webhook API', () => { beforeEach(() => { vi.clearAllMocks(); - // Mock successful file operations - (fs.mkdir as any).mockResolvedValue(undefined); - (fs.writeFile as any).mockResolvedValue(undefined); - // Mock successful email sending (global.fetch as any).mockResolvedValue({ ok: true, @@ -83,9 +76,11 @@ describe('stripe-webhook API', () => { expect(responseText).toBe('Webhook handled successfully'); // Verify purchase data was stored - expect(fs.writeFile).toHaveBeenCalledWith( - expect.stringMatching(/ESC-12345678\.json$/), - expect.stringContaining('"purchaseCode":"ESC-12345678"') + expect(storePurchaseData).toHaveBeenCalledWith( + expect.objectContaining({ + purchaseCode: 'ESC-12345678', + sessionId: 'cs_test_session123', + }) ); }); @@ -186,13 +181,13 @@ describe('stripe-webhook API', () => { expect(responseText).toBe('Webhook handled successfully'); // Should not store any purchase data for unhandled events - expect(fs.writeFile).not.toHaveBeenCalled(); + expect(storePurchaseData).not.toHaveBeenCalled(); }); it('should handle file system errors gracefully', async () => { const { POST } = await import('../../pages/api/stripe-webhook'); - (fs.mkdir as any).mockRejectedValue(new Error('Permission denied')); + (storePurchaseData as any).mockRejectedValueOnce(new Error('Permission denied')); const mockEvent = { type: 'checkout.session.completed', @@ -277,7 +272,7 @@ describe('stripe-webhook API', () => { expect(response.status).toBe(200); // Purchase data should still be stored - expect(fs.writeFile).toHaveBeenCalled(); + expect(storePurchaseData).toHaveBeenCalled(); }); it('should store complete purchase data with all metadata', async () => { @@ -316,13 +311,14 @@ describe('stripe-webhook API', () => { await POST({ request: mockRequest } as any); // Verify all data is included - expect(fs.writeFile).toHaveBeenCalledWith( - expect.stringMatching(/ESC-12345678\.json$/), - expect.stringMatching(/"purchaseCode":"ESC-12345678"/) + expect(storePurchaseData).toHaveBeenCalledWith( + expect.objectContaining({ + purchaseCode: 'ESC-12345678', + }) ); - const writeCall = (fs.writeFile as any).mock.calls[0]; - const savedData = JSON.parse(writeCall[1]); + const storeCall = (storePurchaseData as any).mock.calls[0]; + const savedData = storeCall[0]; expect(savedData).toMatchObject({ sessionId: 'cs_test_session123', From 80f9b96c6651c543927709d1cbcf5045d955e79e Mon Sep 17 00:00:00 2001 From: Brian Montgomery Date: Thu, 12 Feb 2026 15:17:25 -0500 Subject: [PATCH 27/27] Initial setup with Drizzle and Postgres --- Makefile | 20 +- astro/astro.config.mjs | 8 +- astro/drizzle.config.ts | 11 + astro/package-lock.json | 4978 ++++++++++++++++++++++++--------------- astro/package.json | 6 + astro/src/db/index.ts | 4 + astro/src/db/schema.ts | 9 + astro/src/env.d.ts | 4 + docker-compose.yml | 25 + 9 files changed, 3223 insertions(+), 1842 deletions(-) create mode 100644 astro/drizzle.config.ts create mode 100644 astro/src/db/index.ts create mode 100644 astro/src/db/schema.ts diff --git a/Makefile b/Makefile index a52cd879..eb4587d0 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ up: @docker compose up --detach down: - @docker compose down + @docker compose --profile "*" down shell: up @docker compose exec $(CONTAINER) bash @@ -50,6 +50,17 @@ update-dependencies: up build: @docker compose build +serve-tools: + @docker compose --profile tools up --detach + @echo Running Adminer at http://localhost:8080/ + +db-push: + @docker compose exec ${CONTAINER} sh -c "npx drizzle-kit push" + +db-migrate: + @docker compose exec ${CONTAINER} sh -c "npx drizzle-kit generate" + @docker compose exec ${CONTAINER} sh -c "npx drizzle-kit migrate" + $(SOURCE_DIR)/node_modules: @echo Installing JS dependencies. This will take awhile. docker compose exec $(CONTAINER) sh -c "npm install" @@ -63,6 +74,10 @@ clean-astro-content: @$(RemoveDirCmd) $(call FixPath,$(SOURCE_DIR)/.astro) @$(RemoveDirCmd) $(call FixPath,$(SOURCE_DIR)/node_modules/.astro-og-canvas) +clean-db: + @echo Removing database volume + @docker volume rm website_pg_data + clean-js-dist: @echo Removing the $(SOURCE_DIR)/dist directory. @$(RemoveDirCmd) $(call FixPath,$(SOURCE_DIR)/dist) @@ -74,5 +89,6 @@ clean-js-modules: clean: clean-js-dist clean-js-modules .PHONY: serve up down build shell dist version \ - clean clean-astro-content clean-js-dist clean-js-modules \ + prettier serve-tools upgrade-astro update-dependencies \ + clean clean-astro-content clean-db clean-js-dist clean-js-modules \ .FORCE diff --git a/astro/astro.config.mjs b/astro/astro.config.mjs index 2a0aeaa6..2df67611 100644 --- a/astro/astro.config.mjs +++ b/astro/astro.config.mjs @@ -1,4 +1,4 @@ -import { defineConfig } from "astro/config"; +import { defineConfig, envField } from "astro/config"; import styleGuide from "./style-guide/register.js"; import icon from "astro-icon"; @@ -49,6 +49,12 @@ export default defineConfig({ }, }, + env: { + schema: { + DATABASE_URL: envField.string({ context: "server", access: "secret", optional: false }), + } + }, + integrations: [ mdx(), styleGuide(), diff --git a/astro/drizzle.config.ts b/astro/drizzle.config.ts new file mode 100644 index 00000000..0b5c1c92 --- /dev/null +++ b/astro/drizzle.config.ts @@ -0,0 +1,11 @@ +import 'dotenv/config'; +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + out: './drizzle', + schema: './src/db/schema.ts', + dialect: 'postgresql', + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}); diff --git a/astro/package-lock.json b/astro/package-lock.json index 943039f8..0b86f967 100644 --- a/astro/package-lock.json +++ b/astro/package-lock.json @@ -23,14 +23,20 @@ "astro-og-canvas": "0.5.6", "astro-robots-txt": "1.0.0", "bootstrap": "5.3.8", + "dotenv": "17.2.4", + "drizzle-orm": "0.45.1", "lodash-es": "4.17.23", + "pg": "8.18.0", "sanitize-html": "2.13.0", "sass": "1.77.5" }, "devDependencies": { + "@types/pg": "^8.16.0", + "drizzle-kit": "^0.31.9", "jsdom": "^27.0.0", "prettier": "^3.2.5", "prettier-plugin-astro": "^0.14.0", + "tsx": "^4.21.0", "vitest": "^2.1.4" } }, @@ -480,6 +486,13 @@ "node": ">=18" } }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@emnapi/runtime": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", @@ -503,417 +516,853 @@ "node": ">=18.0.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", "cpu": [ - "arm64" + "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "netbsd" + "openbsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/openharmony-arm64": { + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ - "arm64" + "ppc64" ], "license": "MIT", "optional": true, "os": [ - "openharmony" + "aix" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { + "node_modules/@esbuild/android-arm": { "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ - "x64" + "arm" ], "license": "MIT", "optional": true, "os": [ - "sunos" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-arm64": { + "node_modules/@esbuild/android-arm64": { "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ - "win32" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-ia32": { + "node_modules/@esbuild/android-x64": { "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ - "ia32" + "x64" ], "license": "MIT", "optional": true, "os": [ - "win32" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-x64": { + "node_modules/@esbuild/darwin-arm64": { "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ - "x64" + "arm64" ], "license": "MIT", "optional": true, "os": [ - "win32" + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" ], "engines": { "node": ">=18" @@ -4241,6 +4690,18 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "license": "MIT" }, + "node_modules/@types/pg": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz", + "integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/retry": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", @@ -6522,9 +6983,9 @@ } }, "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "version": "17.2.4", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.4.tgz", + "integrity": "sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -6533,6 +6994,147 @@ "url": "https://dotenvx.com" } }, + "node_modules/drizzle-kit": { + "version": "0.31.9", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.9.tgz", + "integrity": "sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.4", + "esbuild-register": "^3.5.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.1.tgz", + "integrity": "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, "node_modules/dset": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", @@ -6703,6 +7305,19 @@ "@esbuild/win32-x64": "0.25.12" } }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -7392,6 +8007,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/github-slugger": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", @@ -8539,7 +9167,19 @@ "lambda-local": "build/cli.js" }, "engines": { - "node": ">=8" + "node": ">=8" + } + }, + "node_modules/lambda-local/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, "node_modules/lazystream": { @@ -10124,2417 +10764,3068 @@ ], "license": "MIT", "engines": { - "node": ">=10.5.0" + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-forge": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-mock-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", + "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", + "license": "MIT" + }, + "node_modules/node-source-walk": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/node-source-walk/-/node-source-walk-7.0.1.tgz", + "integrity": "sha512-3VW/8JpPqPvnJvseXowjZcirPisssnBuDikk6JIZ8jQzF7KJQX52iPFX4RYYxLycYH7IbMRSPUOga/esVjy5Yg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.7" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-stream-zip": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", + "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/antelle" + } + }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, + "node_modules/omit.js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/omit.js/-/omit.js-2.0.2.tgz", + "integrity": "sha512-hJmu9D+bNB40YpL9jYebQl4lsTW6yEHRTroJzNLqQJYHm7c+NQnJGfZmIWh8S3q3KoaxV1aLhV6B3+0N0/kyJg==", + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/oniguruma-parser": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz", + "integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.1", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/p-event": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-6.0.1.tgz", + "integrity": "sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==", + "license": "MIT", + "dependencies": { + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz", + "integrity": "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "node_modules/p-locate/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", "license": "MIT", "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" + "yocto-queue": "^1.0.0" }, "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/node-fetch-native": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", - "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", - "license": "MIT" - }, - "node_modules/node-forge": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", - "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", - "license": "(BSD-3-Clause OR GPL-2.0)", + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "license": "MIT", "engines": { - "node": ">= 6.13.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "node_modules/p-queue": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.1.tgz", + "integrity": "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==", "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/node-mock-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", - "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", - "license": "MIT" - }, - "node_modules/node-source-walk": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/node-source-walk/-/node-source-walk-7.0.1.tgz", - "integrity": "sha512-3VW/8JpPqPvnJvseXowjZcirPisssnBuDikk6JIZ8jQzF7KJQX52iPFX4RYYxLycYH7IbMRSPUOga/esVjy5Yg==", + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.7" + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" }, "engines": { - "node": ">=18" + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/node-stream-zip": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", - "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", + "node_modules/p-timeout": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", + "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", "license": "MIT", "engines": { - "node": ">=0.12.0" + "node": ">=14.16" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/antelle" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/nopt": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", - "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", - "license": "ISC", + "node_modules/p-wait-for": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/p-wait-for/-/p-wait-for-5.0.2.tgz", + "integrity": "sha512-lwx6u1CotQYPVju77R+D0vFomni/AqRfqLmqQ8hekklqZ6gAY9rONh7lBQ0uxWMkC2AuX9b2DVAl8To0NyP1JA==", + "license": "MIT", "dependencies": { - "abbrev": "^3.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" + "p-timeout": "^6.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/normalize-package-data": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", - "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", - "license": "BSD-2-Clause", + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", "dependencies": { - "hosted-git-info": "^7.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" }, - "engines": { - "node": "^16.14.0 || >=18.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-gitignore": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-gitignore/-/parse-gitignore-2.0.0.tgz", + "integrity": "sha512-RmVuCHWsfu0QPNW+mraxh/xjQVw/lhUCUru8Zni3Ctq3AoMhpDTq0OVdKS6iesd6Kqb7viCV3isAL43dciOSog==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=14" } }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "license": "MIT", + "node_modules/parse-imports": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/parse-imports/-/parse-imports-2.2.1.tgz", + "integrity": "sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==", + "license": "Apache-2.0 AND MIT", "dependencies": { - "path-key": "^4.0.0" + "es-module-lexer": "^1.5.3", + "slashes": "^3.0.12" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 18" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "license": "BSD-2-Clause", + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "license": "MIT", "dependencies": { - "boolbase": "^1.0.0" + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" }, "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ofetch": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", - "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "node_modules/parse-latin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", + "integrity": "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==", "license": "MIT", "dependencies": { - "destr": "^2.0.5", - "node-fetch-native": "^1.6.7", - "ufo": "^1.6.1" + "@types/nlcst": "^2.0.0", + "@types/unist": "^3.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-modify-children": "^4.0.0", + "unist-util-visit-children": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/ohash": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", - "license": "MIT" - }, - "node_modules/omit.js": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/omit.js/-/omit.js-2.0.2.tgz", - "integrity": "sha512-hJmu9D+bNB40YpL9jYebQl4lsTW6yEHRTroJzNLqQJYHm7c+NQnJGfZmIWh8S3q3KoaxV1aLhV6B3+0N0/kyJg==", - "license": "MIT" - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", "dependencies": { - "wrappy": "1" + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", "license": "MIT", "dependencies": { - "fn.name": "1.x.x" + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", "license": "MIT", "dependencies": { - "mimic-fn": "^4.0.0" + "parse5": "^7.0.0" }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", "engines": { - "node": ">=12" + "node": ">=0.12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/oniguruma-parser": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", - "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", - "license": "MIT" - }, - "node_modules/oniguruma-to-es": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz", - "integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==", + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", "license": "MIT", - "dependencies": { - "oniguruma-parser": "^0.12.1", - "regex": "^6.0.1", - "regex-recursion": "^6.0.2" + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/p-event": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/p-event/-/p-event-6.0.1.tgz", - "integrity": "sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==", + "node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", "license": "MIT", - "dependencies": { - "p-timeout": "^6.1.2" - }, "engines": { - "node": ">=16.17" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-limit": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz", - "integrity": "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==", - "license": "MIT", + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", "dependencies": { - "yocto-queue": "^1.1.1" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=18" + "node": ">=16 || 14 >=14.18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/p-locate": { + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/path-type": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", "license": "MIT", - "dependencies": { - "p-limit": "^4.0.0" - }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", + "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", "license": "MIT", "dependencies": { - "yocto-queue": "^1.0.0" + "pg-connection-string": "^2.11.0", + "pg-pool": "^3.11.0", + "pg-protocol": "^1.11.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">= 16.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } } }, - "node_modules/p-map": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz", + "integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4.0.0" } }, - "node_modules/p-queue": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.1.tgz", - "integrity": "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==", + "node_modules/pg-pool": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz", + "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", + "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", "license": "MIT", "dependencies": { - "eventemitter3": "^5.0.1", - "p-timeout": "^6.1.2" + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" }, "engines": { - "node": ">=18" + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/piccolore": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", + "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==", + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/p-retry": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", - "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "node_modules/picoquery": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/picoquery/-/picoquery-2.5.0.tgz", + "integrity": "sha512-j1kgOFxtaCyoFCkpoYG2Oj3OdGakadO7HZ7o5CqyRazlmBekKhbDoUnNnXASE07xSY4nDImWZkrZv7toSxMi/g==", + "license": "MIT" + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", "license": "MIT", "dependencies": { - "@types/retry": "0.12.2", - "is-network-error": "^1.0.0", - "retry": "^0.13.1" + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^10 || ^12 || >=14" } }, - "node_modules/p-timeout": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", - "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", - "license": "MIT", + "node_modules/postcss-values-parser": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-6.0.2.tgz", + "integrity": "sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==", + "license": "MPL-2.0", + "dependencies": { + "color-name": "^1.1.4", + "is-url-superb": "^4.0.0", + "quote-unquote": "^1.0.0" + }, "engines": { - "node": ">=14.16" + "node": ">=10" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "postcss": "^8.2.9" } }, - "node_modules/p-wait-for": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/p-wait-for/-/p-wait-for-5.0.2.tgz", - "integrity": "sha512-lwx6u1CotQYPVju77R+D0vFomni/AqRfqLmqQ8hekklqZ6gAY9rONh7lBQ0uxWMkC2AuX9b2DVAl8To0NyP1JA==", + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", "license": "MIT", - "dependencies": { - "p-timeout": "^6.0.0" - }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, - "node_modules/package-json-from-dist": { + "node_modules/postgres-bytea": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, - "node_modules/package-manager-detector": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", - "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", - "license": "MIT" - }, - "node_modules/parse-entities": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", - "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/parse-gitignore": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-gitignore/-/parse-gitignore-2.0.0.tgz", - "integrity": "sha512-RmVuCHWsfu0QPNW+mraxh/xjQVw/lhUCUru8Zni3Ctq3AoMhpDTq0OVdKS6iesd6Kqb7viCV3isAL43dciOSog==", + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", "license": "MIT", "engines": { - "node": ">=14" + "node": ">=0.10.0" } }, - "node_modules/parse-imports": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/parse-imports/-/parse-imports-2.2.1.tgz", - "integrity": "sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==", - "license": "Apache-2.0 AND MIT", + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", "dependencies": { - "es-module-lexer": "^1.5.3", - "slashes": "^3.0.12" + "xtend": "^4.0.0" }, "engines": { - "node": ">= 18" + "node": ">=0.10.0" } }, - "node_modules/parse-json": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", - "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "node_modules/precinct": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/precinct/-/precinct-12.2.0.tgz", + "integrity": "sha512-NFBMuwIfaJ4SocE9YXPU/n4AcNSoFMVFjP72nvl3cx69j/ke61/hPOWFREVxLkFhhEGnA8ZuVfTqJBa+PK3b5w==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "index-to-position": "^1.1.0", - "type-fest": "^4.39.1" + "@dependents/detective-less": "^5.0.1", + "commander": "^12.1.0", + "detective-amd": "^6.0.1", + "detective-cjs": "^6.0.1", + "detective-es6": "^5.0.1", + "detective-postcss": "^7.0.1", + "detective-sass": "^6.0.1", + "detective-scss": "^5.0.1", + "detective-stylus": "^5.0.1", + "detective-typescript": "^14.0.0", + "detective-vue2": "^2.2.0", + "module-definition": "^6.0.1", + "node-source-walk": "^7.0.1", + "postcss": "^8.5.1", + "typescript": "^5.7.3" + }, + "bin": { + "precinct": "bin/cli.js" }, "engines": { "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse-latin": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", - "integrity": "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==", + "node_modules/precinct/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "license": "MIT", - "dependencies": { - "@types/nlcst": "^2.0.0", - "@types/unist": "^3.0.0", - "nlcst-to-string": "^4.0.0", - "unist-util-modify-children": "^4.0.0", - "unist-util-visit-children": "^3.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "engines": { + "node": ">=18" } }, - "node_modules/parse-srcset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", - "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", - "license": "MIT" - }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, "license": "MIT", - "dependencies": { - "entities": "^6.0.0" + "bin": { + "prettier": "bin/prettier.cjs" }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", - "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", - "license": "MIT", - "dependencies": { - "domhandler": "^5.0.3", - "parse5": "^7.0.0" + "engines": { + "node": ">=14" }, "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/parse5-parser-stream": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", - "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "node_modules/prettier-plugin-astro": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-astro/-/prettier-plugin-astro-0.14.1.tgz", + "integrity": "sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==", + "dev": true, "license": "MIT", "dependencies": { - "parse5": "^7.0.0" + "@astrojs/compiler": "^2.9.1", + "prettier": "^3.0.0", + "sass-formatter": "^0.7.6" }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "node": "^14.15.0 || >=16.0.0" } }, - "node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=6" } }, - "node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.6.0" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" }, "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">= 6" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/path-type": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", - "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", "license": "MIT", - "engines": { - "node": ">=18" - }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "license": "MIT" + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 14.16" + "node": ">=6" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], "license": "MIT" }, - "node_modules/piccolore": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", - "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==", - "license": "ISC" + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" + "node_modules/quote-unquote": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/quote-unquote/-/quote-unquote-1.0.0.tgz", + "integrity": "sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==", + "license": "MIT" }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, + "node_modules/read-package-up": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", + "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", "license": "MIT", + "dependencies": { + "find-up-simple": "^1.0.0", + "read-pkg": "^9.0.0", + "type-fest": "^4.6.0" + }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/picoquery": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/picoquery/-/picoquery-2.5.0.tgz", - "integrity": "sha512-j1kgOFxtaCyoFCkpoYG2Oj3OdGakadO7HZ7o5CqyRazlmBekKhbDoUnNnXASE07xSY4nDImWZkrZv7toSxMi/g==", - "license": "MIT" - }, - "node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", "license": "MIT", "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", - "pathe": "^2.0.3" + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/postcss-values-parser": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-6.0.2.tgz", - "integrity": "sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==", - "license": "MPL-2.0", + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", "dependencies": { - "color-name": "^1.1.4", - "is-url-superb": "^4.0.0", - "quote-unquote": "^1.0.0" + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" }, "engines": { "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" }, - "peerDependencies": { - "postcss": "^8.2.9" + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/precinct": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/precinct/-/precinct-12.2.0.tgz", - "integrity": "sha512-NFBMuwIfaJ4SocE9YXPU/n4AcNSoFMVFjP72nvl3cx69j/ke61/hPOWFREVxLkFhhEGnA8ZuVfTqJBa+PK3b5w==", + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", "license": "MIT", "dependencies": { - "@dependents/detective-less": "^5.0.1", - "commander": "^12.1.0", - "detective-amd": "^6.0.1", - "detective-cjs": "^6.0.1", - "detective-es6": "^5.0.1", - "detective-postcss": "^7.0.1", - "detective-sass": "^6.0.1", - "detective-scss": "^5.0.1", - "detective-stylus": "^5.0.1", - "detective-typescript": "^14.0.0", - "detective-vue2": "^2.2.0", - "module-definition": "^6.0.1", - "node-source-walk": "^7.0.1", - "postcss": "^8.5.1", - "typescript": "^5.7.3" - }, - "bin": { - "precinct": "bin/cli.js" + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" }, - "engines": { - "node": ">=18" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/precinct/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", - "dev": true, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" }, "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/prettier-plugin-astro": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-astro/-/prettier-plugin-astro-0.14.1.tgz", - "integrity": "sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==", - "dev": true, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", "license": "MIT", "dependencies": { - "@astrojs/compiler": "^2.9.1", - "prettier": "^3.0.0", - "sass-formatter": "^0.7.6" + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" }, - "engines": { - "node": "^14.15.0 || >=16.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/prismjs": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", - "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "regex-utilities": "^2.3.0" } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", "license": "MIT", - "engines": { - "node": ">= 0.6.0" + "dependencies": { + "regex-utilities": "^2.3.0" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", "license": "MIT" }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "node_modules/rehype": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz", + "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==", "license": "MIT", "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" + "@types/hast": "^3.0.0", + "rehype-parse": "^9.0.0", + "rehype-stringify": "^10.0.0", + "unified": "^11.0.0" }, - "engines": { - "node": ">= 6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/property-information": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", "license": "MIT", "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/quansync": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", - "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/antfu" - }, - { - "type": "individual", - "url": "https://github.com/sponsors/sxzz" - } - ], - "license": "MIT" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "node_modules/quote-unquote": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/quote-unquote/-/quote-unquote-1.0.0.tgz", - "integrity": "sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==", - "license": "MIT" + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "node_modules/radix3": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", - "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", - "license": "MIT" + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "node_modules/read-package-up": { + "node_modules/remark-parse": { "version": "11.0.0", - "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", - "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", "license": "MIT", "dependencies": { - "find-up-simple": "^1.0.0", - "read-pkg": "^9.0.0", - "type-fest": "^4.6.0" - }, - "engines": { - "node": ">=18" + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/read-pkg": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", - "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", "license": "MIT", "dependencies": { - "@types/normalize-package-data": "^2.4.3", - "normalize-package-data": "^6.0.0", - "parse-json": "^8.0.0", - "type-fest": "^4.6.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "node_modules/remark-smartypants": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz", + "integrity": "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==", "license": "MIT", "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" + "retext": "^9.0.0", + "retext-smartypants": "^6.0.0", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=16.0.0" } }, - "node_modules/readdir-glob": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", - "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", - "license": "Apache-2.0", + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", "dependencies": { - "minimatch": "^5.1.0" + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "license": "ISC", + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "license": "ISC" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", + "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.1" + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.8" }, "engines": { - "node": ">=10" + "node": ">=8.6.0" } }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "node_modules/require-in-the-middle/node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, "engines": { - "node": ">= 14.18.0" + "node": ">= 0.4" }, "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/recma-build-jsx": { + "node_modules/require-package-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", + "integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", - "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retext": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", + "integrity": "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==", "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0", - "estree-util-build-jsx": "^3.0.0", - "vfile": "^6.0.0" + "@types/nlcst": "^2.0.0", + "retext-latin": "^4.0.0", + "retext-stringify": "^4.0.0", + "unified": "^11.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/recma-jsx": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", - "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "node_modules/retext-latin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-4.0.0.tgz", + "integrity": "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==", "license": "MIT", "dependencies": { - "acorn-jsx": "^5.0.0", - "estree-util-to-js": "^2.0.0", - "recma-parse": "^1.0.0", - "recma-stringify": "^1.0.0", + "@types/nlcst": "^2.0.0", + "parse-latin": "^7.0.0", "unified": "^11.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz", + "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" }, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/recma-parse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", - "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "node_modules/retext-stringify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-4.0.0.tgz", + "integrity": "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==", "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0", - "esast-util-from-js": "^2.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unified": "^11.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/recma-stringify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", - "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0", - "estree-util-to-js": "^2.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" + "@types/estree": "1.0.8" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" } }, - "node_modules/regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", - "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT", "dependencies": { - "regex-utilities": "^2.3.0" + "queue-microtask": "^1.2.2" } }, - "node_modules/regex-recursion": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", - "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "node_modules/s.color": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/s.color/-/s.color-0.0.15.tgz", + "integrity": "sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", "license": "MIT", - "dependencies": { - "regex-utilities": "^2.3.0" + "engines": { + "node": ">=10" } }, - "node_modules/regex-utilities": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", - "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/rehype": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz", - "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==", + "node_modules/sanitize-html": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.0.tgz", + "integrity": "sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==", "license": "MIT", "dependencies": { - "@types/hast": "^3.0.0", - "rehype-parse": "^9.0.0", - "rehype-stringify": "^10.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" } }, - "node_modules/rehype-parse": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", - "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "node_modules/sanitize-html/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-from-html": "^2.0.0", - "unified": "^11.0.0" + "engines": { + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/rehype-raw": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", - "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "node_modules/sanitize-html/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], "license": "MIT", "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-raw": "^9.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" } }, - "node_modules/rehype-recma": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", - "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "node_modules/sass": { + "version": "1.77.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.5.tgz", + "integrity": "sha512-oDfX1mukIlxacPdQqNb6mV2tVCrnE+P3nVYioy72V5tlk56CPNcO4TCuFcaCRKKfJ1M3lH95CleRS+dVKL2qMg==", "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "hast-util-to-estree": "^3.0.0" + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" } }, - "node_modules/rehype-stringify": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", - "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "node_modules/sass-formatter": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/sass-formatter/-/sass-formatter-0.7.9.tgz", + "integrity": "sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==", + "dev": true, "license": "MIT", "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-to-html": "^9.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "suf-log": "^2.5.3" } }, - "node_modules/remark-gfm": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", - "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "node_modules/sass/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-gfm": "^3.0.0", - "micromark-extension-gfm": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.0" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "node_modules/remark-mdx": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", - "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "node_modules/sass/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "license": "MIT", - "dependencies": { - "mdast-util-mdx": "^3.0.0", - "micromark-extension-mdxjs": "^3.0.0" + "engines": { + "node": ">=8.6" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/remark-parse": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "node_modules/sass/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" + "picomatch": "^2.2.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=8.10.0" } }, - "node_modules/remark-rehype": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", - "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", - "license": "MIT", + "node_modules/sax": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "mdast-util-to-hast": "^13.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" + "xmlchars": "^2.2.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=v12.22.7" } }, - "node_modules/remark-smartypants": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz", - "integrity": "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==", - "license": "MIT", - "dependencies": { - "retext": "^9.0.0", - "retext-smartypants": "^6.0.0", - "unified": "^11.0.4", - "unist-util-visit": "^5.0.0" + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=16.0.0" + "node": ">=10" } }, - "node_modules/remark-stringify": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", - "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", - "license": "MIT", + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-to-markdown": "^2.0.0", - "unified": "^11.0.0" + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, - "node_modules/remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", - "license": "ISC" - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/require-in-the-middle": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", - "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", + "node_modules/shiki": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.22.0.tgz", + "integrity": "sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g==", "license": "MIT", "dependencies": { - "debug": "^4.3.5", - "module-details-from-path": "^1.0.3", - "resolve": "^1.22.8" - }, - "engines": { - "node": ">=8.6.0" + "@shikijs/core": "3.22.0", + "@shikijs/engine-javascript": "3.22.0", + "@shikijs/engine-oniguruma": "3.22.0", + "@shikijs/langs": "3.22.0", + "@shikijs/themes": "3.22.0", + "@shikijs/types": "3.22.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" } }, - "node_modules/require-in-the-middle/node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", "engines": { - "node": ">= 0.4" + "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/require-package-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", - "integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==", + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "license": "MIT" }, - "node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "node_modules/sitemap": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-8.0.2.tgz", + "integrity": "sha512-LwktpJcyZDoa0IL6KT++lQ53pbSrx2c9ge41/SeLTyqy2XUNA6uR4+P9u5IVo5lPeL2arAcOKn1aZAxoYbCKlQ==", "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" + "@types/node": "^17.0.5", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.4.1" }, "bin": { - "resolve": "bin/resolve" + "sitemap": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0", + "npm": ">=6.0.0" + } + }, + "node_modules/sitemap/node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "license": "MIT" + }, + "node_modules/slashes": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/slashes/-/slashes-3.0.12.tgz", + "integrity": "sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==", + "license": "ISC" + }, + "node_modules/smol-toml": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz", + "integrity": "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/cyyynthia" } }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "license": "MIT", + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", "engines": { - "node": ">=8" + "node": ">= 12" } }, - "node_modules/retext": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", - "integrity": "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==", - "license": "MIT", - "dependencies": { - "@types/nlcst": "^2.0.0", - "retext-latin": "^4.0.0", - "retext-stringify": "^4.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/retext-latin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-4.0.0.tgz", - "integrity": "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==", + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", "dependencies": { - "@types/nlcst": "^2.0.0", - "parse-latin": "^7.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, - "node_modules/retext-smartypants": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz", - "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==", - "license": "MIT", - "dependencies": { - "@types/nlcst": "^2.0.0", - "nlcst-to-string": "^4.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/retext-stringify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-4.0.0.tgz", - "integrity": "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==", + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", "license": "MIT", - "dependencies": { - "@types/nlcst": "^2.0.0", - "nlcst-to-string": "^4.0.0", - "unified": "^11.0.0" - }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "license": "MIT", - "engines": { - "node": ">= 4" + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "license": "CC-BY-3.0" }, - "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "license": "CC0-1.0" + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", - "fsevents": "~2.3.2" + "node": ">= 10.x" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" + "engines": { + "node": "*" } }, - "node_modules/s.color": { - "version": "0.0.15", - "resolved": "https://registry.npmjs.org/s.color/-/s.color-0.0.15.tgz", - "integrity": "sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==", + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, "license": "MIT" }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "license": "MIT" }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "node_modules/stream-replace-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz", + "integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==", + "license": "MIT" + }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/sanitize-html": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.0.tgz", - "integrity": "sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==", + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", "dependencies": { - "deepmerge": "^4.2.2", - "escape-string-regexp": "^4.0.0", - "htmlparser2": "^8.0.0", - "is-plain-object": "^5.0.0", - "parse-srcset": "^1.0.2", - "postcss": "^8.3.11" + "safe-buffer": "~5.2.0" } }, - "node_modules/sanitize-html/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/sanitize-html/node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, - "node_modules/sass": { - "version": "1.77.5", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.5.tgz", - "integrity": "sha512-oDfX1mukIlxacPdQqNb6mV2tVCrnE+P3nVYioy72V5tlk56CPNcO4TCuFcaCRKKfJ1M3lH95CleRS+dVKL2qMg==", + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=8" } }, - "node_modules/sass-formatter": { - "version": "0.7.9", - "resolved": "https://registry.npmjs.org/sass-formatter/-/sass-formatter-0.7.9.tgz", - "integrity": "sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==", - "dev": true, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", - "dependencies": { - "suf-log": "^2.5.3" + "engines": { + "node": ">=8" } }, - "node_modules/sass/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node": ">=8" } }, - "node_modules/sass/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", "license": "MIT", - "engines": { - "node": ">=8.6" + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/sass/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", "dependencies": { - "picomatch": "^2.2.1" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/sax": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", - "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "license": "ISC", + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", "dependencies": { - "xmlchars": "^2.2.0" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=v12.22.7" + "node": ">=8" } }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=8" } }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=12" }, "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", "license": "MIT", "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" + "inline-style-parser": "0.2.7" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "node_modules/suf-log": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/suf-log/-/suf-log-2.5.3.tgz", + "integrity": "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "s.color": "0.0.15" } }, - "node_modules/shiki": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.22.0.tgz", - "integrity": "sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g==", + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "license": "MIT", "dependencies": { - "@shikijs/core": "3.22.0", - "@shikijs/engine-javascript": "3.22.0", - "@shikijs/engine-oniguruma": "3.22.0", - "@shikijs/langs": "3.22.0", - "@shikijs/themes": "3.22.0", - "@shikijs/types": "3.22.0", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", "engines": { - "node": ">=14" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "license": "MIT" - }, - "node_modules/sitemap": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-8.0.2.tgz", - "integrity": "sha512-LwktpJcyZDoa0IL6KT++lQ53pbSrx2c9ge41/SeLTyqy2XUNA6uR4+P9u5IVo5lPeL2arAcOKn1aZAxoYbCKlQ==", + "node_modules/svgo": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz", + "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==", "license": "MIT", "dependencies": { - "@types/node": "^17.0.5", - "@types/sax": "^1.2.1", - "arg": "^5.0.0", + "commander": "^11.1.0", + "css-select": "^5.1.0", + "css-tree": "^3.0.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", "sax": "^1.4.1" }, "bin": { - "sitemap": "dist/cli.js" + "svgo": "bin/svgo.js" }, "engines": { - "node": ">=14.0.0", - "npm": ">=6.0.0" - } - }, - "node_modules/sitemap/node_modules/@types/node": { - "version": "17.0.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", - "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", - "license": "MIT" - }, - "node_modules/slashes": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/slashes/-/slashes-3.0.12.tgz", - "integrity": "sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==", - "license": "ISC" - }, - "node_modules/smol-toml": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz", - "integrity": "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 18" + "node": ">=16" }, "funding": { - "url": "https://github.com/sponsors/cyyynthia" + "type": "opencollective", + "url": "https://opencollective.com/svgo" } }, - "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "license": "BSD-3-Clause", + "node_modules/svgo/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", "engines": { - "node": ">= 12" + "node": ">=16" } }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "node_modules/system-architecture": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz", + "integrity": "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==", "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", + "node_modules/tar": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" } }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", "license": "Apache-2.0", "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" + "b4a": "^1.6.4" } }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "license": "CC-BY-3.0" + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "license": "MIT", "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/spdx-license-ids": { - "version": "3.0.22", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", - "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", - "license": "CC0-1.0" - }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, "license": "MIT", "engines": { - "node": "*" + "node": "^18.0.0 || >=20.0.0" } }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "license": "MIT" - }, - "node_modules/stream-replace-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz", - "integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==", - "license": "MIT" - }, - "node_modules/streamx": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", - "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", "license": "MIT", - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" + "engines": { + "node": ">=14.0.0" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" + "engines": { + "node": ">=14.0.0" } }, - "node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "node_modules/tldts": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", + "integrity": "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==", + "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "tldts-core": "^7.0.23" }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz", + "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=14.14" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" + "tmp": "^0.2.0" } }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { + "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, "engines": { - "node": ">=8" + "node": ">=8.0" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", "license": "MIT" }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", + "node_modules/tomlify-j0.4": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tomlify-j0.4/-/tomlify-j0.4-3.0.0.tgz", + "integrity": "sha512-2Ulkc8T7mXJ2l0W476YC/A209PR38Nw8PuaCNtk9uI3t1zzFdGQeWYGQvmj2PZkVvRC/Yoi4xQKMRnWc/N29tQ==", + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "ansi-regex": "^5.0.1" + "tldts": "^7.0.5" }, "engines": { - "node": ">=8" + "node": ">=16" } }, - "node_modules/stringify-entities": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, "license": "MIT", "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" + "punycode": "^2.3.1" }, + "engines": { + "node": ">=20" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">= 14.0.0" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" } }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, "engines": { - "node": ">=12" + "node": "^18 || >=20" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/strnum": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", - "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, - "node_modules/style-to-js": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", - "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "devOptional": true, "license": "MIT", "dependencies": { - "style-to-object": "1.0.14" + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" } }, - "node_modules/style-to-object": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", - "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "inline-style-parser": "0.2.7" + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" } }, - "node_modules/suf-log": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/suf-log/-/suf-log-2.5.3.tgz", - "integrity": "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==", + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "s.color": "0.0.15" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=18" } }, - "node_modules/svgo": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz", - "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==", + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "commander": "^11.1.0", - "css-select": "^5.1.0", - "css-tree": "^3.0.1", - "css-what": "^6.1.0", - "csso": "^5.0.5", - "picocolors": "^1.1.1", - "sax": "^1.4.1" - }, - "bin": { - "svgo": "bin/svgo.js" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" + "node": ">=18" } }, - "node_modules/svgo/node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=16" + "node": ">=18" } }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/system-architecture": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz", - "integrity": "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==", "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tar": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", - "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { "node": ">=18" } }, - "node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/text-decoder": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", - "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", - "license": "MIT" - }, - "node_modules/tiny-inflate": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", - "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", - "license": "MIT" - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=18" } }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "node": ">=18" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": ">=18" } }, - "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14.0.0" + "node": ">=18" } }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14.0.0" + "node": ">=18" } }, - "node_modules/tldts": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", - "integrity": "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==", + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "tldts-core": "^7.0.23" - }, - "bin": { - "tldts": "bin/cli.js" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/tldts-core": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz", - "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==", + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=14.14" + "node": ">=18" } }, - "node_modules/tmp-promise": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", - "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "tmp": "^0.2.0" + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=8.0" + "node": ">=18" } }, - "node_modules/toml": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", - "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", - "license": "MIT" - }, - "node_modules/tomlify-j0.4": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tomlify-j0.4/-/tomlify-j0.4-3.0.0.tgz", - "integrity": "sha512-2Ulkc8T7mXJ2l0W476YC/A209PR38Nw8PuaCNtk9uI3t1zzFdGQeWYGQvmj2PZkVvRC/Yoi4xQKMRnWc/N29tQ==", - "license": "MIT" - }, - "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=16" + "node": ">=18" } }, - "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">=20" + "node": ">=18" } }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" } }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 14.0.0" + "node": ">=18" } }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" + "node": ">=18" } }, - "node_modules/tsconfck": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", - "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "node_modules/tsx/node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "devOptional": true, + "hasInstallScript": true, "license": "MIT", "bin": { - "tsconfck": "bin/tsconfck.js" + "esbuild": "bin/esbuild" }, "engines": { - "node": "^18 || >=20" - }, - "peerDependencies": { - "typescript": "^5.0.0" + "node": ">=18" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, "node_modules/type-fest": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", @@ -14658,6 +15949,15 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/xxhash-wasm": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", diff --git a/astro/package.json b/astro/package.json index 489d291b..90d4c1fd 100644 --- a/astro/package.json +++ b/astro/package.json @@ -26,14 +26,20 @@ "astro-og-canvas": "0.5.6", "astro-robots-txt": "1.0.0", "bootstrap": "5.3.8", + "dotenv": "17.2.4", + "drizzle-orm": "0.45.1", "lodash-es": "4.17.23", + "pg": "8.18.0", "sanitize-html": "2.13.0", "sass": "1.77.5" }, "devDependencies": { + "@types/pg": "^8.16.0", + "drizzle-kit": "^0.31.9", "jsdom": "^27.0.0", "prettier": "^3.2.5", "prettier-plugin-astro": "^0.14.0", + "tsx": "^4.21.0", "vitest": "^2.1.4" } } diff --git a/astro/src/db/index.ts b/astro/src/db/index.ts new file mode 100644 index 00000000..b29b40b4 --- /dev/null +++ b/astro/src/db/index.ts @@ -0,0 +1,4 @@ +import { drizzle } from 'drizzle-orm/node-postgres'; +import { DATABASE_URL } from "astro:env/server"; + +export const db = drizzle(DATABASE_URL!); diff --git a/astro/src/db/schema.ts b/astro/src/db/schema.ts new file mode 100644 index 00000000..5dc37071 --- /dev/null +++ b/astro/src/db/schema.ts @@ -0,0 +1,9 @@ +import { integer, pgTable, varchar } from "drizzle-orm/pg-core"; + +// TODO: This is the example table to make sure everything was hooked up correctly +export const usersTable = pgTable("users", { + id: integer().primaryKey().generatedAlwaysAsIdentity(), + name: varchar({ length: 255 }).notNull(), + age: integer().notNull(), + email: varchar({ length: 255 }).notNull().unique(), +}); diff --git a/astro/src/env.d.ts b/astro/src/env.d.ts index 749cf61e..61a738bd 100644 --- a/astro/src/env.d.ts +++ b/astro/src/env.d.ts @@ -8,3 +8,7 @@ declare namespace App { session?: PurchaseSession; } } + +interface ImportMetaEnv { + readonly DATABASE_URL: string; +} diff --git a/docker-compose.yml b/docker-compose.yml index a3294f18..e1b5268f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,6 @@ +volumes: + pg_data: + services: website: container_name: website-astro @@ -10,3 +13,25 @@ services: - 4322:4322 volumes: - '.:/website' + db: + image: postgres:16 + container_name: postgres + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: db + volumes: + - pg_data:/var/lib/postgresql/data + ports: + - 5432:5432 + + adminer: + image: adminer + container_name: adminer + profiles: + - tools + restart: always + ports: + - 8080:8080 + depends_on: + - db