diff --git a/controller/userPreferencesController.js b/controller/userPreferencesController.js index 5c96cc1..cb1c7ac 100644 --- a/controller/userPreferencesController.js +++ b/controller/userPreferencesController.js @@ -122,6 +122,35 @@ const updateNotificationPreferences = async (req, res) => { } }; + + +const getUiSettings = async (req, res) => { + try { + const response = await userPreferencesService.getUiSettings( + req.user.userId + ); + return res.status(200).json(response); + } catch (error) { + return handleError(res, error, "Error fetching UI settings", { + userId: req.user?.userId, + }); + } +}; + +const updateUiSettings = async (req, res) => { + try { + const response = await userPreferencesService.updateUiSettings( + req.user.userId, + req.body.ui_settings || {} + ); + return res.status(200).json(response); + } catch (error) { + return handleError(res, error, "Error updating UI settings", { + userId: req.user?.userId, + }); + } +}; + module.exports = { getUserPreferences, postUserPreferences, @@ -129,4 +158,6 @@ module.exports = { updateExtendedUserPreferences, getNotificationPreferences, updateNotificationPreferences, + getUiSettings, + updateUiSettings, }; diff --git a/model/updateUserPreferences.js b/model/updateUserPreferences.js index 2417fee..a206bc2 100644 --- a/model/updateUserPreferences.js +++ b/model/updateUserPreferences.js @@ -1,16 +1,42 @@ const supabase = require("../dbConnection.js"); const { EMPTY_HEALTH_CONTEXT, saveUserPreferenceState } = require("./userPreferenceState"); const { ServiceError } = require("../services/serviceError"); +const fetchUserPreferences = require("./fetchUserPreferences"); -function listFromHealthContext(items = []) { - return (Array.isArray(items) ? items : []) - .map((item) => { - if (Number.isInteger(item)) return item; - if (item && Number.isInteger(item.referenceId)) return item.referenceId; - if (item && Number.isInteger(item.id)) return item.id; - return null; - }) - .filter(Number.isInteger); +// ───────────────────────────────────────────────────────────────────────────── +// Helpers +// ───────────────────────────────────────────────────────────────────────────── + +function hasOwnProperty(object, key) { + return Object.prototype.hasOwnProperty.call(object, key); +} + +function hasAnyOwnProperty(object, keys = []) { + return keys.some((key) => hasOwnProperty(object, key)); +} + +function extractPreferenceId(value) { + if (Number.isInteger(value) && value > 0) return value; + if (value && typeof value === "object") { + for (const candidate of [value.referenceId, value.id]) { + const parsed = Number(candidate); + if (Number.isInteger(parsed) && parsed > 0) return parsed; + } + } + return null; +} + +function normalizePreferenceIds(values = []) { + if (!Array.isArray(values)) return []; + return [...new Set( + values.map(extractPreferenceId).filter((v) => Number.isInteger(v) && v > 0) + )]; +} + +function getFoodPreferenceSource(body = {}) { + return body.food_preferences && typeof body.food_preferences === "object" + ? body.food_preferences + : body; } function normalizeHealthContext(healthContext = {}) { @@ -23,9 +49,9 @@ function normalizeHealthContext(healthContext = {}) { function normalizeUiSettings(settings = {}) { return { - language: settings.language || 'en', - theme: settings.theme || 'light', - font_size: settings.font_size || '16px' + language: settings.language || "en", + theme: settings.theme || "light", + font_size: settings.font_size || "16px" }; } @@ -39,43 +65,34 @@ function normalizeNotificationPreferences(preferences = {}) { }; } -function normalizePreferenceIds(values = []) { - if (!Array.isArray(values)) { - return []; - } - - return [...new Set(values - .map((value) => Number(value)) - .filter((value) => Number.isInteger(value) && value > 0))]; -} - -function hasOwnProperty(object, key) { - return Object.prototype.hasOwnProperty.call(object, key); -} +// ───────────────────────────────────────────────────────────────────────────── +// Join table config — single source of truth for all food preference groups +// ───────────────────────────────────────────────────────────────────────────── const PREFERENCE_TABLES = [ { table: "user_dietary_requirements", foreignKey: "dietary_requirement_id", key: "dietary_requirements" }, - { table: "user_allergies", foreignKey: "allergy_id", key: "allergies" }, - { table: "user_cuisines", foreignKey: "cuisine_id", key: "cuisines" }, - { table: "user_dislikes", foreignKey: "dislike_id", key: "dislikes" }, - { table: "user_health_conditions", foreignKey: "health_condition_id", key: "health_conditions" }, - { table: "user_spice_levels", foreignKey: "spice_level_id", key: "spice_levels" }, - { table: "user_cooking_methods", foreignKey: "cooking_method_id", key: "cooking_methods" } + { table: "user_allergies", foreignKey: "allergy_id", key: "allergies" }, + { table: "user_cuisines", foreignKey: "cuisine_id", key: "cuisines" }, + { table: "user_dislikes", foreignKey: "dislike_id", key: "dislikes" }, + { table: "user_health_conditions", foreignKey: "health_condition_id", key: "health_conditions" }, + { table: "user_spice_levels", foreignKey: "spice_level_id", key: "spice_levels" }, + { table: "user_cooking_methods", foreignKey: "cooking_method_id", key: "cooking_methods" }, ]; +const FOOD_PREFERENCE_KEYS = PREFERENCE_TABLES.map(({ key }) => key); + +// ───────────────────────────────────────────────────────────────────────────── +// Join table helpers +// ───────────────────────────────────────────────────────────────────────────── + async function replaceJoinTable(table, userId, foreignKey, values = []) { const { error: deleteError } = await supabase .from(table) .delete() .eq("user_id", userId); - if (deleteError) { - throw deleteError; - } - - if (!values.length) { - return; - } + if (deleteError) throw deleteError; + if (!values.length) return; const records = values.map((value) => ({ user_id: userId, @@ -83,9 +100,7 @@ async function replaceJoinTable(table, userId, foreignKey, values = []) { })); const { error: insertError } = await supabase.from(table).insert(records); - if (insertError) { - throw insertError; - } + if (insertError) throw insertError; } async function replaceUserPreferencesFallback(userId, preferenceGroups) { @@ -106,13 +121,12 @@ async function replaceUserPreferencesTransaction(userId, preferenceGroups) { p_cooking_methods: preferenceGroups.cooking_methods }); - if (!error) { - return; - } + if (!error) return; - const rpcMissing = error.code === "PGRST202" - || error.code === "42883" - || /replace_user_preferences/i.test(error.message || ""); + const rpcMissing = + error.code === "PGRST202" || + error.code === "42883" || + /replace_user_preferences/i.test(error.message || ""); if (rpcMissing) { await replaceUserPreferencesFallback(userId, preferenceGroups); @@ -122,86 +136,108 @@ async function replaceUserPreferencesTransaction(userId, preferenceGroups) { throw error; } +// ───────────────────────────────────────────────────────────────────────────── +// Main export +// ───────────────────────────────────────────────────────────────────────────── + async function updateUserPreferences(userId, body = {}) { - try { - const normalizedUserId = Number(userId); - if (!Number.isInteger(normalizedUserId) || normalizedUserId <= 0) { - throw new ServiceError(400, 'User ID must be a positive integer'); - } + const normalizedUserId = Number(userId); + if (!Number.isInteger(normalizedUserId) || normalizedUserId <= 0) { + throw new ServiceError(400, "User ID must be a positive integer"); + } - const healthContext = normalizeHealthContext(body.health_context); - - const dietaryRequirements = Array.isArray(body.dietary_requirements) ? body.dietary_requirements : []; - const allergies = Array.isArray(body.allergies) ? body.allergies : listFromHealthContext(healthContext.allergies); - const cuisines = Array.isArray(body.cuisines) ? body.cuisines : []; - const dislikes = Array.isArray(body.dislikes) ? body.dislikes : []; - const healthConditions = Array.isArray(body.health_conditions) ? body.health_conditions : listFromHealthContext(healthContext.chronic_conditions); - const spiceLevels = Array.isArray(body.spice_levels) ? body.spice_levels : []; - const cookingMethods = Array.isArray(body.cooking_methods) ? body.cooking_methods : []; - - const shouldUpdateJoinTables = [ - 'dietary_requirements', - 'allergies', - 'cuisines', - 'dislikes', - 'health_conditions', - 'spice_levels', - 'cooking_methods' - ].some((key) => body[key] !== undefined) || body.health_context !== undefined; - - if ( - !body.health_context - && !body.notification_preferences - && !body.ui_settings - && ![ - 'dietary_requirements', - 'allergies', - 'cuisines', - 'dislikes', - 'health_conditions', - 'spice_levels', - 'cooking_methods' - ].every((key) => hasOwnProperty(body, key)) - ) { - throw new ServiceError( - 400, - 'All preference groups are required: dietary_requirements, allergies, cuisines, dislikes, health_conditions, spice_levels, cooking_methods' - ); + // Support both canonical nested payload { food_preferences: { ... } } + // and legacy flat payload { dietary_requirements: [], ... } + const foodSource = getFoodPreferenceSource(body); + + const incomingHealthContext = + body.health_context && typeof body.health_context === "object" + ? body.health_context + : undefined; + + // Detect which food preference groups were explicitly provided + // using FOOD_PREFERENCE_KEYS so allergies and health_conditions are never missed + const foodGroupUpdates = {}; + for (const key of FOOD_PREFERENCE_KEYS) { + if (hasOwnProperty(foodSource, key)) { + foodGroupUpdates[key] = normalizePreferenceIds(foodSource[key]); } + } - if (shouldUpdateJoinTables) { - await replaceUserPreferencesTransaction(normalizedUserId, { - dietary_requirements: normalizePreferenceIds(dietaryRequirements), - allergies: normalizePreferenceIds(allergies), - cuisines: normalizePreferenceIds(cuisines), - dislikes: normalizePreferenceIds(dislikes), - health_conditions: normalizePreferenceIds(healthConditions), - spice_levels: normalizePreferenceIds(spiceLevels), - cooking_methods: normalizePreferenceIds(cookingMethods) - }); - } + const hasFoodGroupUpdates = Object.keys(foodGroupUpdates).length > 0; + const hasHealthContextUpdate = hasOwnProperty(body, "health_context"); + const hasNotificationUpdate = hasOwnProperty(body, "notification_preferences"); + const hasUiSettingsUpdate = hasOwnProperty(body, "ui_settings"); + + const hasAnySupportedUpdate = + hasFoodGroupUpdates || + hasHealthContextUpdate || + hasNotificationUpdate || + hasUiSettingsUpdate; + + if (!hasAnySupportedUpdate) { + throw new ServiceError(400, "No supported preference fields were provided"); + } - if ( - body.health_context !== undefined || - body.notification_preferences !== undefined || - body.ui_settings !== undefined - ) { - await saveUserPreferenceState(normalizedUserId, (current) => ({ - ...current, - health_context: body.health_context !== undefined - ? normalizeHealthContext(body.health_context) - : current.health_context || EMPTY_HEALTH_CONTEXT, - notification_preferences: body.notification_preferences !== undefined - ? normalizeNotificationPreferences(body.notification_preferences) - : current.notification_preferences || {}, - ui_settings: body.ui_settings !== undefined - ? normalizeUiSettings(body.ui_settings) - : current.ui_settings || {} - })); + // Fetch current preferences so we can preserve unmodified groups + const current = await fetchUserPreferences(normalizedUserId); + + // ── Join tables ──────────────────────────────────────────────────────────── + if (hasFoodGroupUpdates) { + // Build the full set of groups — use incoming value if provided, else keep current + const nextGroups = {}; + for (const key of FOOD_PREFERENCE_KEYS) { + nextGroups[key] = hasOwnProperty(foodGroupUpdates, key) + ? foodGroupUpdates[key] + : normalizePreferenceIds(current[key]); } - } catch (error) { - throw error; + + await replaceUserPreferencesTransaction(normalizedUserId, nextGroups); } + + // ── Preference state (health_context, notifications, ui_settings) ────────── + if (hasHealthContextUpdate || hasNotificationUpdate || hasUiSettingsUpdate) { + await saveUserPreferenceState(normalizedUserId, (stored) => { + const currentHealthContext = stored.health_context || EMPTY_HEALTH_CONTEXT; + + const nextHealthContext = hasHealthContextUpdate + ? { + allergies: incomingHealthContext && hasOwnProperty(incomingHealthContext, "allergies") + ? normalizeHealthContext(incomingHealthContext).allergies + : currentHealthContext.allergies || [], + chronic_conditions: incomingHealthContext && hasOwnProperty(incomingHealthContext, "chronic_conditions") + ? normalizeHealthContext(incomingHealthContext).chronic_conditions + : currentHealthContext.chronic_conditions || [], + medications: incomingHealthContext && hasOwnProperty(incomingHealthContext, "medications") + ? normalizeHealthContext(incomingHealthContext).medications + : currentHealthContext.medications || [], + } + : currentHealthContext; + + const nextNotifications = hasNotificationUpdate + ? { + ...(stored.notification_preferences || {}), + ...normalizeNotificationPreferences(body.notification_preferences) + } + : (stored.notification_preferences || {}); + + const nextUiSettings = hasUiSettingsUpdate + ? { + ...(stored.ui_settings || {}), + ...body.ui_settings + } + : (stored.ui_settings || {}); + + return { + ...stored, + health_context: nextHealthContext, + notification_preferences: nextNotifications, + ui_settings: nextUiSettings, + }; + }); + } + + return fetchUserPreferences(normalizedUserId); } module.exports = updateUserPreferences; diff --git a/package-lock.json b/package-lock.json index ecd582f..fd6fcd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,11 +11,13 @@ "@google/generative-ai": "^0.24.1", "@sendgrid/mail": "8.1.3", "@supabase/supabase-js": "2.86.0", + "axios": "^1.6.0", "base64-arraybuffer": "^1.0.2", "bcrypt": "5.1.1", "bcryptjs": "2.4.3", "cors": "2.8.5", "crypto": "1.0.1", + "crypto-js": "^4.2.0", "dotenv": "16.6.1", "express": "4.19.1", "express-rate-limit": "7.5.0", @@ -23,28 +25,44 @@ "fs-extra": "11.3.1", "groq-sdk": "^1.1.2", "helmet": "8.1.0", + "ioredis": "^5.3.2", "joi": "^17.13.3", "jsonwebtoken": "9.0.2", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "morgan": "^1.10.0", "multer": "1.4.5-lts.1", "mysql2": "3.9.2", + "node-cron": "^3.0.3", "node-fetch": "3.3.2", "nodemailer": "8.0.2", + "passport": "^0.7.0", "prom-client": "^15.1.3", + "sharp": "^0.33.5", "sinon": "18.0.0", + "socket.io": "^4.7.5", + "stripe": "^14.25.0", + "supabase": "^1.0.0", "swagger-ui-express": "5.0.0", "twilio": "5.9.0", + "uuid": "^9.0.0", "winston": "^3.11.0", "xlsx": "^0.18.5", "yamljs": "0.3.0" }, "devDependencies": { "chai": "6.0.1", + "chai-http": "^4.4.0", + "concurrently": "^9.2.1", "eslint": "8.57.0", + "eslint-plugin-node": "^11.1.0", + "husky": "^9.1.7", "jest": "^29.7.0", "mocha": "11.7.2", "nyc": "15.1.0", "prettier": "3.2.5", "proxyquire": "^2.1.3", + "sinon": "18.0.0", "supertest": "7.1.4", "swagger-cli": "4.0.4" } @@ -936,6 +954,16 @@ "kuler": "^2.0.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -1157,6 +1185,409 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "license": "MIT" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1260,6 +1691,27 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1975,6 +2427,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" @@ -1984,6 +2437,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -1993,6 +2447,7 @@ "version": "11.2.2", "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0" @@ -2002,6 +2457,7 @@ "version": "8.0.3", "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1", @@ -2012,6 +2468,7 @@ "version": "0.7.3", "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true, "license": "(Unlicense OR Apache-2.0)" }, "node_modules/@so-ric/colorspace": { @@ -2024,6 +2481,12 @@ "text-hex": "1.0.x" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@supabase/auth-js": { "version": "2.86.0", "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.86.0.tgz", @@ -2149,6 +2612,29 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "4.3.20", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -2215,6 +2701,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/superagent": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.13.tgz", + "integrity": "sha512-YIGelp3ZyMiH0/A09PMAORO0EBGlF5xIKfDpK74wdYvWUs2o96b5CItJcWPdH409b7SAXIIG6p8NdU/4U2Maww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", @@ -2662,8 +3159,17 @@ "node": ">= 0.6.0" } }, - "node_modules/baseline-browser-mapping": { - "version": "2.8.30", + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.30", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz", "integrity": "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==", "dev": true, @@ -2672,6 +3178,24 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/bcrypt": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", @@ -2691,6 +3215,47 @@ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", "license": "MIT" }, + "node_modules/bin-links": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-5.0.0.tgz", + "integrity": "sha512-sdleLVfCjBtgO5cNjA2HVRvWBJAHs4zwenaCPMNJAJU0yNxpzj80IpjOIimkpkr+mhlA+how5poQtt53PygbHA==", + "license": "ISC", + "dependencies": { + "cmd-shim": "^7.0.0", + "npm-normalize-package-bin": "^4.0.0", + "proc-log": "^5.0.0", + "read-cmd-shim": "^5.0.0", + "write-file-atomic": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/bin-links/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": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/bin-links/node_modules/write-file-atomic": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-6.0.0.tgz", + "integrity": "sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/bintrees": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", @@ -2973,6 +3538,103 @@ "node": ">=18" } }, + "node_modules/chai-http": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/chai-http/-/chai-http-4.4.0.tgz", + "integrity": "sha512-uswN3rZpawlRaa5NiDUHcDZ3v2dw5QgLyAwnQ2tnVNuP7CwIsOFuYJ0xR1WiR7ymD4roBnJIzOUep7w9jQMFJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "4", + "@types/superagent": "4.1.13", + "charset": "^1.0.1", + "cookiejar": "^2.1.4", + "is-ip": "^2.0.0", + "methods": "^1.1.2", + "qs": "^6.11.2", + "superagent": "^8.0.9" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/chai-http/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/chai-http/node_modules/formidable": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.5.tgz", + "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/chai-http/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/chai-http/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chai-http/node_modules/superagent": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", + "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", + "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.2", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3013,6 +3675,16 @@ "node": ">=10" } }, + "node_modules/charset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/charset/-/charset-1.0.1.tgz", + "integrity": "sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -3069,6 +3741,24 @@ "node": ">=12" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cmd-shim": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-7.0.0.tgz", + "integrity": "sha512-rtpaCbr164TPPh+zFdkWpCyZuKkjpAzODfaZCf/SVJZzJN+4bHQb/LP3Jzq5/+84um3XXY8r548XiWKSborwVw==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -3112,7 +3802,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3125,7 +3814,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/color-string": { @@ -3228,6 +3916,31 @@ "typedarray": "^0.0.6" } }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -3357,6 +4070,12 @@ "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", "license": "ISC" }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "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", @@ -3516,6 +4235,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -3632,6 +4352,68 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.7.tgz", + "integrity": "sha512-DgOngfDKM2EviOH3Mr9m7ks1q8roetLy/IMmYthAYzbpInMbYc/GS+fWFA3rl1gvwKVsQrVV61fo5emD1y3OJQ==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -3780,6 +4562,81 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-plugin-es": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", + "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-node": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", + "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-plugin-es": "^3.0.0", + "eslint-utils": "^2.0.0", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "peerDependencies": { + "eslint": ">=5.16.0" + } + }, + "node_modules/eslint-plugin-node/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-node/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-node/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -3797,6 +4654,32 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", @@ -4751,6 +5634,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4911,6 +5795,22 @@ "node": ">=10.17.0" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iceberg-js": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.0.tgz", @@ -4993,7 +5893,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -5026,6 +5925,63 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ioredis": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ioredis/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ioredis/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -5100,6 +6056,19 @@ "node": ">=0.10.0" } }, + "node_modules/is-ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-2.0.0.tgz", + "integrity": "sha512-9MTn0dteHETtyUx8pxqMwg5hMBi3pvlyglJ+b79KOCca0po23337LbVV2Hl4xmMvfw++ljnO0/+5G6G+0Szh6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-regex": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -5259,6 +6228,17 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-processinfo/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", @@ -6149,6 +7129,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true, "license": "MIT" }, "node_modules/jwa": { @@ -6251,6 +7232,12 @@ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, "node_modules/lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", @@ -6264,6 +7251,12 @@ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -6671,6 +7664,43 @@ "dev": true, "license": "MIT" }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -6768,6 +7798,7 @@ "version": "6.1.5", "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.5.tgz", "integrity": "sha512-SnRDPDBjxZZoU2n0+gzzLtSvo1OZo7j6jnbXsoh3AFxEGhaFU7ZF0TmefuKERq79wxR2U+MPn7ArW+Tl+clC3A==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1", @@ -6781,6 +7812,7 @@ "version": "13.0.5", "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1" @@ -6790,6 +7822,7 @@ "version": "8.4.2", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, "license": "MIT", "engines": { "node": ">=16" @@ -6800,6 +7833,28 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "license": "ISC", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/node-cron/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -6897,6 +7952,15 @@ "node": ">=0.10.0" } }, + "node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -7237,6 +8301,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -7416,6 +8489,32 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7482,6 +8581,11 @@ "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", "license": "MIT" }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7635,6 +8739,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.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", @@ -7816,6 +8929,15 @@ "dev": true, "license": "MIT" }, + "node_modules/read-cmd-shim": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-5.0.0.tgz", + "integrity": "sha512-SEbJV7tohp3DAAILbEMPXavBjAnMN0tVnh4+9G8ihV4Pq3HYF9h8QNez9zkJ1ILkv9G2BjdzwctznGZXgu/HGw==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -7831,11 +8953,45 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } }, "node_modules/release-zalgo": { "version": "1.0.0", @@ -8022,6 +9178,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -8146,6 +9312,68 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/sharp/node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/sharp/node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -8169,6 +9397,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "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", @@ -8246,10 +9487,26 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, "node_modules/sinon": { "version": "18.0.0", "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.0.tgz", "integrity": "sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1", @@ -8268,6 +9525,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -8293,6 +9551,116 @@ "node": ">=8" } }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -8404,6 +9772,12 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -8539,6 +9913,155 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stripe": { + "version": "14.25.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-14.25.0.tgz", + "integrity": "sha512-wQS3GNMofCXwH8TSje8E1SE8zr6ODiGtHQgPtO95p9Mb4FhKC9jvXR2NUTpZ9ZINlckJcFidCmaTFV4P6vsb9g==", + "license": "MIT", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + } + }, + "node_modules/supabase": { + "version": "1.226.4", + "resolved": "https://registry.npmjs.org/supabase/-/supabase-1.226.4.tgz", + "integrity": "sha512-qEzoagrqZs5T7sAlfZzehX3PJ13cSBrJVs2vrh6xC+B0VI0wgOBw2gCNRcsOMJMpSr0V1l0XueCiFBWPm2U03w==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bin-links": "^5.0.0", + "https-proxy-agent": "^7.0.2", + "node-fetch": "^3.3.2", + "tar": "7.4.3" + }, + "bin": { + "supabase": "bin/supabase" + }, + "engines": { + "npm": ">=8" + } + }, + "node_modules/supabase/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/supabase/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/supabase/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/supabase/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/supabase/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supabase/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/supabase/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supabase/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/supabase/node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/supabase/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/superagent": { "version": "10.2.3", "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", @@ -8833,6 +10356,16 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", @@ -8883,6 +10416,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -9011,10 +10545,14 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { "uuid": "dist/bin/uuid" diff --git a/package.json b/package.json index e8831a6..e2724e2 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "scripts": { "start": "node server.js", "dev": "nodemon server.js", + "dev:all": "concurrently --names \"API,AI\" --prefix-colors \"blue,green\" \"npm run dev\" \"cd ../NutriHelp-AI && python3 run.py\"", + "start:all": "concurrently --names \"API,AI\" --prefix-colors \"blue,green\" \"npm run start\" \"cd ../NutriHelp-AI && python3 run.py\"", "test": "mocha ./test/**/*.test.js --timeout 5000 --exit --ignore ./test/contractTests/**", "test:unit": "mocha ./test/unit/**/*.test.js --timeout 5000 --exit", "test:contract": "jest ./test/contractTests/ --testTimeout=10000 --forceExit", @@ -21,12 +23,17 @@ }, "devDependencies": { "chai": "6.0.1", + "chai-http": "^4.4.0", + "concurrently": "^9.2.1", "eslint": "8.57.0", + "eslint-plugin-node": "^11.1.0", + "husky": "^9.1.7", "jest": "^29.7.0", "mocha": "11.7.2", "nyc": "15.1.0", "prettier": "3.2.5", "proxyquire": "^2.1.3", + "sinon": "18.0.0", "supertest": "7.1.4", "swagger-cli": "4.0.4" }, @@ -34,11 +41,13 @@ "@google/generative-ai": "^0.24.1", "@sendgrid/mail": "8.1.3", "@supabase/supabase-js": "2.86.0", + "axios": "^1.6.0", "base64-arraybuffer": "^1.0.2", "bcrypt": "5.1.1", "bcryptjs": "2.4.3", "cors": "2.8.5", "crypto": "1.0.1", + "crypto-js": "^4.2.0", "dotenv": "16.6.1", "express": "4.19.1", "express-rate-limit": "7.5.0", @@ -46,16 +55,27 @@ "fs-extra": "11.3.1", "groq-sdk": "^1.1.2", "helmet": "8.1.0", + "ioredis": "^5.3.2", "joi": "^17.13.3", "jsonwebtoken": "9.0.2", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "morgan": "^1.10.0", "multer": "1.4.5-lts.1", "mysql2": "3.9.2", + "node-cron": "^3.0.3", "node-fetch": "3.3.2", "nodemailer": "8.0.2", + "passport": "^0.7.0", "prom-client": "^15.1.3", + "sharp": "^0.33.5", "sinon": "18.0.0", + "socket.io": "^4.7.5", + "stripe": "^14.25.0", + "supabase": "^1.0.0", "swagger-ui-express": "5.0.0", "twilio": "5.9.0", + "uuid": "^9.0.0", "winston": "^3.11.0", "xlsx": "^0.18.5", "yamljs": "0.3.0" diff --git a/routes/userPreferences.js b/routes/userPreferences.js index 3991775..80c173e 100644 --- a/routes/userPreferences.js +++ b/routes/userPreferences.js @@ -4,8 +4,9 @@ const controller = require("../controller/userPreferencesController"); const { authenticateToken } = require("../middleware/authenticateToken"); const { validateUserPreferences, - validateHealthContext, + validateExtendedUserPreferences, validateNotificationPreferences, + validateUiSettings, } = require("../validators/userPreferencesValidator"); const ValidateRequest = require("../middleware/validateRequest"); @@ -24,11 +25,11 @@ router.post( // GET /api/user/preferences/extended — authenticated user reads full health-context + food prefs router.get("/extended", authenticateToken, controller.getExtendedUserPreferences); -// PUT /api/user/preferences/extended — authenticated user updates health-context +// PUT /api/user/preferences/extended — authenticated user updates canonical preferences payload router.put( "/extended", authenticateToken, - validateHealthContext, + validateExtendedUserPreferences, ValidateRequest, controller.updateExtendedUserPreferences ); @@ -49,4 +50,20 @@ router.put( controller.updateNotificationPreferences ); +// GET /api/user/preferences/extended/ui-settings — authenticated user reads ui settings +router.get( + "/extended/ui-settings", + authenticateToken, + controller.getUiSettings +); + +// PUT /api/user/preferences/extended/ui-settings — authenticated user updates ui settings +router.put( + "/extended/ui-settings", + authenticateToken, + validateUiSettings, + ValidateRequest, + controller.updateUiSettings +); + module.exports = router; diff --git a/services/userPreferencesService.js b/services/userPreferencesService.js index e7a985c..82e3cde 100644 --- a/services/userPreferencesService.js +++ b/services/userPreferencesService.js @@ -1,9 +1,8 @@ const fetchUserPreferences = require('../model/fetchUserPreferences'); const updateUserPreferences = require('../model/updateUserPreferences'); const { ServiceError } = require('./serviceError'); -const { decryptFromDatabase, encryptForDatabase } = require('./encryptionService'); -const USER_PREFERENCES_CONTRACT_VERSION = 'user-preferences-v2'; +const USER_PREFERENCES_CONTRACT_VERSION = 'user-preferences-v3'; const DEFAULT_NOTIFICATION_PREFERENCES = { mealReminders: true, @@ -33,18 +32,30 @@ function normalizeStringArray(items) { function normalizeStructuredAllergy(item = {}) { return { - referenceId: Number.isInteger(item.referenceId) ? item.referenceId : Number.isInteger(item.id) ? item.id : null, + referenceId: Number.isInteger(item.referenceId) + ? item.referenceId + : Number.isInteger(item.id) + ? item.id + : null, name: asTrimmedString(item.name), - severity: ['mild', 'moderate', 'severe', 'unknown'].includes(item.severity) ? item.severity : 'unknown', + severity: ['mild', 'moderate', 'severe', 'unknown'].includes(item.severity) + ? item.severity + : 'unknown', notes: asTrimmedString(item.notes) }; } function normalizeStructuredCondition(item = {}) { return { - referenceId: Number.isInteger(item.referenceId) ? item.referenceId : Number.isInteger(item.id) ? item.id : null, + referenceId: Number.isInteger(item.referenceId) + ? item.referenceId + : Number.isInteger(item.id) + ? item.id + : null, name: asTrimmedString(item.name), - status: ['active', 'managed', 'resolved', 'unknown'].includes(item.status) ? item.status : 'active', + status: ['active', 'managed', 'resolved', 'unknown'].includes(item.status) + ? item.status + : 'active', notes: asTrimmedString(item.notes) }; } @@ -58,7 +69,9 @@ function normalizeMedication(item = {}, index = 0) { unit: asTrimmedString(item.dosage?.unit ?? item.unit) }, frequency: { - timesPerDay: Number.isInteger(item.frequency?.timesPerDay) ? item.frequency.timesPerDay : null, + timesPerDay: Number.isInteger(item.frequency?.timesPerDay) + ? item.frequency.timesPerDay + : null, interval: asTrimmedString(item.frequency?.interval), schedule: normalizeStringArray(item.frequency?.schedule), asNeeded: Boolean(item.frequency?.asNeeded) @@ -70,9 +83,6 @@ function normalizeMedication(item = {}, index = 0) { } function buildStructuredHealthContext(rawPreferences = {}) { - // Week 6: Encryption of allergies and health conditions is handled at the migration - // and model level (fetchUserPreferences decrypts encrypted rows from user_allergies - // and user_health_conditions tables via the RPC/query layer) const storeHealthContext = rawPreferences.health_context || {}; const allergiesById = new Map( @@ -105,17 +115,44 @@ function buildStructuredHealthContext(rawPreferences = {}) { }); }); - const extraAllergies = structuredAllergies.filter((entry) => entry.referenceId == null || !allergiesById.has(entry.referenceId)); - const extraConditions = structuredConditions.filter((entry) => entry.referenceId == null || !conditionsById.has(entry.referenceId)); + const extraAllergies = structuredAllergies.filter( + (entry) => entry.referenceId == null || !allergiesById.has(entry.referenceId) + ); + const extraConditions = structuredConditions.filter( + (entry) => entry.referenceId == null || !conditionsById.has(entry.referenceId) + ); return { allergies: [...mergedAllergies, ...extraAllergies], chronic_conditions: [...mergedConditions, ...extraConditions], - medications: (storeHealthContext.medications || []).map(normalizeMedication).filter((item) => item.name), + medications: (storeHealthContext.medications || []) + .map(normalizeMedication) + .filter((item) => item.name), normalized_summary: { - allergyNames: [...new Set([...mergedAllergies, ...extraAllergies].map((item) => item.name).filter(Boolean).map((item) => item.toLowerCase()))], - chronicConditionNames: [...new Set([...mergedConditions, ...extraConditions].map((item) => item.name).filter(Boolean).map((item) => item.toLowerCase()))], - activeMedicationNames: [...new Set((storeHealthContext.medications || []).map(normalizeMedication).filter((item) => item.name && item.active).map((item) => item.name.toLowerCase()))] + allergyNames: [ + ...new Set( + [...mergedAllergies, ...extraAllergies] + .map((item) => item.name) + .filter(Boolean) + .map((item) => item.toLowerCase()) + ) + ], + chronicConditionNames: [ + ...new Set( + [...mergedConditions, ...extraConditions] + .map((item) => item.name) + .filter(Boolean) + .map((item) => item.toLowerCase()) + ) + ], + activeMedicationNames: [ + ...new Set( + (storeHealthContext.medications || []) + .map(normalizeMedication) + .filter((item) => item.name && item.active) + .map((item) => item.name.toLowerCase()) + ) + ] } }; } @@ -164,9 +201,14 @@ async function updateExtendedPreferences(userId, payload = {}) { } async function getNotificationPreferences(userId) { + if (!userId) { + throw new ServiceError(400, 'User ID is required'); + } + const response = await getExtendedPreferences(userId); return { success: true, + contractVersion: USER_PREFERENCES_CONTRACT_VERSION, data: response.data.notification_preferences }; } @@ -183,6 +225,33 @@ async function updateNotificationPreferences(userId, notificationPreferences = { return getNotificationPreferences(userId); } +async function getUiSettings(userId) { + if (!userId) { + throw new ServiceError(400, 'User ID is required'); + } + + const response = await getExtendedPreferences(userId); + return { + success: true, + contractVersion: USER_PREFERENCES_CONTRACT_VERSION, + data: { + ui_settings: response.data.ui_settings + } + }; +} + +async function updateUiSettings(userId, uiSettings = {}) { + if (!userId) { + throw new ServiceError(400, 'User ID is required'); + } + + await updateUserPreferences(userId, { + ui_settings: uiSettings + }); + + return getUiSettings(userId); +} + module.exports = { DEFAULT_NOTIFICATION_PREFERENCES, DEFAULT_UI_SETTINGS, @@ -191,6 +260,8 @@ module.exports = { buildStructuredHealthContext, getExtendedPreferences, getNotificationPreferences, + getUiSettings, updateExtendedPreferences, - updateNotificationPreferences + updateNotificationPreferences, + updateUiSettings }; diff --git a/test/unit/updateUserPreferences.test.js b/test/unit/updateUserPreferences.test.js index f32724d..a326d8c 100644 --- a/test/unit/updateUserPreferences.test.js +++ b/test/unit/updateUserPreferences.test.js @@ -2,24 +2,81 @@ const { expect } = require('chai'); const sinon = require('sinon'); const proxyquire = require('proxyquire').noCallThru(); +const EMPTY_HEALTH_CONTEXT = { allergies: [], chronic_conditions: [], medications: [] }; + +function makeStubs(overrides = {}) { + const rpc = sinon.stub().resolves({ error: null }); + const from = sinon.stub().callsFake(() => ({ + delete: sinon.stub().returns({ eq: sinon.stub().resolves({ error: null }) }), + insert: sinon.stub().resolves({ error: null }) + })); + const saveUserPreferenceState = sinon.stub().callsFake(async (userId, fn) => fn({})); + const fetchUserPreferences = sinon.stub().resolves({ + dietary_requirements: [], + allergies: [], + cuisines: [], + dislikes: [], + health_conditions: [], + spice_levels: [], + cooking_methods: [], + health_context: EMPTY_HEALTH_CONTEXT, + notification_preferences: {}, + ui_settings: {} + }); + + return proxyquire('../../model/updateUserPreferences', { + '../dbConnection.js': { rpc, from }, + './userPreferenceState': { EMPTY_HEALTH_CONTEXT, saveUserPreferenceState }, + './fetchUserPreferences': fetchUserPreferences, + '../services/serviceError': require('../../services/serviceError'), + ...overrides + }); +} + describe('updateUserPreferences', () => { - afterEach(() => { - sinon.restore(); + afterEach(() => sinon.restore()); + + // ── Validation ───────────────────────────────────────────────────────────── + + it('rejects invalid user ids with a 400 status', async () => { + const update = makeStubs(); + try { + await update(0, { dietary_requirements: [] }); + throw new Error('Expected rejection'); + } catch (err) { + expect(err.statusCode).to.equal(400); + } }); - it('replaces preference rows through the RPC transaction', async () => { + it('rejects when no supported fields are provided', async () => { + const update = makeStubs(); + try { + await update(1, { unsupported_field: 'foo' }); + throw new Error('Expected rejection'); + } catch (err) { + expect(err.statusCode).to.equal(400); + expect(err.message).to.match(/no supported preference fields/i); + } + }); + + // ── Flat legacy payload ──────────────────────────────────────────────────── + + it('replaces join tables via RPC for flat payload', async () => { const rpc = sinon.stub().resolves({ error: null }); - const saveUserPreferenceState = sinon.stub().resolves(); + const saveUserPreferenceState = sinon.stub().callsFake(async (userId, fn) => fn({})); + const fetchUserPreferences = sinon.stub().resolves({ + dietary_requirements: [], allergies: [], cuisines: [], + dislikes: [], health_conditions: [], spice_levels: [], cooking_methods: [] + }); - const updateUserPreferences = proxyquire('../../model/updateUserPreferences', { + const update = proxyquire('../../model/updateUserPreferences', { '../dbConnection.js': { rpc }, - './userPreferenceState': { - EMPTY_HEALTH_CONTEXT: { allergies: [], chronic_conditions: [], medications: [] }, - saveUserPreferenceState - } + './userPreferenceState': { EMPTY_HEALTH_CONTEXT, saveUserPreferenceState }, + './fetchUserPreferences': fetchUserPreferences, + '../services/serviceError': require('../../services/serviceError') }); - await updateUserPreferences(42, { + await update(42, { dietary_requirements: [1, '2', 2, -1], allergies: [3], cuisines: [4], @@ -44,54 +101,179 @@ describe('updateUserPreferences', () => { expect(saveUserPreferenceState.called).to.equal(false); }); - it('rejects invalid user ids with a 400 status', async () => { - const updateUserPreferences = proxyquire('../../model/updateUserPreferences', { - '../dbConnection.js': { rpc: sinon.stub() }, - './userPreferenceState': { - EMPTY_HEALTH_CONTEXT: { allergies: [], chronic_conditions: [], medications: [] }, - saveUserPreferenceState: sinon.stub() + // ── Canonical nested payload ─────────────────────────────────────────────── + + it('accepts canonical nested food_preferences payload', async () => { + const rpc = sinon.stub().resolves({ error: null }); + const saveUserPreferenceState = sinon.stub().callsFake(async (userId, fn) => fn({})); + const fetchUserPreferences = sinon.stub().resolves({ + dietary_requirements: [], allergies: [], cuisines: [], + dislikes: [], health_conditions: [], spice_levels: [], cooking_methods: [] + }); + + const update = proxyquire('../../model/updateUserPreferences', { + '../dbConnection.js': { rpc }, + './userPreferenceState': { EMPTY_HEALTH_CONTEXT, saveUserPreferenceState }, + './fetchUserPreferences': fetchUserPreferences, + '../services/serviceError': require('../../services/serviceError') + }); + + await update(42, { + food_preferences: { + dietary_requirements: [1, 2], + cuisines: [3], + dislikes: [], + spice_levels: [4], + cooking_methods: [] } }); - try { - await updateUserPreferences(0, { - dietary_requirements: [], - allergies: [], + expect(rpc.calledOnce).to.equal(true); + expect(rpc.firstCall.args[1].p_dietary_requirements).to.deep.equal([1, 2]); + expect(rpc.firstCall.args[1].p_cuisines).to.deep.equal([3]); + }); + + it('accepts { id } and { referenceId } objects in nested payload', async () => { + const rpc = sinon.stub().resolves({ error: null }); + const saveUserPreferenceState = sinon.stub().callsFake(async (userId, fn) => fn({})); + const fetchUserPreferences = sinon.stub().resolves({ + dietary_requirements: [], allergies: [], cuisines: [], + dislikes: [], health_conditions: [], spice_levels: [], cooking_methods: [] + }); + + const update = proxyquire('../../model/updateUserPreferences', { + '../dbConnection.js': { rpc }, + './userPreferenceState': { EMPTY_HEALTH_CONTEXT, saveUserPreferenceState }, + './fetchUserPreferences': fetchUserPreferences, + '../services/serviceError': require('../../services/serviceError') + }); + + await update(42, { + food_preferences: { + dietary_requirements: [{ id: 1 }, { referenceId: 2 }, 3], cuisines: [], dislikes: [], - health_conditions: [], spice_levels: [], cooking_methods: [] - }); - throw new Error('Expected updateUserPreferences to reject'); - } catch (error) { - expect(error.statusCode).to.equal(400); - } + } + }); + + expect(rpc.firstCall.args[1].p_dietary_requirements).to.deep.equal([1, 2, 3]); }); - it('falls back to join-table replacement when the RPC migration is missing', async () => { - const rpc = sinon.stub().resolves({ - error: { - code: 'PGRST202', - message: 'Could not find function public.replace_user_preferences' + // ── Partial update safety ────────────────────────────────────────────────── + + it('does NOT wipe unrelated join tables when only ui_settings is updated', async () => { + const rpc = sinon.stub().resolves({ error: null }); + const saveUserPreferenceState = sinon.stub().callsFake(async (userId, fn) => fn({})); + const fetchUserPreferences = sinon.stub().resolves({ + dietary_requirements: [1, 2], + allergies: [3], + cuisines: [4], + dislikes: [], + health_conditions: [], + spice_levels: [], + cooking_methods: [] + }); + + const update = proxyquire('../../model/updateUserPreferences', { + '../dbConnection.js': { rpc }, + './userPreferenceState': { EMPTY_HEALTH_CONTEXT, saveUserPreferenceState }, + './fetchUserPreferences': fetchUserPreferences, + '../services/serviceError': require('../../services/serviceError') + }); + + await update(42, { ui_settings: { theme: 'dark' } }); + + // RPC should NOT be called — no join table changes + expect(rpc.called).to.equal(false); + // Only saveUserPreferenceState should be called + expect(saveUserPreferenceState.calledOnce).to.equal(true); + }); + + it('does NOT wipe food preferences when only health_context is updated', async () => { + const rpc = sinon.stub().resolves({ error: null }); + const saveUserPreferenceState = sinon.stub().callsFake(async (userId, fn) => fn({})); + const fetchUserPreferences = sinon.stub().resolves({ + dietary_requirements: [1, 2], + allergies: [3], + cuisines: [4], + dislikes: [], + health_conditions: [], + spice_levels: [], + cooking_methods: [] + }); + + const update = proxyquire('../../model/updateUserPreferences', { + '../dbConnection.js': { rpc }, + './userPreferenceState': { EMPTY_HEALTH_CONTEXT, saveUserPreferenceState }, + './fetchUserPreferences': fetchUserPreferences, + '../services/serviceError': require('../../services/serviceError') + }); + + await update(42, { + health_context: { + medications: [{ name: 'Aspirin', active: true }] } }); + + expect(rpc.called).to.equal(false); + expect(saveUserPreferenceState.calledOnce).to.equal(true); + }); + + // ── Notification merging ─────────────────────────────────────────────────── + + it('merges notification_preferences with existing stored values', async () => { + let capturedMerger; + const saveUserPreferenceState = sinon.stub().callsFake(async (userId, fn) => { + capturedMerger = fn; + return fn({ notification_preferences: { mealReminders: true, weeklyReports: false } }); + }); + const fetchUserPreferences = sinon.stub().resolves({ + dietary_requirements: [], allergies: [], cuisines: [], + dislikes: [], health_conditions: [], spice_levels: [], cooking_methods: [] + }); + + const update = proxyquire('../../model/updateUserPreferences', { + '../dbConnection.js': { rpc: sinon.stub() }, + './userPreferenceState': { EMPTY_HEALTH_CONTEXT, saveUserPreferenceState }, + './fetchUserPreferences': fetchUserPreferences, + '../services/serviceError': require('../../services/serviceError') + }); + + await update(42, { + notification_preferences: { weeklyReports: true } + }); + + const result = capturedMerger({ notification_preferences: { mealReminders: true, weeklyReports: false } }); + expect(result.notification_preferences.weeklyReports).to.equal(true); + expect(result.notification_preferences.mealReminders).to.equal(true); + }); + + // ── RPC fallback ─────────────────────────────────────────────────────────── + + it('falls back to join-table replacement when RPC is missing', async () => { + const rpc = sinon.stub().resolves({ + error: { code: 'PGRST202', message: 'Could not find function public.replace_user_preferences' } + }); const from = sinon.stub().callsFake(() => ({ - delete: sinon.stub().returns({ - eq: sinon.stub().resolves({ error: null }) - }), + delete: sinon.stub().returns({ eq: sinon.stub().resolves({ error: null }) }), insert: sinon.stub().resolves({ error: null }) })); + const saveUserPreferenceState = sinon.stub().callsFake(async (userId, fn) => fn({})); + const fetchUserPreferences = sinon.stub().resolves({ + dietary_requirements: [], allergies: [], cuisines: [], + dislikes: [], health_conditions: [], spice_levels: [], cooking_methods: [] + }); - const updateUserPreferences = proxyquire('../../model/updateUserPreferences', { + const update = proxyquire('../../model/updateUserPreferences', { '../dbConnection.js': { rpc, from }, - './userPreferenceState': { - EMPTY_HEALTH_CONTEXT: { allergies: [], chronic_conditions: [], medications: [] }, - saveUserPreferenceState: sinon.stub() - } + './userPreferenceState': { EMPTY_HEALTH_CONTEXT, saveUserPreferenceState }, + './fetchUserPreferences': fetchUserPreferences, + '../services/serviceError': require('../../services/serviceError') }); - await updateUserPreferences(42, { + await update(42, { dietary_requirements: [1], allergies: [2], cuisines: [], @@ -103,7 +285,7 @@ describe('updateUserPreferences', () => { expect(rpc.calledOnce).to.equal(true); expect(from.called).to.equal(true); - const touchedTables = [...new Set(from.getCalls().map((call) => call.args[0]))]; + const touchedTables = [...new Set(from.getCalls().map((c) => c.args[0]))]; expect(touchedTables).to.include.members([ 'user_dietary_requirements', 'user_allergies', diff --git a/test/userPreferencesExtended.test.js b/test/userPreferencesExtended.test.js index 5a2abeb..248356b 100644 --- a/test/userPreferencesExtended.test.js +++ b/test/userPreferencesExtended.test.js @@ -3,11 +3,9 @@ * * Covers: * GET /api/user/preferences/extended - * PUT /api/user/preferences/extended (health_context, ui_settings) + * PUT /api/user/preferences/extended * GET /api/user/preferences/extended/notifications * PUT /api/user/preferences/extended/notifications - * - * Valid and invalid payload variants are tested for each. */ require('dotenv').config(); const chai = require('chai'); @@ -19,22 +17,19 @@ chai.use(chaiHttp); const BASE = 'http://localhost:80'; -// ───────────────────────────────────────────────────────────────────────────── -// Helpers -// ───────────────────────────────────────────────────────────────────────────── - async function getToken(testUser) { const res = await chai .request(BASE) .post('/api/login') .send({ email: testUser.email, password: 'testuser123' }); - return res.body.token; + + return res.body?.data?.token; } const VALID_HEALTH_CONTEXT = { allergies: [ { referenceId: null, name: 'Peanuts', severity: 'severe', notes: 'Carries EpiPen' }, - { referenceId: 1, name: 'Shellfish', severity: 'moderate', notes: null }, + { referenceId: null, name: 'Shellfish', severity: 'moderate', notes: null }, ], chronic_conditions: [ { referenceId: null, name: 'Type 2 Diabetes', status: 'managed', notes: 'On metformin' }, @@ -43,7 +38,12 @@ const VALID_HEALTH_CONTEXT = { { name: 'Metformin', dosage: { amount: '500', unit: 'mg' }, - frequency: { timesPerDay: 2, interval: null, schedule: ['morning', 'evening'], asNeeded: false }, + frequency: { + timesPerDay: 2, + interval: null, + schedule: ['morning', 'evening'], + asNeeded: false, + }, purpose: 'Blood sugar control', notes: null, active: true, @@ -59,10 +59,6 @@ const VALID_NOTIFICATION_PREFS = { systemUpdates: true, }; -// ───────────────────────────────────────────────────────────────────────────── -// Tests -// ───────────────────────────────────────────────────────────────────────────── - describe('GET /api/user/preferences/extended', () => { let testUser, token; @@ -112,8 +108,6 @@ describe('GET /api/user/preferences/extended', () => { }); }); -// ───────────────────────────────────────────────────────────────────────────── - describe('PUT /api/user/preferences/extended — valid payloads', () => { let testUser, token; @@ -186,8 +180,6 @@ describe('PUT /api/user/preferences/extended — valid payloads', () => { }); }); -// ───────────────────────────────────────────────────────────────────────────── - describe('PUT /api/user/preferences/extended — invalid payloads', () => { let testUser, token; @@ -320,8 +312,6 @@ describe('PUT /api/user/preferences/extended — invalid payloads', () => { }); }); -// ───────────────────────────────────────────────────────────────────────────── - describe('GET /api/user/preferences/extended/notifications', () => { let testUser, token; @@ -342,7 +332,7 @@ describe('GET /api/user/preferences/extended/notifications', () => { .end((err, res) => { expect(res).to.have.status(200); expect(res.body).to.have.property('success', true); - const prefs = res.body.data; + const prefs = res.body.data.notification_preferences; ['mealReminders', 'waterReminders', 'healthTips', 'weeklyReports', 'systemUpdates'].forEach( (key) => expect(prefs).to.have.property(key) ); @@ -351,8 +341,6 @@ describe('GET /api/user/preferences/extended/notifications', () => { }); }); -// ───────────────────────────────────────────────────────────────────────────── - describe('PUT /api/user/preferences/extended/notifications', () => { let testUser, token; @@ -374,7 +362,7 @@ describe('PUT /api/user/preferences/extended/notifications', () => { .end((err, res) => { expect(res).to.have.status(200); expect(res.body).to.have.property('success', true); - const prefs = res.body.data; + const prefs = res.body.data.notification_preferences; expect(prefs.mealReminders).to.equal(false); expect(prefs.weeklyReports).to.equal(true); done(); diff --git a/validators/userPreferencesValidator.js b/validators/userPreferencesValidator.js index 4f62433..850d859 100644 --- a/validators/userPreferencesValidator.js +++ b/validators/userPreferencesValidator.js @@ -8,18 +8,33 @@ const UI_LANGUAGES = ['en', 'zh', 'es', 'fr', 'de']; const isArrayOfIntegers = (value) => Array.isArray(value) && value.every(Number.isInteger); +function isPositiveInteger(value) { + return Number.isInteger(value) && value > 0; +} + +function isPreferenceReference(value) { + if (isPositiveInteger(value)) return true; + if (value && typeof value === 'object' && !Array.isArray(value)) { + const hasId = Object.prototype.hasOwnProperty.call(value, 'id'); + const hasReferenceId = Object.prototype.hasOwnProperty.call(value, 'referenceId'); + if (!hasId && !hasReferenceId) return false; + if (hasId && !isPositiveInteger(value.id)) return false; + if (hasReferenceId && !isPositiveInteger(value.referenceId)) return false; + return true; + } + return false; +} + // ───────────────────────────────────────────────────────────────────────────── -// Shared: flat food-preference ID arrays +// Flat food-preference ID arrays (integers only) // ───────────────────────────────────────────────────────────────────────────── function buildIntegerArrayRule(field, required) { const chain = body(field); - if (required) { chain.exists({ checkNull: true }).withMessage(`${field} is required`).bail(); } else { chain.optional(); } - return chain.custom(isArrayOfIntegers).withMessage(`${field} must be an array of integers`); } @@ -44,14 +59,44 @@ const optionalFoodPreferenceRules = [ ]; // ───────────────────────────────────────────────────────────────────────────── -// Shared: health_context structured object +// Nested food_preferences object +// Accepts positive integers OR { id: positiveInt } OR { referenceId: positiveInt } +// ───────────────────────────────────────────────────────────────────────────── +function buildPreferenceReferenceArrayRule(field) { + return body(field) + .optional() + .custom((value) => { + if (!Array.isArray(value)) throw new Error(`${field} must be an array`); + if (!value.every(isPreferenceReference)) { + throw new Error( + `${field} must be an array of positive integer IDs or objects with positive integer id/referenceId` + ); + } + return true; + }); +} + +const nestedFoodPreferenceRules = [ + body('food_preferences') + .optional() + .isObject().withMessage('food_preferences must be an object'), + buildPreferenceReferenceArrayRule('food_preferences.dietary_requirements'), + buildPreferenceReferenceArrayRule('food_preferences.allergies'), + buildPreferenceReferenceArrayRule('food_preferences.cuisines'), + buildPreferenceReferenceArrayRule('food_preferences.dislikes'), + buildPreferenceReferenceArrayRule('food_preferences.health_conditions'), + buildPreferenceReferenceArrayRule('food_preferences.spice_levels'), + buildPreferenceReferenceArrayRule('food_preferences.cooking_methods'), +]; + +// ───────────────────────────────────────────────────────────────────────────── +// health_context structured object // ───────────────────────────────────────────────────────────────────────────── const healthContextRules = [ body('health_context') .optional() .isObject().withMessage('health_context must be an object'), - // allergies[] body('health_context.allergies') .optional() .isArray().withMessage('health_context.allergies must be an array'), @@ -71,7 +116,6 @@ const healthContextRules = [ .isString().withMessage('allergy notes must be a string') .isLength({ max: 1000 }).withMessage('allergy notes must be 1000 characters or fewer'), - // chronic_conditions[] body('health_context.chronic_conditions') .optional() .isArray().withMessage('health_context.chronic_conditions must be an array'), @@ -91,47 +135,34 @@ const healthContextRules = [ .isString().withMessage('condition notes must be a string') .isLength({ max: 1000 }).withMessage('condition notes must be 1000 characters or fewer'), - // medications[] body('health_context.medications') .optional() .isArray().withMessage('health_context.medications must be an array'), body('health_context.medications.*.name') - .if(body('health_context.medications').exists()) - .notEmpty().withMessage('medication name is required') - .isString().withMessage('medication name must be a string') + .optional() + .isString().notEmpty().withMessage('medication name must be a non-empty string') .isLength({ max: 200 }).withMessage('medication name must be 200 characters or fewer'), body('health_context.medications.*.dosage') - .optional() + .optional({ nullable: true }) .isObject().withMessage('medication dosage must be an object'), body('health_context.medications.*.dosage.amount') .optional({ nullable: true }) - .isString().withMessage('dosage amount must be a string') - .isLength({ max: 50 }).withMessage('dosage amount must be 50 characters or fewer'), + .isString().withMessage('dosage amount must be a string'), body('health_context.medications.*.dosage.unit') .optional({ nullable: true }) - .isString().withMessage('dosage unit must be a string') - .isLength({ max: 50 }).withMessage('dosage unit must be 50 characters or fewer'), + .isString().withMessage('dosage unit must be a string'), body('health_context.medications.*.frequency') - .optional() + .optional({ nullable: true }) .isObject().withMessage('medication frequency must be an object'), body('health_context.medications.*.frequency.timesPerDay') .optional({ nullable: true }) - .isInt({ min: 1, max: 24 }) - .withMessage('frequency timesPerDay must be an integer between 1 and 24'), - body('health_context.medications.*.frequency.interval') - .optional({ nullable: true }) - .isString().withMessage('frequency interval must be a string') - .isLength({ max: 100 }).withMessage('frequency interval must be 100 characters or fewer'), + .isInt({ min: 1, max: 24 }).withMessage('timesPerDay must be an integer between 1 and 24'), body('health_context.medications.*.frequency.schedule') - .optional() + .optional({ nullable: true }) .isArray().withMessage('frequency schedule must be an array'), - body('health_context.medications.*.frequency.schedule.*') - .optional() - .isString().withMessage('each schedule entry must be a string') - .isLength({ max: 50 }).withMessage('each schedule entry must be 50 characters or fewer'), body('health_context.medications.*.frequency.asNeeded') - .optional() - .isBoolean().withMessage('frequency asNeeded must be a boolean'), + .optional({ nullable: true }) + .isBoolean().withMessage('asNeeded must be a boolean'), body('health_context.medications.*.purpose') .optional({ nullable: true }) .isString().withMessage('medication purpose must be a string') @@ -141,57 +172,34 @@ const healthContextRules = [ .isString().withMessage('medication notes must be a string') .isLength({ max: 1000 }).withMessage('medication notes must be 1000 characters or fewer'), body('health_context.medications.*.active') - .optional() + .optional({ nullable: true }) .isBoolean().withMessage('medication active must be a boolean'), ]; // ───────────────────────────────────────────────────────────────────────────── -// Shared: ui_settings +// ui_settings // ───────────────────────────────────────────────────────────────────────────── const uiSettingsRules = [ body('ui_settings') .optional() .isObject().withMessage('ui_settings must be an object'), - body('ui_settings.language') - .optional() - .isIn(UI_LANGUAGES) - .withMessage(`ui_settings.language must be one of: ${UI_LANGUAGES.join(', ')}`), body('ui_settings.theme') .optional() - .isIn(UI_THEMES) - .withMessage(`ui_settings.theme must be one of: ${UI_THEMES.join(', ')}`), + .isIn(UI_THEMES).withMessage(`ui_settings.theme must be one of: ${UI_THEMES.join(', ')}`), + body('ui_settings.language') + .optional() + .isIn(UI_LANGUAGES).withMessage(`ui_settings.language must be one of: ${UI_LANGUAGES.join(', ')}`), body('ui_settings.font_size') .optional() - .isString().withMessage('ui_settings.font_size must be a string') - .matches(/^\d+(px|rem|em|%)$/) - .withMessage('ui_settings.font_size must be a valid CSS size (e.g. 16px, 1rem)'), -]; - -// ───────────────────────────────────────────────────────────────────────────── -// validateUserPreferences -// POST /api/user/preferences — flat food-preference ID arrays + health_context -// ───────────────────────────────────────────────────────────────────────────── -exports.validateUserPreferences = [ - ...requiredFoodPreferenceRules, + .matches(/^\d+px$/).withMessage('ui_settings.font_size must be in the format "16px"'), ]; // ───────────────────────────────────────────────────────────────────────────── -// validateHealthContext -// PUT /api/user/preferences/extended — full structured health-context update +// notification_preferences // ───────────────────────────────────────────────────────────────────────────── -exports.validateHealthContext = [ - ...healthContextRules, - ...optionalFoodPreferenceRules, - ...uiSettingsRules, -]; - -// ───────────────────────────────────────────────────────────────────────────── -// validateNotificationPreferences -// PUT /api/user/preferences/extended/notifications -// ───────────────────────────────────────────────────────────────────────────── -exports.validateNotificationPreferences = [ +const notificationPreferencesRules = [ body('notification_preferences') - .notEmpty().withMessage('notification_preferences object is required') + .exists({ checkNull: true }).withMessage('notification_preferences is required') .isObject().withMessage('notification_preferences must be an object'), body('notification_preferences.mealReminders') .optional() @@ -209,3 +217,59 @@ exports.validateNotificationPreferences = [ .optional() .isBoolean().withMessage('systemUpdates must be a boolean'), ]; + +// ───────────────────────────────────────────────────────────────────────────── +// Extended preferences — at least one section required +// ───────────────────────────────────────────────────────────────────────────── +const EXTENDED_SECTIONS = [ + 'health_context', + 'food_preferences', + 'notification_preferences', + 'ui_settings', + 'dietary_requirements', + 'allergies', + 'cuisines', + 'dislikes', + 'health_conditions', + 'spice_levels', + 'cooking_methods', +]; + +function atLeastOneSectionRequired(req, res, next) { + const hasAny = EXTENDED_SECTIONS.some((key) => + Object.prototype.hasOwnProperty.call(req.body, key) + ); + if (!hasAny) { + return res.status(400).json({ + success: false, + errors: [{ msg: 'At least one preference section must be provided' }], + }); + } + return next(); +} + +const validateExtendedUserPreferences = [ + atLeastOneSectionRequired, + ...healthContextRules, + ...nestedFoodPreferenceRules, + ...uiSettingsRules, +]; + +const validateUiSettings = [ + ...uiSettingsRules, +]; + +const validateNotificationPreferences = [ + ...notificationPreferencesRules, +]; + +const validateUserPreferences = requiredFoodPreferenceRules; +const validateOptionalUserPreferences = optionalFoodPreferenceRules; + +module.exports = { + validateUserPreferences, + validateOptionalUserPreferences, + validateExtendedUserPreferences, + validateUiSettings, + validateNotificationPreferences, +};