diff --git a/modules/audit/middlewares/audit.middleware.js b/modules/audit/middlewares/audit.middleware.js index 59181ccc2..948a9e9bd 100644 --- a/modules/audit/middlewares/audit.middleware.js +++ b/modules/audit/middlewares/audit.middleware.js @@ -3,6 +3,7 @@ */ import AuditService from '../services/audit.service.js'; import logger from '../../../lib/services/logger.js'; +import config from '../../../config/index.js'; /** * Default route prefixes to skip when auto-capturing audit events. @@ -124,7 +125,10 @@ const createAuditMiddleware = (options = {}) => { AuditService.log({ action, - req, + userId: req.user?._id || req.user?.id, + organizationId: req.organization?._id || req.organization?.id, + ip: config.audit?.captureIp !== false ? (req.ip || req.connection?.remoteAddress || '') : undefined, + userAgent: config.audit?.captureUserAgent !== false ? (req.headers?.['user-agent'] || '') : undefined, targetType, targetId, }).catch((err) => logger.error('audit.middleware: audit log write failed', { message: err?.message, stack: err?.stack })); diff --git a/modules/audit/services/audit.service.js b/modules/audit/services/audit.service.js index d2153aa32..208570e0d 100644 --- a/modules/audit/services/audit.service.js +++ b/modules/audit/services/audit.service.js @@ -10,13 +10,16 @@ import AuditRepository from '../repositories/audit.repository.js'; * @description Record an audit log entry. No-op when audit is disabled. * @param {Object} params * @param {string} params.action - Action identifier (e.g. 'auth.login', 'billing.subscribe') - * @param {Object} [params.req] - Express request object (extracts userId, orgId, ip, userAgent) + * @param {string} [params.userId] - ID of the acting user + * @param {string} [params.organizationId] - ID of the organization context + * @param {string} [params.ip] - IP address of the request + * @param {string} [params.userAgent] - User-agent of the request * @param {string} [params.targetType] - Type of the target entity * @param {string} [params.targetId] - ID of the target entity * @param {Object} [params.metadata] - Additional metadata * @returns {Promise} The created audit log entry or null if disabled */ -const log = async ({ action, req, targetType, targetId, metadata } = {}) => { +const log = async ({ action, userId, organizationId, ip, userAgent, targetType, targetId, metadata } = {}) => { if (!config.audit?.enabled) return null; if (!action) return null; @@ -27,15 +30,10 @@ const log = async ({ action, req, targetType, targetId, metadata } = {}) => { metadata: metadata || {}, }; - // Extract context from request if available (coerce ObjectIds to strings) - if (req) { - const uid = req.user?._id || req.user?.id; - const oid = req.organization?._id || req.organization?.id; - if (uid) entry.userId = String(uid); - if (oid) entry.orgId = String(oid); - entry.ip = config.audit?.captureIp !== false ? (req.ip || req.connection?.remoteAddress || '') : ''; - entry.userAgent = config.audit?.captureUserAgent !== false ? (req.headers?.['user-agent'] || '') : ''; - } + if (userId) entry.userId = String(userId); + if (organizationId) entry.orgId = String(organizationId); + if (ip !== undefined) entry.ip = ip || ''; + if (userAgent !== undefined) entry.userAgent = userAgent || ''; try { return await AuditRepository.create(entry); diff --git a/modules/audit/tests/audit.integration.tests.js b/modules/audit/tests/audit.integration.tests.js index f4e8f88b8..e781ce6fc 100644 --- a/modules/audit/tests/audit.integration.tests.js +++ b/modules/audit/tests/audit.integration.tests.js @@ -68,13 +68,15 @@ describe('Audit integration tests:', () => { // Create some audit entries await AuditService.log({ action: 'auth.login', - req: { user: { _id: adminUser.id }, headers: { 'user-agent': 'test' } }, + userId: adminUser.id, + userAgent: 'test', targetType: 'User', targetId: adminUser.id, }); await AuditService.log({ action: 'auth.signup', - req: { user: { _id: adminUser.id }, headers: { 'user-agent': 'test' } }, + userId: adminUser.id, + userAgent: 'test', targetType: 'User', targetId: adminUser.id, }); diff --git a/modules/audit/tests/audit.middleware.unit.tests.js b/modules/audit/tests/audit.middleware.unit.tests.js index e6e22b825..0a17bf8c2 100644 --- a/modules/audit/tests/audit.middleware.unit.tests.js +++ b/modules/audit/tests/audit.middleware.unit.tests.js @@ -148,7 +148,11 @@ describe('Audit middleware unit tests:', () => { const call = mockLog.mock.calls[0][0]; expect(call.action).toBe('auth.signin'); expect(call.targetType).toBe('User'); - expect(call.req).toBe(req); + expect(call.req).toBeUndefined(); + expect(call.userId).toBe('507f1f77bcf86cd799439011'); + expect(call.organizationId).toBe('507f1f77bcf86cd799439012'); + expect(call.ip).toBe('127.0.0.1'); + expect(call.userAgent).toBe('TestAgent/1.0'); }); test('should log PUT mutations', () => { diff --git a/modules/audit/tests/audit.service.unit.tests.js b/modules/audit/tests/audit.service.unit.tests.js index 63019ba04..56e2c915e 100644 --- a/modules/audit/tests/audit.service.unit.tests.js +++ b/modules/audit/tests/audit.service.unit.tests.js @@ -54,16 +54,13 @@ describe('AuditService unit tests:', () => { test('should log an audit entry with request context', async () => { const userId = '507f1f77bcf86cd799439011'; const orgId = '507f1f77bcf86cd799439012'; - const req = { - user: { _id: userId }, - organization: { _id: orgId }, - ip: '127.0.0.1', - headers: { 'user-agent': 'TestAgent/1.0' }, - }; await AuditService.log({ action: 'auth.login', - req, + userId, + organizationId: orgId, + ip: '127.0.0.1', + userAgent: 'TestAgent/1.0', targetType: 'User', targetId: userId, metadata: { foo: 'bar' }, @@ -115,47 +112,37 @@ describe('AuditService unit tests:', () => { expect(mockList).toHaveBeenCalledWith({ userId }, 1, 10); }); - // GDPR config flag tests - test('should capture IP when captureIp is true (default)', async () => { - mockConfig.audit = { enabled: true, ttlDays: 90, captureIp: true, captureUserAgent: true }; - const req = { ip: '10.0.0.1', headers: { 'user-agent': 'Bot/1.0' } }; - await AuditService.log({ action: 'test.ip', req }); + // GDPR config flag tests — ip/userAgent are now extracted by the caller + test('should store ip when provided', async () => { + await AuditService.log({ action: 'test.ip', ip: '10.0.0.1', userAgent: 'Bot/1.0' }); expect(mockCreate).toHaveBeenCalledTimes(1); const arg = mockCreate.mock.calls[0][0]; expect(arg.ip).toBe('10.0.0.1'); }); - test('should set IP to empty string when captureIp is false', async () => { - mockConfig.audit = { enabled: true, ttlDays: 90, captureIp: false, captureUserAgent: true }; - const req = { ip: '10.0.0.1', headers: { 'user-agent': 'Bot/1.0' } }; - await AuditService.log({ action: 'test.ip', req }); + test('should omit ip when undefined is passed', async () => { + await AuditService.log({ action: 'test.ip', ip: undefined, userAgent: 'Bot/1.0' }); expect(mockCreate).toHaveBeenCalledTimes(1); const arg = mockCreate.mock.calls[0][0]; - expect(arg.ip).toBe(''); + expect(arg.ip).toBeUndefined(); }); - test('should capture User-Agent when captureUserAgent is true (default)', async () => { - mockConfig.audit = { enabled: true, ttlDays: 90, captureIp: true, captureUserAgent: true }; - const req = { ip: '10.0.0.1', headers: { 'user-agent': 'Bot/1.0' } }; - await AuditService.log({ action: 'test.ua', req }); + test('should store userAgent when provided', async () => { + await AuditService.log({ action: 'test.ua', ip: '10.0.0.1', userAgent: 'Bot/1.0' }); expect(mockCreate).toHaveBeenCalledTimes(1); const arg = mockCreate.mock.calls[0][0]; expect(arg.userAgent).toBe('Bot/1.0'); }); - test('should set User-Agent to empty string when captureUserAgent is false', async () => { - mockConfig.audit = { enabled: true, ttlDays: 90, captureIp: true, captureUserAgent: false }; - const req = { ip: '10.0.0.1', headers: { 'user-agent': 'Bot/1.0' } }; - await AuditService.log({ action: 'test.ua', req }); + test('should omit userAgent when undefined is passed', async () => { + await AuditService.log({ action: 'test.ua', ip: '10.0.0.1', userAgent: undefined }); expect(mockCreate).toHaveBeenCalledTimes(1); const arg = mockCreate.mock.calls[0][0]; - expect(arg.userAgent).toBe(''); + expect(arg.userAgent).toBeUndefined(); }); - test('should default to capturing IP and User-Agent when config keys are undefined', async () => { - mockConfig.audit = { enabled: true, ttlDays: 90 }; - const req = { ip: '192.168.1.1', headers: { 'user-agent': 'DefaultBot/2.0' } }; - await AuditService.log({ action: 'test.defaults', req }); + test('should store ip and userAgent when provided', async () => { + await AuditService.log({ action: 'test.defaults', ip: '192.168.1.1', userAgent: 'DefaultBot/2.0' }); expect(mockCreate).toHaveBeenCalledTimes(1); const arg = mockCreate.mock.calls[0][0]; expect(arg.ip).toBe('192.168.1.1'); diff --git a/modules/billing/routes/billing.routes.js b/modules/billing/routes/billing.routes.js index bd22375f1..d841f6c40 100644 --- a/modules/billing/routes/billing.routes.js +++ b/modules/billing/routes/billing.routes.js @@ -5,7 +5,7 @@ import passport from 'passport'; import model from '../../../lib/middlewares/model.js'; import policy from '../../../lib/middlewares/policy.js'; -import organization from '../../organizations/middleware/organizations.middleware.js'; +import organization from '../../organizations/middlewares/organizations.middleware.js'; import billingSchema from '../models/billing.subscription.schema.js'; import billingPlans from '../controllers/billing.plans.controller.js'; import billing from '../controllers/billing.controller.js'; diff --git a/modules/home/services/home.service.js b/modules/home/services/home.service.js index 61f638079..6271405d5 100644 --- a/modules/home/services/home.service.js +++ b/modules/home/services/home.service.js @@ -8,10 +8,10 @@ import { Base64 } from 'js-base64'; import { promises as fs } from 'fs'; import mongoose from 'mongoose'; -import AuthService from '../../auth/services/auth.service.js'; import config from '../../../config/index.js'; import mailer from '../../../lib/helpers/mailer/index.js'; import HomeRepository from '../repositories/home.repository.js'; +import { removeSensitive } from '../../users/utils/sanitizeUser.js'; /** * @desc Check whether a config value is meaningfully set (non-empty, not a DEVKIT placeholder). @@ -21,19 +21,20 @@ import HomeRepository from '../repositories/home.repository.js'; const isSet = (value) => !!(value && typeof value === 'string' && value.trim() !== '' && !value.startsWith('DEVKIT_NODE_')); /** - * @desc Function to get all admin users in db - * @return {Promise} All users + * @desc Function to get page content from markdown file + * @param {string} name - The name of the markdown file + * @returns {Promise} Page content array */ const page = async (name) => { const markdown = await fs.readFile(path.resolve(`./config/markdown/${name}.md`), 'utf8'); const test = await fs.stat(path.resolve(`./config/markdown/${name}.md`)); - return Promise.resolve([ + return [ { title: _.startCase(name), updatedAt: test.mtime, markdown, }, - ]); + ]; }; /** @@ -87,11 +88,11 @@ const changelogs = async () => { /** * @desc Function to get all admin users in db - * @return {Promise} All users + * @returns {Promise} All users (sanitized) */ const team = async () => { const result = await HomeRepository.team(); - return Promise.resolve(result.map((user) => AuthService.removeSensitive(user))); + return result.map((user) => removeSensitive(user)); }; /** diff --git a/modules/organizations/controllers/organizations.membership.controller.js b/modules/organizations/controllers/organizations.membership.controller.js index d4d6fbdae..6ce4c548b 100644 --- a/modules/organizations/controllers/organizations.membership.controller.js +++ b/modules/organizations/controllers/organizations.membership.controller.js @@ -4,6 +4,7 @@ import errors from '../../../lib/helpers/errors.js'; import responses from '../../../lib/helpers/responses.js'; import MembershipService from '../services/organizations.membership.service.js'; +import { MEMBERSHIP_ROLES } from '../lib/constants.js'; /** * @function list @@ -34,7 +35,7 @@ const list = async (req, res) => { const updateRole = async (req, res) => { try { // Belt-and-suspenders: only owners can change roles (CASL blocks admins via no 'update Membership') - if (req.membership && req.membership.role !== 'owner') { + if (!req.membership || req.membership.role !== MEMBERSHIP_ROLES.OWNER) { return responses.error(res, 403, 'Forbidden', 'Only owners can change member roles')(); } const membership = await MembershipService.updateRole(req.membershipDoc, req.body.role); @@ -53,9 +54,13 @@ const updateRole = async (req, res) => { */ const remove = async (req, res) => { try { - // Admins can only remove members, not other admins or owners - if (req.membership && req.membership.role !== 'owner' && req.membershipDoc.role !== 'member') { - return responses.error(res, 403, 'Forbidden', 'Only owners can remove admins or other owners')(); + // Only owners can remove anyone; admins can only remove members + const actorRole = req.membership?.role; + const targetRole = req.membershipDoc.role; + const canRemove = actorRole === MEMBERSHIP_ROLES.OWNER + || (actorRole === MEMBERSHIP_ROLES.ADMIN && targetRole === MEMBERSHIP_ROLES.MEMBER); + if (!canRemove) { + return responses.error(res, 403, 'Forbidden', 'Insufficient permissions to remove this member')(); } const result = await MembershipService.remove(req.membershipDoc); responses.success(res, 'membership deleted')({ id: req.membershipDoc.id, ...result }); diff --git a/modules/organizations/controllers/organizations.membershipRequest.controller.js b/modules/organizations/controllers/organizations.membershipRequest.controller.js index 9e3a91138..53f57329e 100644 --- a/modules/organizations/controllers/organizations.membershipRequest.controller.js +++ b/modules/organizations/controllers/organizations.membershipRequest.controller.js @@ -4,6 +4,7 @@ import errors from '../../../lib/helpers/errors.js'; import responses from '../../../lib/helpers/responses.js'; import MembershipService from '../services/organizations.membership.service.js'; +import { MEMBERSHIP_ROLES, MEMBERSHIP_STATUSES } from '../lib/constants.js'; /** * @function create @@ -33,7 +34,7 @@ const create = async (req, res) => { */ const listPending = async (req, res) => { try { - if (!req.membership || req.membership.role === 'member') { + if (!req.membership || req.membership.role === MEMBERSHIP_ROLES.MEMBER) { return responses.success(res, 'membership request list')([]); } const requests = await MembershipService.listPending(req.organization._id || req.organization.id); @@ -165,7 +166,7 @@ const requestByID = async (req, res, next, id) => { const membership = await MembershipService.get(id); const organizationId = String(req.organization._id || req.organization.id); const membershipOrgId = String(membership?.organizationId?._id || membership?.organizationId); - if (!membership || membership.status !== 'pending' || membershipOrgId !== organizationId) { + if (!membership || membership.status !== MEMBERSHIP_STATUSES.PENDING || membershipOrgId !== organizationId) { return responses.error(res, 404, 'Not Found', 'No pending request with that identifier has been found')(); } req.membershipRequest = membership; diff --git a/modules/organizations/lib/constants.js b/modules/organizations/lib/constants.js new file mode 100644 index 000000000..a65a1c6e8 --- /dev/null +++ b/modules/organizations/lib/constants.js @@ -0,0 +1,12 @@ +export const MEMBERSHIP_STATUSES = { + ACTIVE: 'active', + PENDING: 'pending', + INVITED: 'invited', + REJECTED: 'rejected', +}; + +export const MEMBERSHIP_ROLES = { + OWNER: 'owner', + ADMIN: 'admin', + MEMBER: 'member', +}; diff --git a/modules/organizations/middleware/organizations.middleware.js b/modules/organizations/middlewares/organizations.middleware.js similarity index 93% rename from modules/organizations/middleware/organizations.middleware.js rename to modules/organizations/middlewares/organizations.middleware.js index 471414bca..77198a326 100644 --- a/modules/organizations/middleware/organizations.middleware.js +++ b/modules/organizations/middlewares/organizations.middleware.js @@ -3,8 +3,8 @@ */ import OrganizationsCrudService from '../services/organizations.crud.service.js'; import MembershipService from '../services/organizations.membership.service.js'; - import responses from '../../../lib/helpers/responses.js'; +import { MEMBERSHIP_ROLES } from '../lib/constants.js'; /** * Middleware that resolves the current organization from a route param or @@ -37,7 +37,7 @@ async function resolveOrganization(req, res, next) { // Platform admin bypasses membership requirement if (req.user && req.user.roles && req.user.roles.includes('admin')) { - req.membership = { role: 'owner', organizationId: organization._id }; + req.membership = { role: MEMBERSHIP_ROLES.OWNER, organizationId: organization._id }; return next(); } diff --git a/modules/organizations/policies/organizations.policy.js b/modules/organizations/policies/organizations.policy.js index 386fe497b..08c45f188 100644 --- a/modules/organizations/policies/organizations.policy.js +++ b/modules/organizations/policies/organizations.policy.js @@ -1,6 +1,7 @@ /** * Organization ability definitions for CASL document-level authorization. */ +import { MEMBERSHIP_ROLES } from '../lib/constants.js'; /** * Register organization-related subjects for document-level and path-level resolution. @@ -50,11 +51,11 @@ export function organizationAbilities(user, membership, { can, cannot }) { if (!membership) return; switch (membership.role) { - case 'owner': + case MEMBERSHIP_ROLES.OWNER: can('manage', 'Organization', { _id: String(membership.organizationId._id || membership.organizationId) }); can('manage', 'Membership', { organizationId: String(membership.organizationId._id || membership.organizationId) }); break; - case 'admin': + case MEMBERSHIP_ROLES.ADMIN: can('read', 'Organization', { _id: String(membership.organizationId._id || membership.organizationId) }); can('update', 'Organization', { _id: String(membership.organizationId._id || membership.organizationId) }); cannot('delete', 'Organization'); @@ -62,7 +63,7 @@ export function organizationAbilities(user, membership, { can, cannot }) { can('create', 'Membership', { organizationId: String(membership.organizationId._id || membership.organizationId) }); can('delete', 'Membership', { organizationId: String(membership.organizationId._id || membership.organizationId) }); break; - case 'member': + case MEMBERSHIP_ROLES.MEMBER: can('read', 'Organization', { _id: String(membership.organizationId._id || membership.organizationId) }); can('read', 'Membership', { organizationId: String(membership.organizationId._id || membership.organizationId) }); break; diff --git a/modules/organizations/repositories/organizations.membership.repository.js b/modules/organizations/repositories/organizations.membership.repository.js index 4ef1b3f9a..96644e15b 100644 --- a/modules/organizations/repositories/organizations.membership.repository.js +++ b/modules/organizations/repositories/organizations.membership.repository.js @@ -2,6 +2,7 @@ * Module dependencies */ import mongoose from 'mongoose'; +import { MEMBERSHIP_STATUSES } from '../lib/constants.js'; const Membership = mongoose.model('Membership'); @@ -98,7 +99,7 @@ const deleteMany = (filter) => { */ const aggregateCountByOrganizations = (orgIds) => Membership.aggregate([ - { $match: { organizationId: { $in: orgIds }, status: 'active' } }, + { $match: { organizationId: { $in: orgIds }, status: MEMBERSHIP_STATUSES.ACTIVE } }, { $group: { _id: '$organizationId', count: { $sum: 1 } } }, ]); @@ -109,7 +110,7 @@ const aggregateCountByOrganizations = (orgIds) => * @returns {Promise} An array of memberships. */ const listByUsers = (userIds) => - Membership.find({ userId: { $in: userIds }, status: 'active' }).populate(defaultPopulate).sort('-createdAt').exec(); + Membership.find({ userId: { $in: userIds }, status: MEMBERSHIP_STATUSES.ACTIVE }).populate(defaultPopulate).sort('-createdAt').exec(); export default { list, diff --git a/modules/organizations/services/organizations.crud.service.js b/modules/organizations/services/organizations.crud.service.js index 057965385..377363a8e 100644 --- a/modules/organizations/services/organizations.crud.service.js +++ b/modules/organizations/services/organizations.crud.service.js @@ -24,6 +24,7 @@ import OrganizationsRepository from '../repositories/organizations.repository.js import MembershipRepository from '../repositories/organizations.membership.repository.js'; import UserService from '../../users/services/users.service.js'; import { slugify } from '../helpers/organizations.slug.js'; +import { MEMBERSHIP_STATUSES, MEMBERSHIP_ROLES } from '../lib/constants.js'; /** * @function list @@ -48,7 +49,7 @@ const list = async (search, page, perPage) => { * @returns {Promise} A promise that resolves to the list of organizations. */ const listByUser = async (user) => { - const memberships = await MembershipRepository.list({ userId: user._id || user.id, status: 'active' }); + const memberships = await MembershipRepository.list({ userId: user._id || user.id, status: MEMBERSHIP_STATUSES.ACTIVE }); const organizationIds = memberships.map((m) => m.organizationId._id || m.organizationId); const orgs = await OrganizationsRepository.list({ _id: { $in: organizationIds } }); return orgs.map((org) => { @@ -107,7 +108,7 @@ const create = async (body, user) => { membership = await MembershipRepository.create({ userId: user.id || user._id, organizationId: result._id, - role: 'owner', + role: MEMBERSHIP_ROLES.OWNER, }); await UserService.updateById(user.id || user._id, { currentOrganization: result._id }); @@ -175,7 +176,7 @@ const remove = async (organization) => { // For each affected user, switch to their next available org or set null await Promise.all(affectedUsers.map(async (u) => { - const remaining = await MembershipRepository.list({ userId: u._id, status: 'active' }); + const remaining = await MembershipRepository.list({ userId: u._id, status: MEMBERSHIP_STATUSES.ACTIVE }); const nextOrg = remaining.length > 0 ? (remaining[0].organizationId._id || remaining[0].organizationId) : null; @@ -212,7 +213,7 @@ const switchOrganization = async (user, organizationId) => { const membership = await MembershipRepository.findOne({ userId: user._id || user.id, organizationId, - status: 'active', + status: MEMBERSHIP_STATUSES.ACTIVE, }); if (!membership) { @@ -241,13 +242,13 @@ const autoSetCurrentOrganization = async (user) => { const stillActive = await MembershipRepository.findOne({ userId: user._id || user.id, organizationId: user.currentOrganization._id || user.currentOrganization, - status: 'active', + status: MEMBERSHIP_STATUSES.ACTIVE, }); if (stillActive) return user; // Membership gone — clear stale reference and fall through to find another user.currentOrganization = null; } - const memberships = await MembershipRepository.list({ userId: user._id || user.id, status: 'active' }); + const memberships = await MembershipRepository.list({ userId: user._id || user.id, status: MEMBERSHIP_STATUSES.ACTIVE }); if (memberships.length > 0) { const orgId = memberships[0].organizationId._id || memberships[0].organizationId; await UserService.updateById(user._id || user.id, { currentOrganization: orgId }); diff --git a/modules/organizations/services/organizations.membership.service.js b/modules/organizations/services/organizations.membership.service.js index 597bff35d..e7b4ecb6e 100644 --- a/modules/organizations/services/organizations.membership.service.js +++ b/modules/organizations/services/organizations.membership.service.js @@ -11,6 +11,25 @@ import { assertEmailVerified } from '../../../lib/helpers/emailVerification.js'; import MembershipRepository from '../repositories/organizations.membership.repository.js'; import OrganizationRepository from '../repositories/organizations.repository.js'; import UserService from '../../users/services/users.service.js'; +import { MEMBERSHIP_STATUSES, MEMBERSHIP_ROLES } from '../lib/constants.js'; + +/** + * @function validateLastOwnerProtection + * @description Throws if there is only one active owner left in the organization. + * @param {String} organizationId - The ID of the organization to check. + * @returns {Promise} + * @throws {Error} If the organization has only one active owner. + */ +const validateLastOwnerProtection = async (organizationId) => { + const ownerCount = await MembershipRepository.count({ + organizationId, + role: MEMBERSHIP_ROLES.OWNER, + status: MEMBERSHIP_STATUSES.ACTIVE, + }); + if (ownerCount <= 1) { + throw new Error('Organization must have at least one active owner'); + } +}; /** * @function list @@ -22,7 +41,7 @@ import UserService from '../../users/services/users.service.js'; * @returns {Promise} A promise that resolves to the list of memberships. */ const list = async (organizationId, search, page, perPage) => { - const filter = { organizationId, status: 'active' }; + const filter = { organizationId, status: MEMBERSHIP_STATUSES.ACTIVE }; if (search) { const matchingUsers = await UserService.searchByNameOrEmail(search); filter.userId = { $in: matchingUsers.map((u) => u._id) }; @@ -36,7 +55,7 @@ const list = async (organizationId, search, page, perPage) => { * @param {String} userId - The ID of the user. * @returns {Promise} A promise that resolves to the list of memberships. */ -const listByUser = (userId) => MembershipRepository.list({ userId, status: 'active' }); +const listByUser = (userId) => MembershipRepository.list({ userId, status: MEMBERSHIP_STATUSES.ACTIVE }); /** * @function get @@ -54,7 +73,7 @@ const get = (id) => MembershipRepository.get(id); * @returns {Promise} A promise resolving to the membership or null. */ const findByUserAndOrganization = (userId, organizationId) => - MembershipRepository.findOne({ userId, organizationId, status: 'active' }); + MembershipRepository.findOne({ userId, organizationId, status: MEMBERSHIP_STATUSES.ACTIVE }); /** * @function create @@ -72,10 +91,9 @@ const create = (data) => MembershipRepository.create(data); * @returns {Promise} A promise resolving to the updated membership. */ const updateRole = async (membership, role) => { - if (membership.role === 'owner' && role !== 'owner') { + if (membership.role === MEMBERSHIP_ROLES.OWNER && role !== MEMBERSHIP_ROLES.OWNER) { const orgId = membership.organizationId._id || membership.organizationId; - const ownerCount = await MembershipRepository.count({ organizationId: orgId, role: 'owner', status: 'active' }); - if (ownerCount <= 1) throw new Error('Cannot change role of the last owner'); + await validateLastOwnerProtection(orgId); } membership.role = role; return MembershipRepository.update(membership); @@ -88,10 +106,9 @@ const updateRole = async (membership, role) => { * @returns {Promise} A promise resolving to a confirmation of the deletion. */ const remove = async (membership) => { - if (membership.role === 'owner') { + if (membership.role === MEMBERSHIP_ROLES.OWNER) { const orgId = membership.organizationId._id || membership.organizationId; - const ownerCount = await MembershipRepository.count({ organizationId: orgId, role: 'owner', status: 'active' }); - if (ownerCount <= 1) throw new Error('Cannot remove the last owner of an organization'); + await validateLastOwnerProtection(orgId); } const userId = membership.userId._id || membership.userId; const removedOrgId = membership.organizationId._id || membership.organizationId; @@ -100,7 +117,7 @@ const remove = async (membership) => { // Clear currentOrganization if it pointed to the org the user was removed from const userDoc = await UserService.getBrut({ id: String(userId) }); if (userDoc && String(userDoc.currentOrganization) === String(removedOrgId)) { - const remaining = await MembershipRepository.list({ userId, status: 'active' }); + const remaining = await MembershipRepository.list({ userId, status: MEMBERSHIP_STATUSES.ACTIVE }); const nextOrg = remaining.length > 0 ? (remaining[0].organizationId._id || remaining[0].organizationId) : null; await UserService.updateById(userDoc._id, { currentOrganization: nextOrg }); } @@ -113,7 +130,7 @@ const remove = async (membership) => { * @param {String} organizationId - The ID of the organization. * @returns {Promise} A promise that resolves to the list of pending memberships. */ -const listPending = (organizationId) => MembershipRepository.list({ organizationId, status: 'pending' }); +const listPending = (organizationId) => MembershipRepository.list({ organizationId, status: MEMBERSHIP_STATUSES.PENDING }); /** * @function listPendingByUser @@ -121,7 +138,7 @@ const listPending = (organizationId) => MembershipRepository.list({ organization * @param {String} userId - The ID of the user. * @returns {Promise} A promise that resolves to the list of pending memberships. */ -const listPendingByUser = (userId) => MembershipRepository.list({ userId, status: 'pending' }); +const listPendingByUser = (userId) => MembershipRepository.list({ userId, status: MEMBERSHIP_STATUSES.PENDING }); /** * @function createJoinRequest @@ -138,20 +155,20 @@ const createJoinRequest = async (userId, organizationId) => { if (!user) throw new Error('User not found'); assertEmailVerified(user); - const existing = await MembershipRepository.findOne({ userId, organizationId, status: { $in: ['active', 'pending'] } }); + const existing = await MembershipRepository.findOne({ userId, organizationId, status: { $in: [MEMBERSHIP_STATUSES.ACTIVE, MEMBERSHIP_STATUSES.PENDING] } }); if (existing) { - if (existing.status === 'active') throw new Error('Already a member of this organization'); + if (existing.status === MEMBERSHIP_STATUSES.ACTIVE) throw new Error('Already a member of this organization'); throw new Error('A pending request already exists'); } // Limit to 1 pending request at a time across all organizations - const pendingAnywhere = await MembershipRepository.findOne({ userId, status: 'pending' }); + const pendingAnywhere = await MembershipRepository.findOne({ userId, status: MEMBERSHIP_STATUSES.PENDING }); if (pendingAnywhere) throw new Error('You already have a pending request. Please wait for it to be reviewed before requesting to join another organization.'); - const membership = await MembershipRepository.create({ userId, organizationId, role: 'member', status: 'pending' }); + const membership = await MembershipRepository.create({ userId, organizationId, role: MEMBERSHIP_ROLES.MEMBER, status: MEMBERSHIP_STATUSES.PENDING }); if (mailer.isConfigured()) { const org = await OrganizationRepository.get(organizationId); if (user?.email && org?.name) { - const admins = await MembershipRepository.list({ organizationId, role: { $in: ['owner', 'admin'] }, status: 'active' }); + const admins = await MembershipRepository.list({ organizationId, role: { $in: [MEMBERSHIP_ROLES.OWNER, MEMBERSHIP_ROLES.ADMIN] }, status: MEMBERSHIP_STATUSES.ACTIVE }); for (const admin of admins) { if (admin.userId?.email) { mailer.sendMail({ @@ -181,7 +198,7 @@ const createJoinRequest = async (userId, organizationId) => { * @returns {Promise} The updated membership. */ const approveRequest = async (membership) => { - membership.status = 'active'; + membership.status = MEMBERSHIP_STATUSES.ACTIVE; const result = await MembershipRepository.update(membership); // Set currentOrganization if user doesn't have one @@ -250,17 +267,16 @@ const rejectRequest = async (membership) => { * @returns {Promise} A success confirmation. */ const leave = async (userId, organizationId) => { - const membership = await MembershipRepository.findOne({ userId, organizationId, status: 'active' }); + const membership = await MembershipRepository.findOne({ userId, organizationId, status: MEMBERSHIP_STATUSES.ACTIVE }); if (!membership) throw new Error('You are not a member of this organization'); - if (membership.role === 'owner') { - const ownerCount = await MembershipRepository.count({ organizationId, role: 'owner', status: 'active' }); - if (ownerCount <= 1) throw new Error('You are the last owner. Promote another member before leaving.'); + if (membership.role === MEMBERSHIP_ROLES.OWNER) { + await validateLastOwnerProtection(organizationId); } await MembershipRepository.remove(membership); const userDoc = await UserService.getBrut({ id: String(userId) }); if (userDoc && String(userDoc.currentOrganization) === String(organizationId)) { - const remaining = await MembershipRepository.list({ userId, status: 'active' }); + const remaining = await MembershipRepository.list({ userId, status: MEMBERSHIP_STATUSES.ACTIVE }); const nextOrg = remaining.length > 0 ? (remaining[0].organizationId._id || remaining[0].organizationId) : null; await UserService.updateById(userDoc._id, { currentOrganization: nextOrg }); } @@ -280,7 +296,7 @@ const invite = async (organizationId, email, invitedBy) => { const existingInvite = await MembershipRepository.findOne({ invitedEmail: email.toLowerCase(), organizationId, - status: 'invited', + status: MEMBERSHIP_STATUSES.INVITED, }); if (existingInvite) throw new Error('An invite has already been sent to this email'); @@ -289,7 +305,7 @@ const invite = async (organizationId, email, invitedBy) => { const existingMembership = await MembershipRepository.findOne({ userId: existingUser._id, organizationId, - status: { $in: ['active', 'pending', 'invited'] }, + status: { $in: [MEMBERSHIP_STATUSES.ACTIVE, MEMBERSHIP_STATUSES.PENDING, MEMBERSHIP_STATUSES.INVITED] }, }); if (existingMembership) throw new Error('User is already a member or has a pending request'); } @@ -298,8 +314,8 @@ const invite = async (organizationId, email, invitedBy) => { const membership = await MembershipRepository.create({ userId: existingUser ? existingUser._id : null, organizationId, - role: 'member', - status: 'invited', + role: MEMBERSHIP_ROLES.MEMBER, + status: MEMBERSHIP_STATUSES.INVITED, inviteToken, invitedEmail: email.toLowerCase(), inviteExpiresAt: new Date(Date.now() + 7 * 24 * 3600000), @@ -340,7 +356,7 @@ const invite = async (organizationId, email, invitedBy) => { * @returns {Promise} The updated membership. */ const acceptInvite = async (token, userId) => { - const membership = await MembershipRepository.findOne({ inviteToken: token, status: 'invited' }); + const membership = await MembershipRepository.findOne({ inviteToken: token, status: MEMBERSHIP_STATUSES.INVITED }); if (!membership) throw new Error('Invalid or expired invite'); if (membership.inviteExpiresAt && membership.inviteExpiresAt < Date.now()) { @@ -360,7 +376,7 @@ const acceptInvite = async (token, userId) => { } membership.userId = userId; - membership.status = 'active'; + membership.status = MEMBERSHIP_STATUSES.ACTIVE; membership.inviteToken = null; const result = await MembershipRepository.update(membership); @@ -379,7 +395,7 @@ const acceptInvite = async (token, userId) => { * @param {String} token - The invite token. * @returns {Promise} The invited membership or null. */ -const getInvite = (token) => MembershipRepository.findOne({ inviteToken: token, status: 'invited' }); +const getInvite = (token) => MembershipRepository.findOne({ inviteToken: token, status: MEMBERSHIP_STATUSES.INVITED }); /** * @function count diff --git a/modules/organizations/services/organizations.service.js b/modules/organizations/services/organizations.service.js index 6afc71f52..090f3617c 100644 --- a/modules/organizations/services/organizations.service.js +++ b/modules/organizations/services/organizations.service.js @@ -11,6 +11,7 @@ import MembershipRepository from '../repositories/organizations.membership.repos import MembershipService from './organizations.membership.service.js'; import UserService from '../../users/services/users.service.js'; import { slugify, generateOrganizationSlug } from '../helpers/organizations.slug.js'; +import { MEMBERSHIP_ROLES } from '../lib/constants.js'; /** * @desc Strip sensitive fields from an organization document before returning to public flows. @@ -90,7 +91,7 @@ const createOrganizationForUser = async ({ name, slug, domain, user, slugGenerat membership = await MembershipRepository.create({ userId, organizationId: organization._id, - role: 'owner', + role: MEMBERSHIP_ROLES.OWNER, }); await UserService.updateById(userId, { currentOrganization: organization._id }); diff --git a/modules/organizations/tests/organizations.membership.controller.unit.tests.js b/modules/organizations/tests/organizations.membership.controller.unit.tests.js new file mode 100644 index 000000000..d658a462a --- /dev/null +++ b/modules/organizations/tests/organizations.membership.controller.unit.tests.js @@ -0,0 +1,158 @@ +/** + * Module dependencies. + */ +import { jest, describe, test, expect, beforeEach } from '@jest/globals'; +import { MEMBERSHIP_ROLES } from '../lib/constants.js'; + +const mockList = jest.fn(); +const mockUpdateRole = jest.fn(); +const mockRemove = jest.fn(); +const mockGet = jest.fn(); + +jest.unstable_mockModule('../services/organizations.membership.service.js', () => ({ + default: { + list: mockList, + updateRole: mockUpdateRole, + remove: mockRemove, + get: mockGet, + }, +})); + +const { default: membershipController } = await import('../controllers/organizations.membership.controller.js'); + +/** + * Unit tests for the membership controller RBAC guards. + */ +describe('Membership controller unit tests:', () => { + /** + * @desc Build a minimal Express-like req object + * @param {Object} overrides + * @returns {Object} mock request + */ + function mockReq(overrides = {}) { + return { + query: {}, + body: {}, + organization: { _id: 'org1' }, + membership: { role: MEMBERSHIP_ROLES.OWNER }, + membershipDoc: { id: 'mem1', role: MEMBERSHIP_ROLES.MEMBER, organizationId: 'org1' }, + ...overrides, + }; + } + + /** + * @desc Build a minimal Express-like res object with spies + * @returns {Object} mock response + */ + function mockRes() { + const res = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + return res; + } + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('updateRole', () => { + test('should reject when req.membership is missing (fail closed)', async () => { + const req = mockReq({ membership: undefined }); + const res = mockRes(); + + await membershipController.updateRole(req, res); + + expect(res.status).toHaveBeenCalledWith(403); + expect(mockUpdateRole).not.toHaveBeenCalled(); + }); + + test('should reject when actor is not an owner', async () => { + const req = mockReq({ membership: { role: MEMBERSHIP_ROLES.ADMIN } }); + const res = mockRes(); + + await membershipController.updateRole(req, res); + + expect(res.status).toHaveBeenCalledWith(403); + expect(mockUpdateRole).not.toHaveBeenCalled(); + }); + + test('should allow owner to update role', async () => { + const updatedMembership = { id: 'mem1', role: MEMBERSHIP_ROLES.ADMIN }; + mockUpdateRole.mockResolvedValue(updatedMembership); + + const req = mockReq({ membership: { role: MEMBERSHIP_ROLES.OWNER }, body: { role: MEMBERSHIP_ROLES.ADMIN } }); + const res = mockRes(); + + await membershipController.updateRole(req, res); + + expect(mockUpdateRole).toHaveBeenCalledTimes(1); + expect(res.status).not.toHaveBeenCalledWith(403); + }); + }); + + describe('remove', () => { + test('should reject when actor is a member (cannot remove anyone)', async () => { + const req = mockReq({ membership: { role: MEMBERSHIP_ROLES.MEMBER }, membershipDoc: { id: 'mem2', role: MEMBERSHIP_ROLES.MEMBER } }); + const res = mockRes(); + + await membershipController.remove(req, res); + + expect(res.status).toHaveBeenCalledWith(403); + expect(mockRemove).not.toHaveBeenCalled(); + }); + + test('should reject when req.membership is missing', async () => { + const req = mockReq({ membership: undefined, membershipDoc: { id: 'mem2', role: MEMBERSHIP_ROLES.MEMBER } }); + const res = mockRes(); + + await membershipController.remove(req, res); + + expect(res.status).toHaveBeenCalledWith(403); + expect(mockRemove).not.toHaveBeenCalled(); + }); + + test('should reject when admin tries to remove an owner', async () => { + const req = mockReq({ membership: { role: MEMBERSHIP_ROLES.ADMIN }, membershipDoc: { id: 'mem2', role: MEMBERSHIP_ROLES.OWNER } }); + const res = mockRes(); + + await membershipController.remove(req, res); + + expect(res.status).toHaveBeenCalledWith(403); + expect(mockRemove).not.toHaveBeenCalled(); + }); + + test('should reject when admin tries to remove an admin', async () => { + const req = mockReq({ membership: { role: MEMBERSHIP_ROLES.ADMIN }, membershipDoc: { id: 'mem2', role: MEMBERSHIP_ROLES.ADMIN } }); + const res = mockRes(); + + await membershipController.remove(req, res); + + expect(res.status).toHaveBeenCalledWith(403); + expect(mockRemove).not.toHaveBeenCalled(); + }); + + test('should allow admin to remove a member', async () => { + mockRemove.mockResolvedValue({ success: true }); + + const req = mockReq({ membership: { role: MEMBERSHIP_ROLES.ADMIN }, membershipDoc: { id: 'mem2', role: MEMBERSHIP_ROLES.MEMBER } }); + const res = mockRes(); + + await membershipController.remove(req, res); + + expect(mockRemove).toHaveBeenCalledTimes(1); + expect(res.status).not.toHaveBeenCalledWith(403); + }); + + test('should allow owner to remove any member', async () => { + mockRemove.mockResolvedValue({ success: true }); + + const req = mockReq({ membership: { role: MEMBERSHIP_ROLES.OWNER }, membershipDoc: { id: 'mem2', role: MEMBERSHIP_ROLES.ADMIN } }); + const res = mockRes(); + + await membershipController.remove(req, res); + + expect(mockRemove).toHaveBeenCalledTimes(1); + expect(res.status).not.toHaveBeenCalledWith(403); + }); + }); +}); diff --git a/modules/organizations/tests/organizations.middleware.unit.tests.js b/modules/organizations/tests/organizations.middleware.unit.tests.js index 64bf15c79..850406cc8 100644 --- a/modules/organizations/tests/organizations.middleware.unit.tests.js +++ b/modules/organizations/tests/organizations.middleware.unit.tests.js @@ -15,7 +15,7 @@ jest.unstable_mockModule('../services/organizations.membership.service.js', () = default: { findByUserAndOrganization: mockFindByUserAndOrganization }, })); -const { default: organizationsMiddleware } = await import('../middleware/organizations.middleware.js'); +const { default: organizationsMiddleware } = await import('../middlewares/organizations.middleware.js'); const { resolveOrganization } = organizationsMiddleware; /** diff --git a/modules/tasks/routes/tasks.routes.js b/modules/tasks/routes/tasks.routes.js index 867abb363..f29b37bd6 100644 --- a/modules/tasks/routes/tasks.routes.js +++ b/modules/tasks/routes/tasks.routes.js @@ -4,7 +4,7 @@ import passport from 'passport'; import model from '../../../lib/middlewares/model.js'; -import organization from '../../organizations/middleware/organizations.middleware.js'; +import organization from '../../organizations/middlewares/organizations.middleware.js'; import policy from '../../../lib/middlewares/policy.js'; import tasks from '../controllers/tasks.controller.js'; import tasksSchema from '../models/tasks.schema.js'; diff --git a/modules/users/services/users.service.js b/modules/users/services/users.service.js index 677cb92f9..c1c54eaa3 100644 --- a/modules/users/services/users.service.js +++ b/modules/users/services/users.service.js @@ -8,23 +8,25 @@ import AuthService from '../../auth/services/auth.service.js'; import UserRepository from '../repositories/users.repository.js'; import MembershipService from '../../organizations/services/organizations.membership.service.js'; import OrganizationsCrudService from '../../organizations/services/organizations.crud.service.js'; +import { MEMBERSHIP_ROLES, MEMBERSHIP_STATUSES } from '../../organizations/lib/constants.js'; +import { removeSensitive } from '../utils/sanitizeUser.js'; /** * @desc Function to get all users in db * @param {String} search * @param {Int} page * @param {Int} perPage - * @return {Promise} users selected + * @returns {Promise} users selected */ const list = async (search, page, perPage) => { const result = await UserRepository.list(search, page || 0, perPage || 20); - return result.map((user) => AuthService.removeSensitive(user)); + return result.map((user) => removeSensitive(user)); }; /** - * @desc Function to ask repository to create a user (define provider, check & haspassword, save) + * @desc Function to ask repository to create a user (define provider, check & hashpassword, save) * @param {Object} user - * @return {Promise} user + * @returns {Promise} created user (sanitized) */ const create = async (user) => { // Set provider to local @@ -41,33 +43,33 @@ const create = async (user) => { } const result = await UserRepository.create(user); // Remove sensitive data before return - return AuthService.removeSensitive(result); + return removeSensitive(result); }; /** * @desc Function to ask repository to search users by request - * @param {Object} mongoose input request - * @return {Array} users + * @param {Object} input - mongoose query input + * @returns {Promise} matching users (sanitized) */ const search = async (input) => { const result = await UserRepository.search(input); - return result.map((user) => AuthService.removeSensitive(user)); + return result.map((user) => removeSensitive(user)); }; /** * @desc Function to ask repository to get a user by id or email - * @param {Object} user.id / user.email - * @return {Object} user + * @param {Object} user - object with id or email field + * @returns {Promise} sanitized user or null */ const get = async (user) => { const result = await UserRepository.get(user); - return AuthService.removeSensitive(result); + return removeSensitive(result); }; /** * @desc Function to ask repository to get a user by id or email without filter data return (test & intern usage) - * @param {Object} user.id / user.email - * @return {Object} user + * @param {Object} user - object with id or email field + * @returns {Promise} full user document or null */ const getBrut = async (user) => { const result = await UserRepository.get(user); @@ -75,36 +77,36 @@ const getBrut = async (user) => { }; /** - * @desc Functio to ask repository to update a user - * @param {Object} user - original user - * @param {Object} body - user edited - * @param {boolean} admin - true if admin update - * @return {Promise} user - + * @desc Function to ask repository to update a user + * @param {Object} user - original user document + * @param {Object} body - fields to update + * @param {string} [option] - update mode: 'admin', 'recover', or undefined for user self-update + * @returns {Promise} updated user (sanitized) */ const update = async (user, body, option) => { - if (!option) user = _.assignIn(user, AuthService.removeSensitive(body, config.whitelists.users.update)); - else if (option === 'admin') user = _.assignIn(user, AuthService.removeSensitive(body, config.whitelists.users.updateAdmin)); - else if (option === 'recover') user = _.assignIn(user, AuthService.removeSensitive(body, config.whitelists.users.recover)); + if (!option) user = _.assignIn(user, removeSensitive(body, config.whitelists.users.update)); + else if (option === 'admin') user = _.assignIn(user, removeSensitive(body, config.whitelists.users.updateAdmin)); + else if (option === 'recover') user = _.assignIn(user, removeSensitive(body, config.whitelists.users.recover)); const result = await UserRepository.update(user); - return AuthService.removeSensitive(result); + return removeSensitive(result); }; /** - * @desc Functio to ask repository to sign terms for current user - * @param {Object} user - original user - * @return {Promise} user - + * @desc Function to ask repository to sign terms for current user + * @param {Object} user - original user document + * @returns {Promise} updated user (sanitized) */ const terms = async (user) => { user = _.assignIn(user, { terms: new Date() }); const result = await UserRepository.update(user); - return AuthService.removeSensitive(result); + return removeSensitive(result); }; /** - * @desc Function to ask repository to a user from db by id or email - * @param {Object} user - * @return {Promise} result & id + * @desc Function to remove a user from db and clean up associated memberships/orgs + * @param {Object} user - user document with _id or id field + * @returns {Promise} deletion result */ const remove = async (user) => { const userId = user._id || user.id; @@ -113,9 +115,9 @@ const remove = async (user) => { const memberships = await MembershipService.listByUser(userId); for (const membership of memberships) { const orgId = membership.organizationId._id || membership.organizationId; - if (membership.role === 'owner') { + if (membership.role === MEMBERSHIP_ROLES.OWNER) { // Check if this user is the only owner of the org - const ownerCount = await MembershipService.count({ organizationId: orgId, role: 'owner' }); + const ownerCount = await MembershipService.count({ organizationId: orgId, role: MEMBERSHIP_ROLES.OWNER, status: MEMBERSHIP_STATUSES.ACTIVE }); if (ownerCount <= 1) { // Sole owner — delete the entire org and its memberships // Clear currentOrganization for affected users @@ -135,7 +137,7 @@ const remove = async (user) => { /** * @desc Function to get all stats of db - * @return {Promise} All stats + * @returns {Promise} user statistics */ const stats = async () => { const result = await UserRepository.stats(); @@ -196,4 +198,5 @@ export default { findByIdAndUpdatePopulated, searchByNameOrEmail, findByEmail, + removeSensitive, }; diff --git a/modules/users/utils/sanitizeUser.js b/modules/users/utils/sanitizeUser.js new file mode 100644 index 000000000..c80a3437e --- /dev/null +++ b/modules/users/utils/sanitizeUser.js @@ -0,0 +1,21 @@ +/** + * Module dependencies + */ +import _ from 'lodash'; + +import config from '../../../config/index.js'; + +/** + * @desc Remove sensitive data from user object, returning only whitelisted keys. + * @param {Object} user - Mongoose document or plain user object + * @param {Array} [conf] - Optional list of keys to pick. Defaults to config.whitelists.users.default. + * @returns {Object|null} sanitized user object or null + */ +const removeSensitive = (user, conf) => { + if (!user || typeof user !== 'object') return null; + const keys = conf || config.whitelists.users.default; + const plain = typeof user.toJSON === 'function' ? user.toJSON() : user; + return _.pick(plain, keys); +}; + +export { removeSensitive }; diff --git a/package-lock.json b/package-lock.json index de56b0661..ff42db852 100644 --- a/package-lock.json +++ b/package-lock.json @@ -175,6 +175,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1016,6 +1017,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1038,6 +1040,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2260,7 +2263,6 @@ "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", "license": "MIT", - "peer": true, "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", @@ -2278,7 +2280,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2294,7 +2295,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2311,7 +2311,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2323,15 +2322,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@jest/core": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", "license": "MIT", - "peer": true, "dependencies": { "@jest/console": "30.2.0", "@jest/pattern": "30.0.1", @@ -2379,7 +2376,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2395,7 +2391,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2412,7 +2407,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2424,15 +2418,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@jest/diff-sequences": { "version": "30.0.1", "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", "license": "MIT", - "peer": true, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } @@ -2442,7 +2434,6 @@ "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", "license": "MIT", - "peer": true, "dependencies": { "@jest/fake-timers": "30.2.0", "@jest/types": "30.2.0", @@ -2682,7 +2673,6 @@ "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", "license": "MIT", - "peer": true, "dependencies": { "expect": "30.2.0", "jest-snapshot": "30.2.0" @@ -2696,7 +2686,6 @@ "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", "license": "MIT", - "peer": true, "dependencies": { "@jest/get-type": "30.1.0" }, @@ -2709,7 +2698,6 @@ "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", "license": "MIT", - "peer": true, "dependencies": { "@jest/types": "30.2.0", "@sinonjs/fake-timers": "^13.0.0", @@ -3169,7 +3157,6 @@ "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", "license": "MIT", - "peer": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "30.2.0", @@ -3212,7 +3199,6 @@ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "license": "ISC", - "peer": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -3230,7 +3216,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -3245,15 +3230,13 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@jest/reporters/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -3263,7 +3246,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3280,7 +3262,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -3292,15 +3273,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@jest/reporters/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@jest/reporters/node_modules/glob": { "version": "10.5.0", @@ -3308,7 +3287,6 @@ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "deprecated": "Old versions of glob 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", - "peer": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -3329,7 +3307,6 @@ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -3344,15 +3321,13 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/@jest/reporters/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -3368,7 +3343,6 @@ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -3385,7 +3359,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", - "peer": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -3403,7 +3376,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -3419,7 +3391,6 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -3437,7 +3408,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3462,7 +3432,6 @@ "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", "license": "MIT", - "peer": true, "dependencies": { "@jest/types": "30.2.0", "chalk": "^4.1.2", @@ -3478,7 +3447,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -3494,7 +3462,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3511,7 +3478,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -3523,8 +3489,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@jest/source-map": { "version": "30.0.1", @@ -3545,7 +3510,6 @@ "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", "license": "MIT", - "peer": true, "dependencies": { "@jest/console": "30.2.0", "@jest/types": "30.2.0", @@ -3561,7 +3525,6 @@ "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", "license": "MIT", - "peer": true, "dependencies": { "@jest/test-result": "30.2.0", "graceful-fs": "^4.2.11", @@ -3577,7 +3540,6 @@ "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.27.4", "@jest/types": "30.2.0", @@ -3604,7 +3566,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -3620,7 +3581,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3637,7 +3597,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -3649,15 +3608,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@jest/types": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", "license": "MIT", - "peer": true, "dependencies": { "@jest/pattern": "30.0.1", "@jest/schemas": "30.0.5", @@ -3676,7 +3633,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -3692,7 +3648,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3709,7 +3664,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -3721,8 +3675,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", @@ -3818,6 +3771,7 @@ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -3963,6 +3917,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -3984,6 +3939,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.6.1.tgz", "integrity": "sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ==", "license": "Apache-2.0", + "peer": true, "engines": { "node": "^18.19.0 || >=20.6.0" }, @@ -3996,6 +3952,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -4404,6 +4361,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4420,6 +4378,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz", "integrity": "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", @@ -4437,6 +4396,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=14" } @@ -5502,7 +5462,6 @@ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@sinonjs/commons": "^3.0.1" } @@ -5668,6 +5627,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -6080,6 +6040,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6178,7 +6139,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -6270,7 +6230,6 @@ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", "license": "MIT", - "peer": true, "dependencies": { "@jest/transform": "30.2.0", "@types/babel__core": "^7.20.5", @@ -6292,7 +6251,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -6308,7 +6266,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -6325,7 +6282,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -6337,8 +6293,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/babel-plugin-istanbul": { "version": "7.0.1", @@ -6364,7 +6319,6 @@ "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", "license": "MIT", - "peer": true, "dependencies": { "@types/babel__core": "^7.20.5" }, @@ -6403,7 +6357,6 @@ "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", "license": "MIT", - "peer": true, "dependencies": { "babel-plugin-jest-hoist": "30.2.0", "babel-preset-current-node-syntax": "^1.2.0" @@ -6635,6 +6588,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7643,6 +7597,7 @@ "integrity": "sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -7747,6 +7702,7 @@ "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -8578,6 +8534,7 @@ "integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -8836,7 +8793,6 @@ "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", "license": "MIT", - "peer": true, "dependencies": { "@jest/expect-utils": "30.2.0", "@jest/get-type": "30.1.0", @@ -10513,7 +10469,6 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -10540,7 +10495,6 @@ "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", "license": "MIT", - "peer": true, "dependencies": { "execa": "^5.1.1", "jest-util": "30.2.0", @@ -10555,7 +10509,6 @@ "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", "license": "MIT", - "peer": true, "dependencies": { "@jest/environment": "30.2.0", "@jest/expect": "30.2.0", @@ -10587,7 +10540,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -10603,7 +10555,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -10620,7 +10571,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -10632,15 +10582,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-circus/node_modules/dedent": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", "license": "MIT", - "peer": true, "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, @@ -11828,7 +11776,6 @@ "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.27.4", "@jest/get-type": "30.1.0", @@ -11880,7 +11827,6 @@ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "license": "ISC", - "peer": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -11898,7 +11844,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -11913,15 +11858,13 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-config/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -11931,7 +11874,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -11948,7 +11890,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -11960,15 +11901,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-config/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-config/node_modules/glob": { "version": "10.5.0", @@ -11976,7 +11915,6 @@ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "deprecated": "Old versions of glob 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", - "peer": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -11997,7 +11935,6 @@ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -12012,15 +11949,13 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/jest-config/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -12036,7 +11971,6 @@ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -12053,7 +11987,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", - "peer": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -12071,7 +12004,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -12087,7 +12019,6 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -12105,7 +12036,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -12118,7 +12048,6 @@ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", "license": "MIT", - "peer": true, "dependencies": { "@jest/diff-sequences": "30.0.1", "@jest/get-type": "30.1.0", @@ -12134,7 +12063,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -12150,7 +12078,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -12167,7 +12094,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -12179,8 +12105,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-docblock": { "version": "30.2.0", @@ -12199,7 +12124,6 @@ "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", "license": "MIT", - "peer": true, "dependencies": { "@jest/get-type": "30.1.0", "@jest/types": "30.2.0", @@ -12216,7 +12140,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -12232,7 +12155,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -12249,7 +12171,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -12261,8 +12182,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-environment-jsdom": { "version": "30.3.0", @@ -12488,7 +12408,6 @@ "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", "license": "MIT", - "peer": true, "dependencies": { "@jest/environment": "30.2.0", "@jest/fake-timers": "30.2.0", @@ -12507,7 +12426,6 @@ "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", "license": "MIT", - "peer": true, "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", @@ -12532,7 +12450,6 @@ "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", "license": "MIT", - "peer": true, "dependencies": { "@jest/get-type": "30.1.0", "pretty-format": "30.2.0" @@ -12546,7 +12463,6 @@ "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", "license": "MIT", - "peer": true, "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", @@ -12562,7 +12478,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -12578,7 +12493,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -12595,7 +12509,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -12607,15 +12520,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-message-util": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@jest/types": "30.2.0", @@ -12636,7 +12547,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -12652,7 +12562,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -12669,7 +12578,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -12681,15 +12589,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-mock": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", "license": "MIT", - "peer": true, "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", @@ -12730,7 +12636,6 @@ "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", "license": "MIT", - "peer": true, "dependencies": { "chalk": "^4.1.2", "graceful-fs": "^4.2.11", @@ -12750,7 +12655,6 @@ "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", "license": "MIT", - "peer": true, "dependencies": { "jest-regex-util": "30.0.1", "jest-snapshot": "30.2.0" @@ -12764,7 +12668,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -12780,7 +12683,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -12797,7 +12699,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -12809,15 +12710,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-runner": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", "license": "MIT", - "peer": true, "dependencies": { "@jest/console": "30.2.0", "@jest/environment": "30.2.0", @@ -12851,7 +12750,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -12867,7 +12765,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -12884,7 +12781,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -12896,15 +12792,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-runtime": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", "license": "MIT", - "peer": true, "dependencies": { "@jest/environment": "30.2.0", "@jest/fake-timers": "30.2.0", @@ -12938,7 +12832,6 @@ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "license": "ISC", - "peer": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -12956,7 +12849,6 @@ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", "license": "MIT", - "peer": true, "dependencies": { "@jest/environment": "30.2.0", "@jest/expect": "30.2.0", @@ -12972,7 +12864,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -12987,15 +12878,13 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-runtime/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -13005,7 +12894,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -13022,7 +12910,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -13034,15 +12921,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-runtime/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-runtime/node_modules/glob": { "version": "10.5.0", @@ -13050,7 +12935,6 @@ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "deprecated": "Old versions of glob 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", - "peer": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -13071,7 +12955,6 @@ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -13086,15 +12969,13 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/jest-runtime/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -13110,7 +12991,6 @@ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -13127,7 +13007,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", - "peer": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -13145,7 +13024,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -13161,7 +13039,6 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -13179,7 +13056,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13192,7 +13068,6 @@ "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", @@ -13225,7 +13100,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -13241,7 +13115,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -13258,7 +13131,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -13270,15 +13142,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-util": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", "license": "MIT", - "peer": true, "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", @@ -13296,7 +13166,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -13312,7 +13181,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -13329,7 +13197,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -13341,15 +13208,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-util/node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13362,7 +13227,6 @@ "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", "license": "MIT", - "peer": true, "dependencies": { "@jest/get-type": "30.1.0", "@jest/types": "30.2.0", @@ -13380,7 +13244,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -13396,7 +13259,6 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -13409,7 +13271,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -13426,7 +13287,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -13438,15 +13298,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-watcher": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", "license": "MIT", - "peer": true, "dependencies": { "@jest/test-result": "30.2.0", "@jest/types": "30.2.0", @@ -13466,7 +13324,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -13482,7 +13339,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -13499,7 +13355,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -13511,15 +13366,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-worker": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", @@ -13536,7 +13389,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -13552,7 +13404,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -13568,7 +13419,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -13585,7 +13435,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -13597,15 +13446,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest/node_modules/jest-cli": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/test-result": "30.2.0", @@ -13672,6 +13519,7 @@ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -14291,6 +14139,7 @@ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -16878,6 +16727,7 @@ "dev": true, "inBundle": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -17932,7 +17782,6 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", "license": "MIT", - "peer": true, "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", @@ -18463,6 +18312,7 @@ "integrity": "sha512-WRgl5GcypwramYX4HV+eQGzUbD7UUbljVmS+5G1uMwX/wLgYuJAxGeerXJDMO2xshng4+FXqCgyB5QfClV6WjA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", @@ -20081,6 +19931,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" },