From 5382659312dfff9e7efb333a4f3576d84082cee7 Mon Sep 17 00:00:00 2001 From: JamesFlare1212 Date: Fri, 9 May 2025 09:04:25 -0400 Subject: [PATCH] init version --- .gitignore | 3 + Dockerfile | 17 + README.md | 12 + docker-compose.yaml | 11 + engage-api/get-activity.mjs | 306 ++++++++++++++ engage-api/login_template.txt | 1 + engage-api/struct-activity.mjs | 151 +++++++ engage-api/struct-staff.mjs | 21 + example.env | 4 + main.js | 105 +++++ package.json | 18 + pnpm-lock.yaml | 721 +++++++++++++++++++++++++++++++++ 12 files changed, 1370 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yaml create mode 100644 engage-api/get-activity.mjs create mode 100644 engage-api/login_template.txt create mode 100644 engage-api/struct-activity.mjs create mode 100644 engage-api/struct-staff.mjs create mode 100644 example.env create mode 100644 main.js create mode 100644 package.json create mode 100644 pnpm-lock.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..441104d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +nkcs-engage.cookie.txt +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5ebe743 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM node:lts-alpine + +ENV NODE_ENV=production + +RUN corepack enable && corepack prepare pnpm@latest --activate + +WORKDIR /usr/src/app + +COPY package.json pnpm-lock.yaml ./ + +RUN pnpm install --frozen-lockfile --prod + +COPY . . + +EXPOSE 3000 + +CMD ["pnpm", "start"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e64a682 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +## How to Run + +copy `example.env` + +```bash +cp example.env .env +``` + +edit `.env` + +`API_USERNAME` is your engage username in URL-encode. +`API_PASSWORD` is your engage password in URL-encode. diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..b160fbc --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,11 @@ +services: + dsas-cca-backend: + build: + context: . + dockerfile: Dockerfile + container_name: dsas-cca-backend + ports: + - "${PORT}:${PORT}" + env_file: + - .env + restart: unless-stopped \ No newline at end of file diff --git a/engage-api/get-activity.mjs b/engage-api/get-activity.mjs new file mode 100644 index 0000000..253cc1d --- /dev/null +++ b/engage-api/get-activity.mjs @@ -0,0 +1,306 @@ +// get-activity.mjs + +import axios from 'axios'; +import fs from 'fs/promises'; // Using fs.promises directly +import path from 'path'; +import { fileURLToPath } from 'url'; + +// --- Replicating __dirname for ESM --- +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// --- Cookie Cache Configuration & In-Memory Cache --- +const COOKIE_FILE_PATH = path.resolve(__dirname, 'nkcs-engage.cookie.txt'); +let _inMemoryCookie = null; + +// --- Custom Error for Authentication --- +class AuthenticationError extends Error { + constructor(message = "Authentication failed, cookie may be invalid.", status) { + super(message); + this.name = "AuthenticationError"; + this.status = status; + } +} + +// --- Cookie Cache Helper Functions --- +async function loadCachedCookie() { + if (_inMemoryCookie) { + console.log("Using in-memory cached cookie."); + return _inMemoryCookie; + } + try { + const cookieFromFile = await fs.readFile(COOKIE_FILE_PATH, 'utf8'); + if (cookieFromFile) { + _inMemoryCookie = cookieFromFile; + console.log("Loaded cookie from file cache."); + return _inMemoryCookie; + } + } catch (err) { + if (err.code === 'ENOENT') { + console.log("Cookie cache file not found. No cached cookie loaded."); + } else { + console.warn("Error loading cookie from file:", err.message); + } + } + return null; +} + +async function saveCookieToCache(cookieString) { + if (!cookieString) { + console.warn("Attempted to save an empty or null cookie. Aborting save."); + return; + } + _inMemoryCookie = cookieString; + try { + await fs.writeFile(COOKIE_FILE_PATH, cookieString, 'utf8'); + console.log("Cookie saved to file cache."); + } catch (err) { + console.error("Error saving cookie to file:", err.message); + } +} + +async function clearCookieCache() { + _inMemoryCookie = null; + try { + await fs.unlink(COOKIE_FILE_PATH); + console.log("Cookie cache file deleted."); + } catch (err) { + if (err.code !== 'ENOENT') { + console.error("Error deleting cookie file:", err.message); + } else { + console.log("Cookie cache file did not exist, no need to delete."); + } + } +} + +async function testCookieValidity(cookieString) { + if (!cookieString) return false; + console.log("Testing cookie validity..."); + try { + const url = 'https://engage.nkcswx.cn/Services/ActivitiesService.asmx/GetActivityDetails'; + const headers = { + 'Content-Type': 'application/json; charset=UTF-8', + 'Cookie': cookieString, + 'User-Agent': 'Mozilla/5.0 (Node.js DSAS-CCA get-activity Module)', + }; + const payload = { "activityID": "3350" }; + await axios.post(url, payload, { headers, timeout: 10000 }); + console.log("Cookie test successful (API responded 2xx). Cookie is valid."); + return true; + } catch (error) { + console.warn("Cookie validity test failed."); + if (error.response) { + console.warn(`Cookie test API response status: ${error.response.status}. Cookie is likely invalid or expired.`); + } else { + console.warn(`Cookie test failed due to network or other error: ${error.message}`); + } + return false; + } +} + +// --- Core API Interaction Functions --- +async function getSessionId() { + const url = 'https://engage.nkcswx.cn/Login.aspx'; + try { + const response = await axios.get(url, { + headers: { 'User-Agent': 'Mozilla/5.0 (Node.js DSAS-CCA get-activity Module)' } + }); + const setCookieHeader = response.headers['set-cookie']; + if (setCookieHeader && setCookieHeader.length > 0) { + const sessionIdCookie = setCookieHeader.find(cookie => cookie.trim().startsWith('ASP.NET_SessionId=')); + if (sessionIdCookie) { + console.log('Debugging - ASP.NET_SessionId created'); + return sessionIdCookie.split(';')[0]; + } + } + console.error("No ASP.NET_SessionId cookie found in Set-Cookie header."); + return null; + } catch (error) { + console.error(`Error in getSessionId: ${error.response ? `${error.response.status} - ${error.response.statusText}` : error.message}`); + throw error; + } +} + +async function getMSAUTH(sessionId, userName, userPwd, templateFilePath) { + const url = 'https://engage.nkcswx.cn/Login.aspx'; + try { + let templateData = await fs.readFile(templateFilePath, 'utf8'); + const postData = templateData + .replace('{{USERNAME}}', userName) + .replace('{{PASSWORD}}', userPwd); + const headers = { + 'Content-Type': 'application/x-www-form-urlencoded', 'Cookie': sessionId, + 'User-Agent': 'Mozilla/5.0 (Node.js DSAS-CCA get-activity Module)', + 'Referer': 'https://engage.nkcswx.cn/Login.aspx' + }; + console.log('Debugging - Getting .ASPXFORMSAUTH'); + const response = await axios.post(url, postData, { + headers, maxRedirects: 0, + validateStatus: (status) => status >= 200 && status < 400 + }); + const setCookieHeader = response.headers['set-cookie']; + let formsAuthCookieValue = null; + if (setCookieHeader && setCookieHeader.length > 0) { + const aspxAuthCookies = setCookieHeader.filter(cookie => cookie.trim().startsWith('.ASPXFORMSAUTH=')); + if (aspxAuthCookies.length > 0) { + for (let i = aspxAuthCookies.length - 1; i >= 0; i--) { + const cookieCandidateParts = aspxAuthCookies[i].split(';'); + const firstPart = cookieCandidateParts[0].trim(); + if (firstPart.length > '.ASPXFORMSAUTH='.length && firstPart.substring('.ASPXFORMSAUTH='.length).length > 0) { + formsAuthCookieValue = firstPart; break; + } + } + } + } + if (formsAuthCookieValue) { + console.log('Debugging - .ASPXFORMSAUTH cookie obtained.'); + return formsAuthCookieValue; + } else { + console.error("No valid .ASPXFORMSAUTH cookie found. Headers:", setCookieHeader || "none"); + return null; + } + } catch (error) { + if (error.code === 'ENOENT') console.error(`Error: Template file '${templateFilePath}' not found.`); + else console.error(`Error in getMSAUTH: ${error.message}`); + throw error; + } +} + +async function getCompleteCookies(userName, userPwd, templateFilePath) { + console.log('Debugging - Attempting to get complete cookie string (login process).'); + const sessionId = await getSessionId(); + if (!sessionId) throw new Error("Login failed: Could not obtain ASP.NET_SessionId."); + + const msAuth = await getMSAUTH(sessionId, userName, userPwd, templateFilePath); + if (!msAuth) throw new Error("Login failed: Could not obtain .ASPXFORMSAUTH cookie."); + + return `${sessionId}; ${msAuth}`; +} + +async function getActivityDetailsRaw(activityId, cookies, maxRetries = 3, timeoutMilliseconds = 20000) { + const url = 'https://engage.nkcswx.cn/Services/ActivitiesService.asmx/GetActivityDetails'; + const headers = { + 'Content-Type': 'application/json; charset=UTF-8', 'Cookie': cookies, + 'User-Agent': 'Mozilla/5.0 (Node.js DSAS-CCA get-activity Module)', + 'X-Requested-With': 'XMLHttpRequest' + }; + const payload = { "activityID": String(activityId) }; + + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + const response = await axios.post(url, payload, { + headers, timeout: timeoutMilliseconds, responseType: 'text' + }); + const outerData = JSON.parse(response.data); + if (outerData && typeof outerData.d === 'string') { + const innerData = JSON.parse(outerData.d); + if (innerData.isError) { + console.warn(`API reported isError:true for activity ${activityId}.`); + return null; + } + return response.data; + } else { + console.error(`Unexpected API response structure for activity ${activityId}.`); + } + } catch (error) { + if (error.response && (error.response.status === 403 || error.response.status === 401)) { + console.warn(`Authentication error (${error.response.status}) while fetching activity ${activityId}. Cookie may be invalid.`); + throw new AuthenticationError(`Received ${error.response.status} for activity ${activityId}`, error.response.status); + } + console.error(`Attempt ${attempt + 1}/${maxRetries} for activity ${activityId} failed: ${error.message}`); + if (error.response) { + console.error(`Status: ${error.response.status}, Data (getActivityDetailsRaw): ${ String(error.response.data).slice(0,100)}...`); + } + if (attempt === maxRetries - 1) { + console.error(`All ${maxRetries} retries failed for activity ${activityId}.`); + throw error; + } + await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1))); + } + } + return null; +} + +/** + * Main exported function. Handles cookie caching, validation, re-authentication, and fetches activity details. + * @param {string} activityId - The ID of the activity to fetch. + * @param {string} userName - URL-encoded username. + * @param {string} userPwd - URL-encoded password. + * @param {string} [templateFileName="login_template.txt"] - Name of the login template file. + * @param {boolean} [forceLogin=false] - If true, bypasses cached cookie and forces a new login. + * @returns {Promise} The parsed JSON object of activity details, or null on failure. + */ +export async function fetchActivityData(activityId, userName, userPwd, templateFileName = "login_template.txt", forceLogin = false) { + let currentCookie = forceLogin ? null : await loadCachedCookie(); + + if (forceLogin && currentCookie) { + await clearCookieCache(); + currentCookie = null; + } + + if (currentCookie) { + const isValid = await testCookieValidity(currentCookie); + if (!isValid) { + console.log("Cached cookie test failed or cookie expired. Clearing cache."); + await clearCookieCache(); + currentCookie = null; + } else { + console.log("Using valid cached cookie."); + } + } + + if (!currentCookie) { + console.log(forceLogin ? "Forcing new login." : "No valid cached cookie found or cache bypassed. Attempting login..."); + try { + currentCookie = await getCompleteCookies(userName, userPwd, path.resolve(__dirname, templateFileName)); + await saveCookieToCache(currentCookie); + } catch (loginError) { + console.error(`Login process failed: ${loginError.message}`); + return null; + } + } + + if (!currentCookie) { + console.error("Critical: No cookie available after login attempt. Cannot fetch activity data."); + return null; + } + + try { + const rawActivityDetailsString = await getActivityDetailsRaw(activityId, currentCookie); + if (rawActivityDetailsString) { + const parsedOuter = JSON.parse(rawActivityDetailsString); + return JSON.parse(parsedOuter.d); + } + console.warn(`No data returned from getActivityDetailsRaw for activity ${activityId}, but no authentication error was thrown.`); + return null; + } catch (error) { + if (error instanceof AuthenticationError) { + console.warn(`Initial fetch failed with AuthenticationError (Status: ${error.status}). Cookie was likely invalid. Attempting re-login and one retry.`); + await clearCookieCache(); + + try { + console.log("Attempting re-login due to authentication failure..."); + currentCookie = await getCompleteCookies(userName, userPwd, path.resolve(__dirname, templateFileName)); + await saveCookieToCache(currentCookie); + + console.log("Re-login successful. Retrying request for activity details once..."); + const rawActivityDetailsStringRetry = await getActivityDetailsRaw(activityId, currentCookie); + if (rawActivityDetailsStringRetry) { + const parsedOuterRetry = JSON.parse(rawActivityDetailsStringRetry); + return JSON.parse(parsedOuterRetry.d); + } + console.warn(`Still no details for activity ${activityId} after re-login and retry.`); + return null; + } catch (retryLoginOrFetchError) { + console.error(`Error during re-login or retry fetch for activity ${activityId}: ${retryLoginOrFetchError.message}`); + return null; + } + } else { + console.error(`Failed to fetch activity data for ${activityId} due to non-authentication error: ${error.message}`); + return null; + } + } +} + +// Optionally export other functions if they are meant to be used externally +export { clearCookieCache, testCookieValidity }; \ No newline at end of file diff --git a/engage-api/login_template.txt b/engage-api/login_template.txt new file mode 100644 index 0000000..0ccecb9 --- /dev/null +++ b/engage-api/login_template.txt @@ -0,0 +1 @@ +ctl00_ctl13_TSSM=%3BTelerik.Web.UI%2C+Version%3D2021.3.1111.45%2C+Culture%3Dneutral%2C+PublicKeyToken%3D121fae78165ba3d4%3Aen-GB%3Ab406acc5-0028-4c73-8915-a9da355d848a%3A1c2121e&ctl00_ScriptManager1_HiddenField=&__LASTFOCUS=&__EVENTTARGET=&__EVENTARGUMENT=&__VIEWSTATE=pKaH5F3otHosG4r24lmDPRIspWqbbAMpsJLanFjRNXqr5e1T8Q6iZwjjXLdcLT0v3GWv7zQ9bK6ODye657W4jPwllrGhgeG%2BVUnPB85iX3B%2FcYOky1by2z5hD41Jfg9fnlN1oOtJ8ihjzzgufVv%2Bktc7RmfUEt%2BhAAQ%2FXxW2fno9OjYcxO4jSdW5psDv7wGk9JvHBiDjBECVFcPgwFCnaIphrC%2BFcJgRL1NMT2HBDd%2FwAoy4EwsUZlvriLMfD7Tj%2F5B8tsLTLPHK756PHjOzheLZ5Es2rmzMB19g0OL1xb7i42VA%2FsUY%2BevQuOJLyBm2e1Fphk4eVFCVMkX%2BF8%2B7b6jphK%2FbXFLF7oKEcJWctFn7LNjiiqOEYIv0BOronH5mNowjRAxYI97cF90OuCX%2BI2kK62Okr%2FZ7MADZWnsKYah6fuOBXGgYzRBVU5affWya2rkygKcCRFdh4LzB6j3pJsqee37bvIIvS%2FzDD6NzxSjilChamXBEnEGzC3EWfG0w3%2BDm8kQQKBPVmc4QU10KMZzYkjdzjyJNhA8QncnYg9xdWdCJ3bKu5GY%2FFhf1y8ACFA1FkwTaDxZy%2FT42BS1L5zxMWGQ24Ch0J4IgOKHkIHT0RVfLuZLE3LkLCI%2BMQ3mbiTloWJJ265lRre5wo76CExyyVHoh3ZR8L44e%2BusvVp44tisI0KdNuYlTm2BfNIWFZ7c36dZoNPb8Bp%2BcTRUneBghRoR9Jwr42AY2Z%2FfpeOwQpPl21lzOMuUEgi4MZRY2PFSZCyEeWMY%2FxqLqdC%2F0ITBULzVGdRSOqf2aXIjYEoJTfmkeYliZL21z7Pzxgm%2F%2BFX8RPcKWqobGDDlQvmiitC9%2FtmahiQxdavc3bNpNv%2BLvK3QXZuWxBnAzrwKADPrap4iNNp48ZVuD9IMTcccaPhXshXKSZkxdlDtPYwXPtKOBAsJtXmjWcy%2BPr8wvBUXlzxraEAMhmjtjklaGjZUgen25vrCgeUfOyswdpvmKuEtsY5wObSz09ftChpwyPDQVYgwbDViU8yNH%2Bf0x2C0stsRu1KZg%2FpGG6786DcyLgzQ2muju02aSoDTzv1oUPetGmeHiiT3KwJsSXCqDJuk9z9Z3FAW0DpYt1dN%2BBZWu%2BXHEkv9M2rfcCeYwfLJhI%2FNZMXZuHhGGYTxwkIg9OTud5PbDP1A0gr7KhlRcfZ47DzZZA%2BLebtxoVvSeaFdiGCZtbr5wZAxrdu3knyFRiRR5XR3S0FFSqxsTVGZEyJPu890WLNFgonQ85nQmdD8U6mtwtFxr9CHeR%2BYRwvPmrLr%2Blz7WpPpayMR9HRreg2TjBIRMeQjO6PFgNtytMzFCFJdfcsB7oM6eDVDfVyAPrFeFCHrY7TJCGB0HKvJSXhKMYhzekwtJ2J47B5XAduRUITkpRIMqRG0dv0RKoOozKPXBnEJbdUl%2BnE5W6Sxh8L9VrBgRVjHwfekfBGI4c7ZHVUW1FYZqb6YYC1YJa2LL5CvzGbhbc2biJ3u9U51ug4%2FXDNgXic%2BfUWSzALUZoJa7YOSpnKKm01DNkqCVA2JGJwT8M2C62vTevGGAJ5QQUT%2FmyJi2LdOOkIXTuXaEnul1ZEgdubhov1QYRWwimpjCtuTc7i7aW265LYCUdusbZsgqGDihuKuwA4Ifpw32R0g6aUvuwjPI9yYOhtzewpFHkX8nY0cV%2FJr%2F2tlaLVRqybZ%2B5RgqN3RTDqPgX6kxnFrxrvBB%2Fml%2FgN3mG9XIJiT9Tza4vR3ARB88A%2BXb1yrkVBTLRPXY3Zjy1ZKO6Px0WKliitlegU2h%2Bv089Hmyq%2FafCuNLwFlJSKvhekhM4gLEa4ZSo0IT1hOsDndg3ebEbNdZN0gtS%2BL9v4yTWh3qCMIqWSUh9QFJ0BjJjEeM4UwJD2f5J5CCpsYe60cjLU65ynczTl1eMbN5a%2B7wnWcIh2J9denQ0xNR%2FGK2vH368SlDoqknuIaDYAng4UIGy2UobAvqz5wIB7vN1aLfmcxvzoU%2BP1RjWNCK9P6U5yg70GROdABeAXjRQT3RUe09Y2T2QdL%2Bn0aTU%2BdWToCkT0AxFDdg9NwMe%2B1EL5ShJ0pBHsS%2FsfglhGObj3yGrRblmSetzjICK1uhC1nPS%2BN3pmURXO5gGZjD0IUYrLuaVDjek1Z4AHoUKMdZkoDUMmvGLSwSXnCWOiDgC0rMYTa8O8sqChrXKLJSmCR6ScYIiaJ5rDmhqx9wpvgSwLP7QhRYEK%2FYhj2FlkCQmws9EWGWAZkmHy%2FkTsJX%2FutgndM594gKCQSzWDICz0r7PJ5sOjmyd5n8YzeqUyZWGg960ojAI1nmldycoj9UY7GYBu%2FUd7YUgEd2Xt4u86nMKBMvnt85JDZ0DmpLvMnIsr6pxDh2hUA6BNskLhMQgKQkPWp7zy74hvWbabmezQFkJ%2BAXmRtxqiTxnlUUkkNp7cvCkVKCX%2BmNSfYq2mDE9%2BO9rUnSEu%2BLqomXh0cxAa3ZQcCy52wGcqjm1dEaOBDOI1Qam7hcAhDjrI7vke0FNKmdFePX%2FX2RcoY8ReyQLZvLriDrleNL1%2FGC6ufBXH2%2F3saDhPOLz2vcAUMJGj1rU%2BCxOXB3WMw4JlDHk8ggnd9ZBfnQcT%2BNQiHXbe%2Bea7lNGaUBtmgTkxzOHfwAAfx8tHu0tkoArMDENtXay9ljS4NMvcIIcIP5WIpeuQN5xCN2i1PM5zBGWa14KNFpcO%2BPSDewhGFsAFszW3AxIimKnAW6S2Zev4gywMC5soQF8t0aXoRYIk8Mos4p6wNKigjvMWcaxbe6xnSpYKnJ7HaAk8cPZrh%2BxNaitR0x4mC5yO2sHyFzh2axWhota7HHvoraJOeczkHnL00FF6MgaV6%2Fcn0vSq%2Brrtnb3p6eVp2D3CsexvzHuqYLP9V50NCE3u2eW1aa94WHVF9uGBqsZGaFlwvnfVz1tMdwGONWu3ks%2F7OVLNCQX52FBBCOlR5Gy%2FzYxgA07rCQZ6d%2FKYQYB%2FMS0XdMi3wSg6cy%2BXtL650dsdg68HBfz37IFLi24zJ7EBPV3SzYQ%2BcXWopgBwprd8cfyLF%2FFoXsvIWQc3n5SLcst8hqyEho1IKR9bZEyp1L6Fx3sJaRhZEaaRjCglUULJYNb1NJxh%2F3c8iNetj7bOrVGFOZZTjGuET8pgDhwrST2AN%2B9C0AnmeM82uh%2BHbnzcQ08zEVEWS2zafj2knQlYYvvgO7Ej7W1%2BilFos941WR69CTLP0jwiYMMsVfFq4wz4i7WCMGBzcwH7rqVgc4IHVOeBP4fnuY1WQcwnz2suWe2cPSyucNFIakeT3XgDphF4V3AxOuuYR%2FZMKaSoOH4WprAfpmSaeWO51CzPTD1joCLp5kC951zSTC1CO4N56ORDig1iC6uqEFiBoIS37fU%2F%2B9%2F36BT6McLvn8mDMviShDfmic95TbrAmd44VoagmaLzXv9Zs2iAnejPV4uuMbzRMFbZ7n0os6yEE%2F9Zf8QRim%2F%2F%2BMJ9I6z1H%2BxUUBxQS3sa18QHB%2F%2BMz0t4yKA3bKJqf8PtJM%2FYN6JEEh1%2FSf2Gay9iDZ%2FrVvGDkTZ%2FHJH7y2uO0LJrR2%2Bvw0dA7YVWEJW5gyo5TR7HTPO1H1iKMvVzee0KSxyMyfFtGpYaVEUELBq9iS9UpyWp%2BRmBSAHsHLiJpRmmCpmwVA%2BRtN4xEP2NdZlsp8ASRS9bfQ0ODHrPnzt49nSXHs10zdM%2FVmBQ6bO57m1sOygXkRT3Atvgh%2BKkD4IQmE9jZ9pd8gQNbT%2FZwqSjnmn4qcPfO4qxV9R5enwEMeso9fkNdDKQK9H89Pc%2Fr07Td0UtoQitsg9NJk1%2FUljvrlnv4C88dfC324QyoK6SQUMSgScbNVkMmdjttGsmDLb37IMSTF%2BYX85d1VH8KGL6ydgGRadbSx%2Fg3Dq8IY%2BalLscag%2FmbIkPq4oJ09HlATEVwl1mJU1rNNSYojU4F4Q3K4Mwrz2BFP062LclV31EUDTixQKPsI9KGHjbPA9tmKxNNvPZp5r6cFoq405DpgMKWSRJCADg%2BN4ODFkVnD8K1aER%2B8Hk4y7IMk4U1kZeXDTJQ4cpWsZskOCsRI%2BQS6ooRFJ4oXqR9Ca2850lW3IK334QKI2XeZNQ8V9QkIqaeNCnZZTOx3qasvuq2tRxbaVpCyqRcOdJnCqlJP8DikbN5TVDZlmmZMBZs%2BWVCIqLzXyxpmvZKKWdBgdjTJ5u97pmQb5nQO%2B03pccnkNBxHLc9vlpa81JgNkLCBzq1OMGbCb3dP2i0XoHkx54bi9%2B%2BjgRVcRRyTSDt9WzQ8dIZ2ZmwSbEe%2BnK1nxvH01IlfXMnTrHpvPELMl%2FjufyhXsaTwwrCNES%2F6U5FjoonGK9r0iu0VPFqsf5V00izKlCiOJ%2F9kogPdLw4f7f3KAEpe33AgpDop9J4NwGFOo4LvVl9LnjMB%2FFCTo9O%2BH5UQz4zSL%2FMhg6Jwvx4YqiVgpF2%2FrouiYDUyIdmU90807d4GrHOFblcXciHv0xUGe7OIeHM9WjWzNCIBoIs2Ccroen%2FEAshdc4ovQ%2FYeaKtvVCxwHHiMz6%2BCqjg2UOxbbAbNJwhCEvnvxrqOnWO8MC%2BP6GJESISjtpon3GovBtpeuQYPcP%2B1G8EoB35m2vKtLFKyBtHAUY8lkvBYL37%2BzYxuX3w7lqkyAHG2mJie2wprlZkY78z1PQ%2BrHI%2FzpCB1N%2F2swf0v5SLpbGO7TfdW3pRODWoTXI5pWkxKMpBTJ6vU0DXpvy9DgIAcq2kP1CyJsMcuf9YRlIA1NchJYnp81YZN3q%2F%2ByBApI0Z6vX%2FoNn5Z6IqeI9w5vHpHx7BwjqORSxjo8XAa8VAI4jYJhrisnTgwzy3b%2Box1FzOqPVcHlxv59WoKsF6iLvMUVSZCGLOKi95e%2BRQWJgiH2ofLbj50z%2BBxUC7SRj1yPI0CPZuui%2BB2s5rp%2FdU3HM6493zCcgArDGH4luC08K6RgaXk0JkDwePvLzMoM04ct9JOy5x9X91mhJQAbmoZnAeOPKIRsFInMjmUY%2FiOXFJPQMVYDCIjRtQCav4kj3X5%2FnZB4I7xgZVfT8JSREyeY6aDH2XEiDJ9wA9rhW0KWJVVgjKDtvhN3GSX2jCG%2BXQXWbKYmagCROOVJanPCRLSvbNFPLyroPiKxgQSujPDqd6RvpojfolHtHxPkyfluc8bjUXJteJfRj2iWnjphO065dU6tVwCkgU%2FEsnA7cQ140rkMhmX9eaqUYdsq%2FCgghr1OrVaL3x2d4iIrnnizycF2NX%2BHwITQrL%2BOj7NKBys9BVjDBxTSlXUk3hAwYN8JWLDqifC4q1KOwmlRORGrv%2Fzz9LNeyE9e5VY8Ag23DAwTCC2ARIB66o5gJZZ0oZQ7uAcurrMRasswtaRPim8yHbdNa0B98Q%2FlUpc0XUcaDtIZbH8vSy8AaLm%2FA5CLyQb8J0qr55dLJ4iFnfBBTc7ZWBfEbv5VncU%2BzBPG%2BWudu0UCt%2FfcdI%2FM3kN3xjvE0viL90n8Di%2FQUSXEjEr59GQZ2RqWuxPU7UAfQOjf4%2FZ1urDZGyJGjtR%2BWgBrygO8PzzDSxYwZJRrCMYHTAYLripkAn440Mpk7MvPKjTcDV8epDmyrAzHRAkPf71w13tWls%2BZo0AwWjYo4ZtjvXA7IFq4lGPPefQQ6vT%2BTWk3nWCXAhV0gvpIVdGD4nA3dNRKylBtZsQX7GXagO%2FA9wLEv5k2SrI7oC6CVjoCax70ElSqsjjHecumX1PV2yTZBxS047OdLwW04K1eaNDMCUZm17jkKQwellcueQoc1Zmu%2B79c83Wqv%2F9lcTqnr4BdmHj2gLp6qCaoGXB2oqa9wxx6x9BKCX682umHWYc5OMtFdio49QUzfrfjpZzvw6OrcI3uA5GvzQU37BEx6Grxi1YAI2phEYFXmbCBRVxSuktT1z%2BVIqG%2FaxQymhIcZjm6Kwzp4uDDARqBVLDqfKHvzheM0SPmx865MUR2YK8ErEBgmfTiD5Bh8WKNr%2B14kySESWdaKRmg5qSNcoijssVqn7NKgSJSxiORoONCruH7naTr3GCUUYlkXn74TC48GIJBBeCbvLS8VGLkDbyQC%2Bvf5YNS3w7lV8neVcU3%2FwYzg%2BgVwQDTYFStYePxddOYGW4T0tpqzIehky%2FX1UX6jP%2BjbILeDejjbV4%2FEackltSHSA8ueZy0IRjB0g7ljARiFJ5sAxZvfemENO2yMbVOr872R3%2B2kKfpg5UWs8DUSQQPDwjttCkUbYT6Afn3YYqZwce84jHN1W5ZIUcxPWI20X65MTsA7WTXd2mQmKUKcoWAJE6w71tFycOiPl01j8PYjiErIrWQiUcN2a8uOnjSZAz3J8IWLb2Wgv9k%2FSMFn2k9ohXXZiGw4b59bsqDuon5qCDYkrH1Dw9KfTWhRjtTgWe3xLbPIkpU%2B0Hwd97p%2FhPGYTCC6NQhcbOVu9Rj61Eo64HFjvK7WI6WXkBegJp1%2FhfOhLkIQ8%2BgYNP8p0I%2BFTXuk5p3F2LdY56bBXVVe5CGFDVFSSlqrO0KoMasdtCQ%2BTcIS%2BZ1yfS5wl9duw6seog4e1mzpnpQ8iAlIcNbA8fgQNKfbEf2vk3S%2FCP8WOuI6B4mIWz%2BdLhuAtunQ2IH2dm6CJM9WrCw6OpF7vBMo%2BGqc6dZY9zTN8QnyxlxiPnN1b51mTdjppusAv1gc0O7r%2Bjq1bv728xN%2B6yNrJnOjxEHuPP5C7VzkYoMZbMGWvDvS69T05Zkk0i5jEJjO0exVUYmpaTCybqkaboYkBD18czz1Y4NZuMsIH0pPnCafimkwrzk1LZ%2BgaD5Es87gDjs%2BFgxONGhuUjaltLaKYyshmQb%2BqzgOlnMCzeAHatY%2FTpYaynT0CAvsjnxrca36ojEZHrf3MAD%2Bx0ZIwICKecYWSjEU11DXUS5340%2FxfmrU2qUrvbKHG4AnH5CQjDltofnPKfGL5K%2Fzank8Dkhlik7XSuTdNtpa0tUM%2FaZEr4Bz20WjaoMXIxy%2Bgk9zjHVqZoXOSwH2AOJWv4xVJSZFbG0niTUeNLCRnuSQwoEidUNrWUmU8ZS7BD07WpRzEI43OFb7bv2j3Be7kdAv9Be2V3cT9SOvkHMxixp9eNTHivjnzj87B42tZVjXCYs8GORNHkgXCkDsi%2F%2BADJzTXv1qjUUa5EjBOpavHoxicLttoD2Jjxa0%2BIhsG5qKOMEHQvven3OKQC0wlW1dNY9KQ%2F5B4h7Zhy8tF%2BcwUhddunNl4iG%2BNp%2Fsv0mJy7y6LP1vifgdVxBXIwzjaBRRXLCY7TqGrwtIZidu8dNqTRKHg7%2FwcVJW2TxwdGjXi2Le7tzCd6HaLkHeqR2Prx%2Fh59IaG9eQ9q8vp3raEK7mFMeg2lnkuG5PV1nfghiOpM9H8C2Y6QOGEU%2FpF7gz4PoR8Fm699SJjTvJv93UFbB03iyL2zuSJEVYg7ukY0yPueVIqXpj0DH66c3gEGMFoZKXVQmmE9BPaXVSL1Yb6ZaMjm9vIVSVMf4qiiYDlGEipcIx1WTC8nQvYfxDDe5V%2FgQMCh5Tjf0YTfqhGTHegTmCNyeapWQbIYSKC51yUi11txCRbh1PmiqnGR%2BrSUE4Rr%2BuOUhtQ5hkxgwBiKTGSYFF0iwjZtWtpV3855G5Stdonk00h81KkaCsFgk6x7eglcOpFebO8zpSz8CINJgsfBFzRxL2dDEORfJEXQu7vB97gtQTAxiSqmGLpu74r%2F9%2FQdXMxlwvcK2JFdyhhBj1O86xXnwDi0PpX4NDezkB0oLmTzQHMb8d7BnPpEmijcaSbAmpsutM6Uqdz3QV%2FOu2HG94xAgW0M%2BOC6VA0TVnlGRaGYfVKR%2BGIO07HWOqjjclkIPaTyYnhKSdS0zJJEZDKyiVMvoGoar1WjfZMRpdQA%2FafuK4rm5GmZtyUUXa4kDDDE3tMMG%2FcbrCej%2Fm4zduSkvwUpmNDhB3veXg6xYRMfd0zfQHy8mCm7kc54sckM4xLeebjUMiWmQ7YSEPLcC5SG7%2FNIiy7GDzmNLCI3oi8iDXwQEnGj%2FTVMWu2bXfWVUNLazp261ftKVe8xL8ZZcYeJlQ7IYGNGsUAdR82%2BgqkRWW590RZ%2FZrrT5JOq%2B0N7sONfKeTur3XK2VTQ4hpmHx%2FYGj%2F4B8SYOz9HaMfmq4Qngm5o7AK2cNTF8Gn25li7EeCtzyzqE32DcYcU8Dq4Ne8gr3HoP%2B2bulk92rFblkxBcoMyZYAUXRdHAC4mEuhAlNwZ3UH7%2F6aCrQNHZVTRNx059%2B2SXPiqgGkgTCE%2BOGPAGiQEjPTL0EaD7ojA4NYUjb8YKYOPnM57FCyB2dj55sNMRU9Gy5f7%2Fucj6%2B5KF1RYTgoyr28q3ovJU6ePLXkgrG1IgFMz%2FEdV%2FKkmElZbvN5DV5UWw2qlHnQNJePUooTFhArmsuHhyITSvLAHdq6fcTKW1p6aH3znc1Ml6JbxV6hVXNrUoFaQ3sP6RbzhOptKmi7VWFEEmFQvcJ9C8%2BETu6YU4PqQy6kO0vKti2qZYr7hXG2d88ZxdMG%2FE3VHI45h5hao7LxmDTU2ZxrvHsGj%2Fgitf%2ByaJD8pGZCKJ4WBriyfpgRo0hos2gkxgE5%2BJRjt1Lx5TZNAYdvH1zodhf3B9R2ddjnvSVc5Zc%2FQkZ8Ixtj81nAevx%2BRGbF9QBOD1%2BGmngNRpy3O1%2FZt0OPZ2rPwJfutApakOKVmxeRCW5kTa4Yo2F6fh3DaRW%2BFBL9VbJa%2Fnj8tG0RbuPBAcK7o%2BawLrW6JxpkyN%2BMGwoiXkrBn%2FE8NfvS6O0xsmxtbOWABlbpTY3uDjk%2BEtUndGq2iNA95mTcM5WMKmhEk%2FcTyKVlK1dsi5oUVwqCAsoM2qasUigbU3Cycar85mt6iHkUP7VstHl5MwXLp1u4W5HiNl4BVhrual9cHhoAp4DbwIQPpaqa3VcKiLVUbyYSVhgvFTrqYzQeRMz9IJiGDB9Oj8u%2FQFtW7zWGAbilPadp2el3S8wj58Q5v1qHulBWkb0NZJV7whx0YYrEmQw4xxB8wvSgbZmkNMHRFpc6rZjckj30SnBAlfzxTJZ18HtEvrfAr0rpv%2FitROeL6Z5Cdmt2ZaeoyJ%2FQmhhGE77GHwr6oHUh%2FA0R6CseoGAzMHzdROjaRKZGcitkHY07fV4lUP4sRx0o%2FXxFOqSd9Xw8NyXTZmjMGbHCxk617J9PwDQslaaJQW5bYnsyBkkphXwuSVVHesh36eu5JBgdLaCj32qu%2BiZmlR5IbTu5HkVHmF6rvBb60ihNIfres%2FNj%2BFZFuqP0oHSV%2FhKHu5Y402PcPUOy4fYqS5iG4NdLmnY5u45%2B3%2Fi1NQEU41FHQkYXu0dVRmSHplMrlVJTaK7ow%2BMZbVO8hGNzLdkstvdeC35vJGPD3BtrXCuHeo0HwcaUoQ0ZGYF8FtCMVRfTDfupjgTBtIN%2B%2BBEdt%2FvHh1lR8qgRLeF%2FuzPnxlMxwIR3TswN5r2DESB6sNth%2B5O1aqfN8gOid3XRg6RibsgMoFicNQ3s6EEyOB7v45RipYBSQQqXQYtY1Hdgt5gBlKw7Vac6UQ2XQYmRiYO7giqSutcH2hjq%2FHl8NZDP32y9i6VY4mZmCC2aLLZ54qRy9bdYpNTUImhBBpF32%2B3C3Rfwa7Ma4NboUppq%2FElVfp50ZNYSkbVEstjAWc2ZdDVgn%2BTPbVp4tLg0VGwMfMCI4gb%2B5V%2FLl%2BriV1k%2BN8%2FL%2BK1bfyBHbAaP8lGJVr8s%2BWOWm2hY5f0skELL1kTBTw9om8KYygI3mQLWaXLvgqlAyFRBkIJhbw5BJRU1q2%2FvI6C5lKScxjrB4PbXFxp6jXixCNa60DrOWQr56DLOyuplex9ej9T5Tj3OAZ30Si2K17hJTi9l4KwT4hsGU%2B1fiYtdhkFCrafeUC5cPhkg2RDKNxqbBWPpPW9Fux1JcmSBAEPyiCg%2BWylQgfyZSn5MXapT5GoQ8XHfBOfIpKBN3dT43vJoRlkmiZnwiHilSA3u9lxRFqkn%2FURjK8ho1sR8UYgz3CrOce6%2BZ5KI71jbk%2BNTsFt%2BOBgr4SvNrJVQsEgxwAWbAxleU%2FG%2Biv68Y5bZC9TuAwcuwuzAQWR%2B9poapwvO0N14oa9%2FqY0SHszbED808vXFxEHuDLBu92SZvOjECOpCiTkuL6LAGXEepRsGpFKo1pp%2FI6edP4AWMB7%2F3PlORS2KCctNBHyWw19UI0qri9%2BP80ych400Fub990ZQFN4sGrigG%2FeiPLEkYT%2BHW4vURE554a3rvyA67LNPmijCJYWWmR7hFvwk%2FZ4rkmIFiTetvefuq1G%2FyPhT8u5eIvNcRsdTgi%2B0a1bhmg7ZT7ngMGEtf7RzhEUVlqhMRzxxe%2FQp1HtL5ItdMJeRYXWYxPt4WgYrb%2B34ms0LcJ%2FET3xc4eNcyeHsSqXdek%2B9D6gRFfyRrMUktXqwkAuQ1lwVIIAbKMzeXtl73rtMCXeyxrWe11I1q7KteO%2BdbpmbSbe%2FQCszlPQKNdR%2BTgf3vsOiJPqzGYMXsCU49rsAtt185JxAgMZHytmLsFpkpiQl4WkutYnMFWNySCzvrznarNoLWTbbirmvrq26Q%2BtSZJ9YE6B4s9TebBJj%2B4dn%2F8jcjbyZVlIzyUs9unmYMVDuSZDxXIUTcARjnTRL0Dnwx4F%2BAuXr1CTRtk04py3d9PeBW7deyK2hJUsZTKP7K5GSntnSkCofOftRYJIVkmMpe2fsDQm5njJbkJ%2FzTXMMW4Df4%2FGDZTUwYXS5mpBgV7tXgK78kvU0Pk7T1UTHBDhrgWEqGrJ9ye66UYUbgT68Eo7WM3bDaf9BlGk6kBA%2FWbjwwVQ5R%2FjU955KadJCLV03IHRvHcGc5oSp%2BvV0Obxfs8DCIqkfgx%2BDXknymatWD1bpI1ZChk66QQRzeZvpP67fSD5KWyQmSho0OZdUA%2FVnyOw3JaYWPYXEGXlUEaMEUjhHbawzriNsDTU1MI2GkRmnsugJeIqciKscf0RAUV%2Fwkp1wBOqHLWiJHAKMIng6rT7tvFvF8B2t%2FP6Mxd8zjlx7mpP8s5abh1ElZKU%2Fi0nBfNK9VtBu88dbfGCRGwwY6%2FZhCKEjAy%2BQmmyauWYiOpYzW9fWrgWEAgmDEoAeiR1h7pQo6QQk%2BtlQFOhZeUj7neJ%2Bl8IGOmjBPLgIofs8jD772D7lkbCMUI1SuOZJpYVS77F%2BPY4J4375I%2FpUkDoe5QOWYWgrwyQXqhalOCCLqt4vlIaCSDgBcC9Nl2T3akJKAcS2%2FCiKqn7H9iMkFk5QC02%2FeX91uk%2BkZT1Ldo%2BGwaBgObaLlhsw56j%2FQhI3CX%2F%2F7iSv1lbEb%2Bj48XTYho1pLqk9Z3mLeaFjSweqKHxg5KB1AVvBN6HHwjL7WCNnLtTuOcx0v8drBU5pczIRztiRc4yJqm3C4civFTos5sxtReYG0qZMUa5DBf4z95eTGMx0SbtqoY6eZ2xubw2C9X3pV5ZKqlVJmrX5RyUCxYZ%2FGXgwA3ccF38Egzk4oTCr8wDO9giQnceIRFlg9JsFfG3fQdFXi7p4AkjChB679JCdDTJQCUrGtKOwveinx42HOId4PJFPH5Te65qRnbpQW62%2B0m%2F%2FXU6Dp%2BurAYLWckQgyUuLJLzHD7ND1JROZZoo%2BX4ViOGPA3CfbKuOEa%2B0kmdIpdYOI7t2uJhG136y%2FjjB%2Foh8nmfz5KogBkt%2Bmhg%2FQtbH4iurFzqctVjtIZrLup5aME9s15NSWnwZnTluj0YObRpX2kJXmbN4OB68QwfRoqvjsJMJNGj91MuvKOZp6GkJblpIMflpgbI9R8ElQnpyfQeSeU%2BbTi5rIGZB0EvKID5rS%2BOEVLYhhPSCOtHAha%2Bk%2BBdVDpZNpG%2BFHeiW1tMQAnNlDyANoPMNuEFRPKaf2TbAA6SATNo7RWH%2FZqrpYGF0IkwT7UEd5fmZ6paIw%2BSGlbNT0HvV22gQUlmWa4sAglOhReDyJAzL4tycF6s5VP91GR%2FzBdCuxFKdGpYQDEP%2F%2FIFQQjGO8qx5erCGO%2BdW1WVe03OMsRUA0oLkgE2s7V3sRC7vIkdLmNTCpcOzU2DtKEKDqeaNFIlKhaxmiV00dNFJWB8tAHdHLDTJSIacEj%2FHKrk0QOQZKVp1NlvFC4FThtYElcNlajzywp%2Fe2mf%2FGKKyloS8nL8bNsFe4quqcAjENCb0Ho5Dj83nMte%2B32JoqzWUs7Hj8%2FbYsXBlbMaXmto4XDcihSPB9AvolBrSg14p1pmZlEDHyuMB1c4HoHJkSnK5DlSL55UELCmg98tmY2m1hmOXt%2FinmM%2FWqfgDg%2ByiJTIi4Y7km7l%2FMY%2FhCEnmvnlPTekjIf1qC0ZfLF%2B1dFUBOvqMz9GI1KiamUHOLJxqDF8oh5VSN9rPiRo5WAVyCBb%2BF%2BQPu5QY%2BCc97JcUmE%2Bf%2BNJQiJWP%2FGNqqPSTcBAL%2F8LP6G7nuQzB5E01oB%2BUY7WdeTPQQmy%2B5AWAG8g5HkeBUK8vvbTf3Z%2FnYtG2ob3DLYuFvaECsz1L57rRU2tYfU9uUMRjNC1DPEipZOKoRRPzlkaApY7HcypJY58qgYcx%2BkRkdsBTKp6dmuMSlYXGgMjA76oC1Ut3t92h8SF1gFVuntDK%2F3FeFT8KINLSyZff96hV2lI6xlGp%2F7dpXWifK2zWqNR3SumKHrUy8JIbUKxft1DACI%2Bw48NimL6chYiTIpTZIn3iDt73VuXeasRbOA8NIAE%2BC3X%2FG63%2F511LDRHVR2eJa2VDUkVYrwVnxKmpLpfCTlgfcrAgLmIp0DAqA%2FqDyXi9pd2R5f1%2F6kXAUTTzB1WJVKsgPIAwVp93m7Pc0kS6wrltIwTbAhjLOmsaLsPIjI%2FfrdIft0nnE5pF0dMPZQijI6cVJ6GoZ9G9ZaIX0eJ1Rc7MV2a32cc7yRcWg7Gh59r8OrtZnz%2Fqc3gW2lWi51xfpoVzX%2BMYnAy3%2F7QeB01m0Eif8PotzcImj4NgVZnB508uYFtbER6i0HX2ndNEnqE%2BO%2B5JoNhJ9mBEgukxHC4lOcDPkzyn02BQbr6e8z4PYZWyiNayxyBVjUE1XnsKVkdyd%2FMKuoIaYqhucLYwpiXM9lQBgoYuNtGhmGdbaYFB05rmi74cYLtebwrcvyX%2Brj12g%2FjlwsC4PCbPwh3D1BpzMjHMsQhSKnctyLx42M9L0B9ciIoShXDC%2BXHx31A6WftqMq5rsvKJ7%2F0N8ubarduhekxA93s8gTc%2BLDnG9HZ4mLZODMApYWJJTGM1IL%2FBJ%2F1d6ELiOdRhiy%2BQ1VqU44dnziswuw%2FhK2IzIiAqtFxm%2BtmGo33MHBzxu06YvqBKOZndFp6CYoco3Soqrl8nxOJq9Cb%2BurzAF7ci3X97DO10T4P5xuqZZpyWP3mScDjxgRLMo2vmrNhORTkUfdIv%2B4wtSWtdQyQ%2FArhq01sZ4KDbUz1KQhjfeZNpyQf%2BLY0e7gozN1JmhgL9wC0QsCsRKteJa45qqKSmvDoyuZwFpnPFxo%2F1VP4Nz%2BLl9eGiilJnTLAOLBP5Q1va8pFby76cij1IwUKakQ%2B6nr7yhXBETobWpNCP3flezVr8z2w3mZduY2GBzfWItsGqnhv%2FFpskZsq%2BLqOuTltIAWzWPn%2FL%2FgAZPoqX%2Bn60yj6OeRDnesf6NB%2FPThsYNpKAYEP7wIkpljikBmc7ugFCIFpV9epN%2FTXM5ccFhvWvPR64H%2BRJd%2BWj0wrU4z7xOOznSthciu3%2F6TBK9BB71MEaAuhiWK3DH%2FNwNGbShA4P1CWn9xf1ArGl8PXHTk8iQQGM8qR8Bi1B42YkfaCaHiOq4CShcMe%2BMn9mLySXZiwDCqAYNUclZQKt%2BdYZ3ViVN6ivIkMNVgDFXc7O1lbXlI0UIViMWt1Avlb8%2B3lNbG1jWqjF2WiTom3dX4%2Be%2BNYqsDpaC%2FpDg2pwHu89vyIUsQa7Gx3NQLWLNzfdZEwURjk6kie%2F3GGtiVGUIKx82pY2ZExOz7oEzeF3jnAyLD62HAdv8fAXRNPEOP8YKm8ye%2FO6aZG9UpyOFQIVWCbpYAQrm7iU8db2NXm61FEWvY83JpahPSJr58gbqCp9sCr%2F7YdVe%2FjbRONtFu1PD07w8WUZSEpmm6KkIe2RrfN1gZxJKKGiCjnQAdBe3oIASxn30%2FpPBKlHbS5t1jTZiGuDHdtnPKelIa6CtNwBhA7vPcFcVLLBBbCR%2FkOz6j%2Bnr5uAwac0KxaM4a7uhoi1ayhwUjlqhchGtV81P1a57S9ma8twPS%2BoqmTZruRRiapZOQLbv41FXv89gVcmqH0AZcFipPahK35nRyeBvSCyrYt39N%2Fb3jjPdkoZOwxkJc7VAQPVjHl7RpHrOflW1CVzyMqdWBExmHgafPsCF3UKjZeNaCZj0TzIrJI48%2BvB1ZCOHtXaECU6mVuAUuqWgmGsPskg1vkIcWRBvEyslN73uY5xwS38QCV7xnEbdlPJDlXkc8BZLgvnLLlFTp2GNecMn51RnnsFS4KB1aDLmDGk%2BZVV4ZFbLoo86gDwqyFvJ65iK443FPIscmykE2Ye6kkUFrJC6Jse69YqWzcOhFei1CGVLdDZTlGMszcYaQ5DmrPJiAP2Ojm61AnCWKLXCkMEvPRNRX1rQHGjNjtoUjDapbtmNXOhqDrNsVGksAWmT22nZqBLorpwMtINZhD5xJwEm2XMmcTakhLPhuIaO04nq%2F9Yoa1w6j02BKr8zsAhFMRXox0uq%2B5GLjSXjXztcIiQdxBWCl7UajIUbyoeEQFWsc%2FmMtj4BW8T%2F9NM0LqV9dFhXIBBwo1bZwnC8rdAHT2qD5HHcyNpNcmOfrW%2FDOy4je96tbhE2TjzHN9gcnWsbsIQvX%2Fi2rzR%2FLkFrGZPOkT1gKbSnx7%2F3VoiVtngGqsVE2fzFwpCmTvXlKsmGs6xThzMUaYBY0a3PipCaPEWLynmo2IeGHih%2B5rI95YLNwAk8SvDSd4zxygwaegenuGaYv6xebLF9BHRRuxTdlT9Ps%2B7ELuxGhNVIVa%2BtipUi9N40F8p1K3XzHDKD%2FcdKMhgSFNbK%2BGeclT9KIoq4LPxbmzT%2BGKRJ4Yxl%2FRVgxTi9Gl%2BACg%3D%3D&__VIEWSTATEGENERATOR=C2EE9ABB&__VIEWSTATEENCRYPTED=&__EVENTVALIDATION=P0ldEyCXyRIx4S1aexEbtjI40PGbrukNkS1tPWHWZHDs1BueEG%2B6vUxTALDU6zKoH5MfPzTbOi%2FxfNyOHNm88MDf7FMJbDc%2BqTIbb9xGfLjXAH4tjJjIOAxxKc7yzAnLIBlAJlrdlLVHLB2FDWcoUfE2Ll%2FmoRTEKdPfDFbcbgC2sAbjkGGDz9cPqQeOyZmlVCDVrHN6DocxsbFkljofVLxf2ojXRlFu%2F7tj1rB20c0Tfal2Oopjp7Fd%2FOBGinTqCIXyaVKCn4FcerfQdDLoKCVOKytg0NDVc%2BmH%2B8ENdQdDf2AdeO3Dp03ljNzXBENEBCrXtnBrw%2B92RzPYoOQqI8h7LS9xo3ojce7T1tKkCHFZm8GiZhvOkYTPi11shdDkkX4%2FwXh977vMyWhhW%2F7QwQ9bumdZ5D1MZ4slOaIZnxNHPWBCVjtSXfKLCzRdPfhGCDbQusGQoC4UII9%2F33A1eQsOXDFExBZM6OHycqywRBJr8FjFtt%2FkEnI1W6QLrGepz%2FYtbS%2F48l1xe1VQbuJP%2B3AI0YU%3D&ctl00%24hdnUnsavedDataWarningEnabled=false&ctl00%24hdnHorizontalScrollPosition=&ctl00%24hdnVerticalScrollPosition=&ctl00%24hdnStaffRegisterInFlag=in&ctl00%24PageContent%24loginControl%24hdnMaxLoginAttempts=0&ctl00%24PageContent%24loginControl%24hdnToken=&ctl00%24PageContent%24loginControl%24hdnLinkAccount=0&ctl00%24PageContent%24loginControl%24hdnIsPWALogin=false&ctl00%24PageContent%24loginControl%24hdnIsPupilPortal=0&ctl00%24PageContent%24loginControl%24languageSelect%24ddlLanguage=UK+English&ctl00_PageContent_loginControl_languageSelect_ddlLanguage_ClientState=&ctl00%24PageContent%24loginControl%24txtUN={{USERNAME}}&ctl00%24PageContent%24loginControl%24txtPwd={{PASSWORD}}&ctl00%24PageContent%24loginControl%24txtMFA=&ctl00%24PageContent%24loginControl%24btnLogin=Login&ctl00%24ddlReason=Select&ctl00_ddlReason_ClientState=&ctl00%24txtNotes= \ No newline at end of file diff --git a/engage-api/struct-activity.mjs b/engage-api/struct-activity.mjs new file mode 100644 index 0000000..7590f10 --- /dev/null +++ b/engage-api/struct-activity.mjs @@ -0,0 +1,151 @@ +// struct-activity.mjs + +let clubSchema = { + academicYear: null, + category: null, + description: null, + duration: { + endDate: null, + isRecurringWeekly: null, + startDate: null + }, + grades: { + max: null, + min: null + }, + id: null, + isPreSignup: null, + isStudentLed: null, + materials: [], + meeting: { + day: null, + endTime: null, + location: { + block: null, + room: null, + site: null + }, + startTime: null + }, + name: null, + photo: null, + poorWeatherPlan: null, + requirements: [], + schedule: null, + semesterCost: null, + staff: [], + staffForReports: [], + studentLeaders: [] +} + +async function applyFields(field, structuredActivityData) { + switch (true) { + case field.fID == "academicyear": + structuredActivityData.academicYear = field.fData; + break; + case field.fID == "schedule": + structuredActivityData.schedule = field.fData; + break; + case field.fID == "category": + structuredActivityData.category = field.fData; + break; + case field.fID == "activityname": + structuredActivityData.name = field.fData; + break; + case field.fID == "day": + structuredActivityData.meeting.day = field.fData; + break; + case field.fID == "start": + structuredActivityData.meeting.startTime = field.fData; + break; + case field.fID == "end": + structuredActivityData.meeting.endTime = field.fData; + break; + case field.fID == "site": + structuredActivityData.meeting.location.site = field.fData; + break; + case field.fID == "block": + structuredActivityData.meeting.location.block = field.fData; + break; + case field.fID == "room": + structuredActivityData.meeting.location.room = field.fData; + break; + case field.fID == "staff": + let staff = field.fData.split(", "); + structuredActivityData.staff = staff; + break; + case field.fID == "runsfrom": + structuredActivityData.duration.startDate = field.fData; + break; + case field.fID == "runsto": + structuredActivityData.duration.endDate = field.fData; + break; + case field.fData == "Recurring Weekly": + structuredActivityData.duration.isRecurringWeekly = true; + break; + default: + //console.log(`No matching case for field: fID=${field.fID}, fType=${field.fType}`); + break; + } +} + +async function postProcess(structuredActivityData) { + structuredActivityData.description = structuredActivityData.description.replaceAll("
","\n"); + if (structuredActivityData.name.search("Student-led") != -1) { + structuredActivityData.isStudentLed = true; + } else { + structuredActivityData.isStudentLed = false; + } + const grades = structuredActivityData.schedule.match(/G(\d+)-(\d+)/); + structuredActivityData.grades.min = grades[1]; + structuredActivityData.grades.max = grades[2]; +} + +export async function structActivityData(rawActivityData) { + let structuredActivityData = JSON.parse(JSON.stringify(clubSchema)); + let rows = rawActivityData.newRows; + // Load club id - "rID": "3350:1:0:0" + structuredActivityData.id = rows[0].rID.split(":")[0]; + for (const rowObject of rows) { + for (let i = 0; i < rowObject.fields.length; i++) { + const field = rowObject.fields[i]; + // Optimize: no fData, just skip + if (field.fData == null && field.fData == "") { continue; } + // Process hard cases first + if (field.fData == "Description") { + structuredActivityData.description = rowObject.fields[i + 1].fData; + continue; + } else if (field.fData == "Name To Appear On Reports"){ + let staffForReports = rowObject.fields[i + 1].fData.split(", "); + structuredActivityData.staffForReports = staffForReports; + } else if (field.fData == "Upload Photo") { + structuredActivityData.photo = rowObject.fields[i + 1].fData; + } else if (field.fData == "Poor Weather Plan") { + structuredActivityData.poorWeatherPlan = rowObject.fields[i + 1].fData; + } else if (field.fData == "Activity Runs From") { + if (rowObject.fields[i + 4].fData == "Recurring Weekly") { + structuredActivityData.duration.isRecurringWeekly = true; + } else { + structuredActivityData.duration.isRecurringWeekly = false; + } + } else if (field.fData == "Is Pre Sign-up") { + if (rowObject.fields[i + 1].fData == "") { + structuredActivityData.isPreSignup = false; + } else { + structuredActivityData.isPreSignup = true; + } + } else if (field.fData == "Semester Cost") { + if (rowObject.fields[i + 1].fData == "") { + structuredActivityData.semesterCost = null; + } else { + structuredActivityData.semesterCost = rowObject.fields[i + 1].fData + } + } else { + // Pass any other easy cases to helper function + applyFields(field, structuredActivityData); + } + } + } + postProcess(structuredActivityData); + return structuredActivityData +} diff --git a/engage-api/struct-staff.mjs b/engage-api/struct-staff.mjs new file mode 100644 index 0000000..422c494 --- /dev/null +++ b/engage-api/struct-staff.mjs @@ -0,0 +1,21 @@ +// struct-staff.mjs + +let staffs = new Map(); + +async function updateStaffMap(staffs,lParms) { + for (const staff of lParms) { + staffs.set(staff.key, staff.val) + } +} + +export async function structStaffData(rawActivityData) { + let rows = rawActivityData.newRows; + for (const rowObject of rows) { + for (const field of rowObject.fields) { + if (field.fID == "staff") { + await updateStaffMap(staffs, field.lParms); + return staffs; + } + } + } +} \ No newline at end of file diff --git a/example.env b/example.env new file mode 100644 index 0000000..2d77d80 --- /dev/null +++ b/example.env @@ -0,0 +1,4 @@ +API_USERNAME= +API_PASSWORD= +PORT=3000 +FIXED_STAFF_ACTIVITY_ID=7095 \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 0000000..e5457ab --- /dev/null +++ b/main.js @@ -0,0 +1,105 @@ +// main.js +import express from 'express'; +import dotenv from 'dotenv'; +import { fetchActivityData } from './engage-api/get-activity.mjs'; +import { structActivityData } from './engage-api/struct-activity.mjs'; +import { structStaffData } from './engage-api/struct-staff.mjs'; + +// Load environment variables from .env file +dotenv.config(); + +// --- Configuration --- +const USERNAME = process.env.API_USERNAME; +const PASSWORD = process.env.API_PASSWORD; +const PORT = process.env.PORT || 3000; // Default to port 3000 if not specified +const FIXED_STAFF_ACTIVITY_ID = process.env.FIXED_STAFF_ACTIVITY_ID || '3350'; + +// --- Initialize Express App --- +const app = express(); + +// Middleware to parse JSON request bodies (useful if you add POST/PUT later) +app.use(express.json()); + +// --- API Endpoints --- + +// GET Endpoint: Welcome message +app.get('/', (req, res) => { + res.send('Welcome to the DSAS CCA API!

\ + API Endpoints:
\ + GET /v1/activity/:activityId (ID must be 1-4 digits)
\ + GET /v1/staffs'); +}); + +// GET Endpoint: Fetch structured activity data by ID +app.get('/v1/activity/:activityId', async (req, res) => { + let { activityId } = req.params; // Extract activityId from URL parameter + + // Validate activityId: should be 1 to 4 digits + if (!/^\d{1,4}$/.test(activityId)) { + return res.status(400).json({ + error: 'Invalid Activity ID format. Activity ID must be 1 to 4 digits (e.g., 1, 0001, 9999).' + }); + } + + if (!USERNAME || !PASSWORD) { + console.error('API username or password not configured. Check .env file or environment variables.'); + return res.status(500).json({ error: 'Server configuration error.' }); + } + + console.log(`Workspaceing activity details for activity ID: ${activityId}`); + try { + const activityJson = await fetchActivityData(activityId, USERNAME, PASSWORD); + if (activityJson) { + const structuredActivity = await structActivityData(activityJson); + res.json(structuredActivity); // Assuming structActivityData returns a JSON-friendly object + } else { + res.status(404).json({ error: `Could not retrieve details for activity ${activityId}.` }); + } + } catch (error) { + console.error(`Error fetching activity ${activityId}:`, error); + res.status(500).json({ error: 'An internal server error occurred while fetching activity data.' }); + } +}); + +// GET Endpoint: Fetch fixed structured staff data +app.get('/v1/staffs', async (req, res) => { + if (!USERNAME || !PASSWORD) { + console.error('API username or password not configured. Check .env file or environment variables.'); + return res.status(500).json({ error: 'Server configuration error.' }); + } + + console.log(`Workspaceing staff details (using fixed activity ID: ${FIXED_STAFF_ACTIVITY_ID})`); + try { + const activityJson = await fetchActivityData(FIXED_STAFF_ACTIVITY_ID, USERNAME, PASSWORD); + if (activityJson) { + const staffMap = await structStaffData(activityJson); // This returns a Map + + // Convert Map to a plain object for JSON serialization + const staffObject = Object.fromEntries(staffMap); + res.json(staffObject); + } else { + res.status(404).json({ error: `Could not retrieve base data using activity ID ${FIXED_STAFF_ACTIVITY_ID} to get staff details.` }); + } + } catch (error) { + console.error(`Error fetching staff data (for fixed activity ID ${FIXED_STAFF_ACTIVITY_ID}):`, error); + res.status(500).json({ error: 'An internal server error occurred while fetching staff data.' }); + } +}); + +// --- Start the Server --- +app.listen(PORT, () => { + console.log(`Server is running on http://localhost:${PORT}`); + console.log('API Endpoints:'); + console.log(` GET /v1/activity/:activityId (ID must be 1-4 digits)`); + console.log(` GET /v1/staffs`); + if (!USERNAME || !PASSWORD) { + console.warn('Warning: API_USERNAME or API_PASSWORD is not set. Please configure them in your .env file or environment variables.'); + } +}); + +// Graceful shutdown (optional but good practice) +process.on('SIGINT', () => { + console.log('Server shutting down...'); + // Perform any cleanup here + process.exit(0); +}); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..ad4a506 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "dsas-cca-backend", + "version": "1.0.0", + "description": "", + "main": "main.js", + "type": "module", + "scripts": { + "start": "node main.js", + "dev": "node --watch main.js" + }, + "dependencies": { + "axios": "^1.9.0", + "dotenv": "^16.5.0", + "express": "^5.1.0", + "node-fetch": "^3.3.2" + }, + "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..069b19c --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,721 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + axios: + specifier: ^1.9.0 + version: 1.9.0 + dotenv: + specifier: ^16.5.0 + version: 16.5.0 + express: + specifier: ^5.1.0 + version: 5.1.0 + node-fetch: + specifier: ^3.3.2 + version: 3.3.2 + +packages: + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.9.0: + resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==} + + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dotenv@16.5.0: + resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-to-regexp@8.2.0: + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + +snapshots: + + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + + asynckit@0.4.0: {} + + axios@1.9.0: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.2 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.0 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.0 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + data-uri-to-buffer@4.0.1: {} + + debug@4.4.0: + dependencies: + ms: 2.1.3 + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + dotenv@16.5.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.1 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + finalhandler@2.1.0: + dependencies: + debug: 4.4.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + follow-redirects@1.15.9: {} + + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + 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 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + is-promise@4.0.0: {} + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + + ms@2.1.3: {} + + negotiator@1.0.0: {} + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + parseurl@1.3.3: {} + + path-to-regexp@8.2.0: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@1.1.0: {} + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + + router@2.2.0: + dependencies: + debug: 4.4.0 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.2.0 + transitivePeerDependencies: + - supports-color + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + send@1.2.0: + dependencies: + debug: 4.4.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + statuses@2.0.1: {} + + toidentifier@1.0.1: {} + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + + unpipe@1.0.0: {} + + vary@1.1.2: {} + + web-streams-polyfill@3.3.3: {} + + wrappy@1.0.2: {}