diff --git a/__tests__/KanbanBoard.test.jsx b/__tests__/KanbanBoard.test.jsx new file mode 100644 index 00000000..ca09cd75 --- /dev/null +++ b/__tests__/KanbanBoard.test.jsx @@ -0,0 +1,135 @@ +/** + * @jest-environment jsdom + */ +jest.mock('framer-motion', () => ({ + motion: { + div: ({ children, onClick, ...props }) =>
{children}
, + }, + AnimatePresence: ({ children }) => <>{children}, +})); + +jest.mock('@/components/ui/badge', () => ({ + Badge: ({ children, className }) => {children}, +})); + +jest.mock('@/components/ui/avatar', () => ({ + Avatar: ({ children, className }) =>
{children}
, + AvatarImage: () => null, + AvatarFallback: ({ children, className }) => {children}, +})); + +jest.mock('@/components/workspace/TaskDetailDialog', () => { + return function MockDialog({ open, onOpenChange }) { + return open ?
onOpenChange(false)} /> : null; + }; +}); + +jest.mock('date-fns', () => ({ + format: jest.fn(() => '1 Jan 2026'), + formatDistanceToNow: jest.fn(() => '2 days ago'), + isToday: jest.fn(() => false), + isYesterday: jest.fn(() => false), +})); + +const { render, screen, fireEvent } = require('@testing-library/react'); +const React = require('react'); +const KanbanBoard = require('@/components/workspace/KanbanBoard').default; + +const makeTask = (overrides = {}) => ({ + _id: 'task-' + Math.random().toString(36).slice(2, 8), + id: 'task-' + Math.random().toString(36).slice(2, 8), + title: 'Test Task', + status: 'Ready', + assignedTo: 'user-1', + assignedToName: 'User One', + ...overrides, +}); + +const makeStep = (tasks = []) => ({ + _id: 'step-1', + id: 'step-1', + title: 'Backlog', + tasks, +}); + +const defaultProps = (overrides = {}) => ({ + steps: [], + onUpdateTask: jest.fn(), + users: [{ uid: 'user-1', displayName: 'User One', email: 'u@t.com' }], + isOwner: true, + currentUser: { uid: 'user-1' }, + ...overrides, +}); + +// F1: renders 5 columns +test('F1: renders all 5 Kanban columns', () => { + render(); + expect(screen.getByText('Ready')).toBeTruthy(); + expect(screen.getByText('Active')).toBeTruthy(); + expect(screen.getByText('In Progress')).toBeTruthy(); + expect(screen.getByText('Done')).toBeTruthy(); + expect(screen.getByText('PR Raised')).toBeTruthy(); +}); + +// F2: maps Pending → Ready (legacy fallback) +test('F2: maps Pending status to Ready column', () => { + const task = makeTask({ status: 'Pending' }); + const step = makeStep([task]); + render(); + const readyColumn = screen.getByText('Ready').closest('div.flex.flex-col'); + expect(readyColumn.textContent).toContain('Test Task'); +}); + +// F3: maps Completed → Done +test('F3: maps Completed status to Done column', () => { + const task = makeTask({ status: 'Completed', title: 'Completed Task' }); + const step = makeStep([task]); + render(); + const doneColumn = screen.getByText('Done').closest('div.flex.flex-col'); + expect(doneColumn.textContent).toContain('Completed Task'); +}); + +// F4: clicking Ready task as assignee calls onUpdateTask with Active +test('F4: clicking Ready task as assignee auto-updates to Active', () => { + const task = makeTask({ status: 'Ready', _id: 't1', id: 't1' }); + const step = makeStep([task]); + const onUpdateTask = jest.fn(); + render(); + fireEvent.click(screen.getByText('Test Task')); + expect(onUpdateTask).toHaveBeenCalledWith('step-1', 't1', { status: 'Active' }); +}); + +// F5: clicking Ready task as non-assignee does NOT call onUpdateTask +test('F5: clicking Ready task as non-assignee does not auto-update', () => { + const task = makeTask({ status: 'Ready', _id: 't2', id: 't2', assignedTo: 'other-user' }); + const step = makeStep([task]); + const onUpdateTask = jest.fn(); + render(); + fireEvent.click(screen.getByText('Test Task')); + expect(onUpdateTask).not.toHaveBeenCalled(); +}); + +// F6: clicking non-Ready task does NOT call onUpdateTask +test('F6: clicking In Progress task does not auto-update', () => { + const task = makeTask({ status: 'In Progress', _id: 't3', id: 't3' }); + const step = makeStep([task]); + const onUpdateTask = jest.fn(); + render(); + fireEvent.click(screen.getByText('Test Task')); + expect(onUpdateTask).not.toHaveBeenCalled(); +}); + +// F7: column count badges reflect task counts +test('F7: column badges show correct task counts', () => { + const tasks = [ + makeTask({ status: 'Ready', title: 'R1', _id: 'r1', id: 'r1' }), + makeTask({ status: 'Done', title: 'D1', _id: 'd1', id: 'd1' }), + makeTask({ status: 'Done', title: 'D2', _id: 'd2', id: 'd2' }), + ]; + const step = makeStep(tasks); + render(); + const badges = screen.getAllByText(/^[0-9]+$/); + const counts = badges.map(b => parseInt(b.textContent, 10)); + expect(counts).toContain(1); // Ready: 1 + expect(counts).toContain(2); // Done: 2 +}); diff --git a/__tests__/taskSocketService.test.js b/__tests__/taskSocketService.test.js new file mode 100644 index 00000000..5defa30d --- /dev/null +++ b/__tests__/taskSocketService.test.js @@ -0,0 +1,137 @@ +jest.mock('socket.io-client', () => { + const handlers = {}; + const mockSocket = { + connected: false, + on: jest.fn((event, cb) => { handlers[event] = cb; }), + emit: jest.fn(), + disconnect: jest.fn(() => { mockSocket.connected = false; }), + _fire: (event, data) => handlers[event]?.(data), + _setConnected: (val) => { mockSocket.connected = val; }, + }; + return { io: jest.fn(() => mockSocket), __mockSocket: mockSocket }; +}); + +jest.mock('@/lib/utils', () => ({ SOCKET_BASE_URL: 'http://localhost:3000' })); + +const { io, __mockSocket } = require('socket.io-client'); +const { + connectTaskSocket, + disconnectTaskSocket, + getTaskSocket, + joinProject, + leaveProject, + onTaskCreated, + onTaskUpdated, + onTaskDeleted, + onTaskAssigned, +} = require('@/services/taskSocketService'); + +beforeEach(() => { + jest.clearAllMocks(); + __mockSocket.connected = false; + disconnectTaskSocket(); +}); + +afterEach(() => { + disconnectTaskSocket(); +}); + +// F8: connectTaskSocket creates socket connection +test('F8: connectTaskSocket creates socket with /tasks namespace', () => { + const sock = connectTaskSocket('user-1'); + expect(io).toHaveBeenCalledWith('http://localhost:3000/tasks', expect.objectContaining({ + query: { userId: 'user-1' }, + transports: ['websocket', 'polling'], + })); + expect(sock).toBe(__mockSocket); +}); + +// F9: connectTaskSocket is idempotent when already connected +test('F9: connectTaskSocket returns existing socket when already connected', () => { + __mockSocket._setConnected(true); + const sock1 = connectTaskSocket('user-1'); + const sock2 = connectTaskSocket('user-1'); + expect(io).toHaveBeenCalledTimes(1); + expect(sock1).toBe(sock2); +}); + +// F10: onTaskCreated fires callback on task-created event +test('F10: onTaskCreated callback fires when task-created event received', () => { + connectTaskSocket('user-1'); + const cb = jest.fn(); + onTaskCreated(cb); + __mockSocket._fire('task-created', { projectId: 'p1', taskId: 't1' }); + expect(cb).toHaveBeenCalledWith({ projectId: 'p1', taskId: 't1' }); +}); + +// F11: onTaskUpdated fires callback +test('F11: onTaskUpdated callback fires on task-updated event', () => { + connectTaskSocket('user-1'); + const cb = jest.fn(); + onTaskUpdated(cb); + __mockSocket._fire('task-updated', { projectId: 'p1', taskId: 't2', changes: { status: 'Done' } }); + expect(cb).toHaveBeenCalledWith({ projectId: 'p1', taskId: 't2', changes: { status: 'Done' } }); +}); + +// F12: onTaskDeleted fires callback +test('F12: onTaskDeleted callback fires on task-deleted event', () => { + connectTaskSocket('user-1'); + const cb = jest.fn(); + onTaskDeleted(cb); + __mockSocket._fire('task-deleted', { projectId: 'p1', taskId: 't3' }); + expect(cb).toHaveBeenCalledWith({ projectId: 'p1', taskId: 't3' }); +}); + +// F13: onTaskAssigned fires callback +test('F13: onTaskAssigned callback fires on task-assigned event', () => { + connectTaskSocket('user-1'); + const cb = jest.fn(); + onTaskAssigned(cb); + __mockSocket._fire('task-assigned', { projectId: 'p1', taskId: 't4' }); + expect(cb).toHaveBeenCalledWith({ projectId: 'p1', taskId: 't4' }); +}); + +// F14: unsubscribe removes callback +test('F14: unsubscribe function removes callback from listeners', () => { + connectTaskSocket('user-1'); + const cb = jest.fn(); + const unsub = onTaskCreated(cb); + unsub(); + __mockSocket._fire('task-created', { projectId: 'p1' }); + expect(cb).not.toHaveBeenCalled(); +}); + +// F15: joinProject emits join-project event +test('F15: joinProject emits join-project on socket', () => { + connectTaskSocket('user-1'); + joinProject('proj-123'); + expect(__mockSocket.emit).toHaveBeenCalledWith('join-project', 'proj-123'); +}); + +// F16: leaveProject emits leave-project event +test('F16: leaveProject emits leave-project on socket', () => { + connectTaskSocket('user-1'); + joinProject('proj-123'); + leaveProject('proj-123'); + expect(__mockSocket.emit).toHaveBeenCalledWith('leave-project', 'proj-123'); +}); + +// F17: disconnectTaskSocket clears socket and disconnects +test('F17: disconnectTaskSocket disconnects and clears state', () => { + connectTaskSocket('user-1'); + joinProject('p1'); + disconnectTaskSocket(); + expect(__mockSocket.disconnect).toHaveBeenCalled(); + expect(getTaskSocket()).toBeNull(); +}); + +// F18: reconnect re-joins previously joined projects +test('F18: on reconnect, previously joined projects are re-joined', () => { + connectTaskSocket('user-1'); + joinProject('proj-a'); + joinProject('proj-b'); + __mockSocket.emit.mockClear(); + __mockSocket._fire('connect', {}); + expect(__mockSocket.emit).toHaveBeenCalledWith('join-project', 'proj-a'); + expect(__mockSocket.emit).toHaveBeenCalledWith('join-project', 'proj-b'); +}); diff --git a/app-clients/android-kotlin/.gradle/8.9/checksums/checksums.lock b/app-clients/android-kotlin/.gradle/8.9/checksums/checksums.lock new file mode 100644 index 00000000..14d5fc7c Binary files /dev/null and b/app-clients/android-kotlin/.gradle/8.9/checksums/checksums.lock differ diff --git a/app-clients/android-kotlin/.gradle/8.9/dependencies-accessors/gc.properties b/app-clients/android-kotlin/.gradle/8.9/dependencies-accessors/gc.properties new file mode 100644 index 00000000..e69de29b diff --git a/app-clients/android-kotlin/.gradle/8.9/executionHistory/executionHistory.lock b/app-clients/android-kotlin/.gradle/8.9/executionHistory/executionHistory.lock new file mode 100644 index 00000000..13fd3c51 Binary files /dev/null and b/app-clients/android-kotlin/.gradle/8.9/executionHistory/executionHistory.lock differ diff --git a/app-clients/android-kotlin/.gradle/8.9/fileChanges/last-build.bin b/app-clients/android-kotlin/.gradle/8.9/fileChanges/last-build.bin new file mode 100644 index 00000000..f76dd238 Binary files /dev/null and b/app-clients/android-kotlin/.gradle/8.9/fileChanges/last-build.bin differ diff --git a/app-clients/android-kotlin/.gradle/8.9/fileHashes/fileHashes.lock b/app-clients/android-kotlin/.gradle/8.9/fileHashes/fileHashes.lock new file mode 100644 index 00000000..ff47bc1d Binary files /dev/null and b/app-clients/android-kotlin/.gradle/8.9/fileHashes/fileHashes.lock differ diff --git a/app-clients/android-kotlin/.gradle/8.9/gc.properties b/app-clients/android-kotlin/.gradle/8.9/gc.properties new file mode 100644 index 00000000..e69de29b diff --git a/app-clients/android-kotlin/.gradle/9.2.0/checksums/checksums.lock b/app-clients/android-kotlin/.gradle/9.2.0/checksums/checksums.lock new file mode 100644 index 00000000..900ea5d3 Binary files /dev/null and b/app-clients/android-kotlin/.gradle/9.2.0/checksums/checksums.lock differ diff --git a/app-clients/android-kotlin/.gradle/9.2.0/fileChanges/last-build.bin b/app-clients/android-kotlin/.gradle/9.2.0/fileChanges/last-build.bin new file mode 100644 index 00000000..f76dd238 Binary files /dev/null and b/app-clients/android-kotlin/.gradle/9.2.0/fileChanges/last-build.bin differ diff --git a/app-clients/android-kotlin/.gradle/9.2.0/fileHashes/fileHashes.bin b/app-clients/android-kotlin/.gradle/9.2.0/fileHashes/fileHashes.bin new file mode 100644 index 00000000..5ca7449f Binary files /dev/null and b/app-clients/android-kotlin/.gradle/9.2.0/fileHashes/fileHashes.bin differ diff --git a/app-clients/android-kotlin/.gradle/9.2.0/fileHashes/fileHashes.lock b/app-clients/android-kotlin/.gradle/9.2.0/fileHashes/fileHashes.lock new file mode 100644 index 00000000..b2701bba Binary files /dev/null and b/app-clients/android-kotlin/.gradle/9.2.0/fileHashes/fileHashes.lock differ diff --git a/app-clients/android-kotlin/.gradle/9.2.0/gc.properties b/app-clients/android-kotlin/.gradle/9.2.0/gc.properties new file mode 100644 index 00000000..e69de29b diff --git a/app-clients/android-kotlin/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/app-clients/android-kotlin/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 00000000..c5156449 Binary files /dev/null and b/app-clients/android-kotlin/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/app-clients/android-kotlin/.gradle/buildOutputCleanup/cache.properties b/app-clients/android-kotlin/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 00000000..c6960bbe --- /dev/null +++ b/app-clients/android-kotlin/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Mon Aug 03 21:46:29 IST 2026 +gradle.version=8.9 diff --git a/app-clients/android-kotlin/.gradle/vcs-1/gc.properties b/app-clients/android-kotlin/.gradle/vcs-1/gc.properties new file mode 100644 index 00000000..e69de29b diff --git a/babel.config.cjs b/babel.config.cjs index ca6259e4..b9512543 100644 --- a/babel.config.cjs +++ b/babel.config.cjs @@ -5,5 +5,6 @@ module.exports = { modules: 'commonjs' }], '@babel/preset-typescript', + ['@babel/preset-react', { runtime: 'automatic' }], ], }; diff --git a/backend/jest.config.js b/backend/jest.config.js index fe03913d..7b3297d1 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -75,6 +75,9 @@ */ module.exports = { testEnvironment: 'node', + // Run tests sequentially to avoid ESM module loading race conditions + // with jose/jwks-rsa (firebase-admin transitive deps) under parallel workers. + maxWorkers: 1, // Includes both `*.test.js` and legacy `*_test.js` suffixes. testMatch: ['**/tests/**/*.test.js', '**/tests/**/*_test.js', '**/__tests__/**/*.test.js'], // ESM-only transitive deps (jose, jwks-rsa via firebase-admin) must go @@ -83,4 +86,7 @@ module.exports = { transform: { '^.+\\.js$': 'babel-jest' }, transformIgnorePatterns: ['node_modules/(?!jose|jwks-rsa|@panva/asn1.js|firebase-admin|@firebase/)'], clearMocks: true, + moduleNameMapper: { + '^octokit$': '/tests/__mocks__/octokit.js', + }, }; diff --git a/backend/models/ProjectTask.js b/backend/models/ProjectTask.js index e0753875..f10bbe89 100644 --- a/backend/models/ProjectTask.js +++ b/backend/models/ProjectTask.js @@ -77,7 +77,7 @@ const mongoose = require('mongoose'); const projectTaskSchema = new mongoose.Schema( { - displayId: { type: String, default: null }, + displayId: { type: String }, title: { type: String, required: true }, description: { type: String, default: null }, status: { type: String, default: 'Ready' }, @@ -89,7 +89,7 @@ const projectTaskSchema = new mongoose.Schema( assignedBy: { type: String, default: null }, - commitCode: { type: String, default: null }, + commitCode: { type: String }, commitMessage: { type: String, default: null }, commitUrl: { type: String, default: null }, commitAuthor: { type: String, default: null }, diff --git a/backend/models/Team.js b/backend/models/Team.js index 3637b4df..bd039aa9 100644 --- a/backend/models/Team.js +++ b/backend/models/Team.js @@ -92,7 +92,6 @@ const teamSchema = new mongoose.Schema( } ); -teamSchema.index({ inviteCode: 1 }, { unique: true }); teamSchema.index({ members: 1 }); teamSchema.index({ ownerId: 1 }); diff --git a/backend/models/User.js b/backend/models/User.js index 9afc25c6..b95e215c 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -140,8 +140,6 @@ const userSchema = new mongoose.Schema( } ); -userSchema.index({ uid: 1 }, { unique: true }); -userSchema.index({ email: 1 }, { unique: true }); userSchema.index({ displayName: 'text', firstName: 'text', lastName: 'text' }); module.exports = mongoose.model('User', userSchema); diff --git a/backend/package-lock.json b/backend/package-lock.json index e5538714..233f7e8f 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -29,6 +29,9 @@ "nodemailer": "^9.0.1", "octokit": "^5.0.4", "pg": "^8.16.3", + "puppeteer": "^25.6.0", + "puppeteer-extra": "^3.3.6", + "puppeteer-extra-plugin-stealth": "^2.10.4", "redis": "^5.10.0", "rss-parser": "^3.13.0", "sharp": "^0.35.3", @@ -48,9 +51,6 @@ "mongodb-memory-server": "^11.2.0", "nodemon": "^3.1.9", "prisma": "^5.22.0", - "puppeteer": "^24.35.0", - "puppeteer-extra": "^3.3.6", - "puppeteer-extra-plugin-stealth": "^2.10.4", "supertest": "^7.2.2", "tsx": "^4.21.0", "typescript": "6.0.3" @@ -1919,9 +1919,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", "cpu": [ "ppc64" ], @@ -1936,9 +1936,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], @@ -1953,9 +1953,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ "arm64" ], @@ -1970,9 +1970,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], @@ -1987,9 +1987,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], @@ -2004,9 +2004,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ "x64" ], @@ -2021,9 +2021,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ "arm64" ], @@ -2038,9 +2038,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ "x64" ], @@ -2055,9 +2055,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ "arm" ], @@ -2072,9 +2072,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], @@ -2089,9 +2089,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ "ia32" ], @@ -2106,9 +2106,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ "loong64" ], @@ -2123,9 +2123,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ "mips64el" ], @@ -2140,9 +2140,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ "ppc64" ], @@ -2157,9 +2157,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", "cpu": [ "riscv64" ], @@ -2174,9 +2174,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ "s390x" ], @@ -2191,9 +2191,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", "cpu": [ "x64" ], @@ -2208,9 +2208,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], @@ -2225,9 +2225,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], @@ -2242,9 +2242,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", "cpu": [ "arm64" ], @@ -2259,9 +2259,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], @@ -2276,9 +2276,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], @@ -2293,9 +2293,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], @@ -2310,9 +2310,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], @@ -2327,9 +2327,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ "ia32" ], @@ -2344,9 +2344,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], @@ -2388,12 +2388,12 @@ "license": "Apache-2.0" }, "node_modules/@firebase/component": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.4.tgz", - "integrity": "sha512-tLpOaaCol9ugUIYp2R3CbWPPA8Ajg/papX/XHEy8U52b/QXH3BbX8tTJX9aShDCjp+9sMAxMLD94i7lresdugQ==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.3.tgz", + "integrity": "sha512-wFofIaa2879ogD/WvkjYXJxRmfnL0scen6ORgaC3na1FNOR9ASIUANQdhqQcmWu/h77/pVHY7ch5flewa5Bcew==", "license": "Apache-2.0", "dependencies": { - "@firebase/util": "1.15.2", + "@firebase/util": "1.15.1", "tslib": "^2.1.0" }, "engines": { @@ -2401,16 +2401,16 @@ } }, "node_modules/@firebase/database": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.4.tgz", - "integrity": "sha512-D+j4+8uhGtNd1tVD+X+c8JrC4ppStGJKyujSQt2NPwdN26QcCk0BeIxue+UqspHkHiFHyQOimwlzjLewGq6S+A==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.3.tgz", + "integrity": "sha512-XwWCa+E4TvNGpGwXrycLRNfdogADwFcvuhyow6wDWma9W54roaQIhe+4PM0KiLsIftBdSCGI7OKCXrdSRHbIhw==", "license": "Apache-2.0", "dependencies": { "@firebase/app-check-interop-types": "0.3.4", "@firebase/auth-interop-types": "0.2.5", - "@firebase/component": "0.7.4", + "@firebase/component": "0.7.3", "@firebase/logger": "0.5.1", - "@firebase/util": "1.15.2", + "@firebase/util": "1.15.1", "faye-websocket": "0.11.4", "tslib": "^2.1.0" }, @@ -2419,42 +2419,30 @@ } }, "node_modules/@firebase/database-compat": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.6.tgz", - "integrity": "sha512-mu7S/75UIajB1A5M9Vfojk69LttW55uABp9nHEtWrV/mIaSEwvoaIe9GySsEzS2EKFK5/3f5okcAuUbihhYeJg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.4.tgz", + "integrity": "sha512-3pK35F1MAgmqFJQlf2nhQl44vtAXQO1uaCaQOEUI9kCRtLFqi7N+QRKR7lFZPg+xIZIyubgxQaxY69YgfZRZWg==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.4", - "@firebase/database": "1.1.4", - "@firebase/database-types": "1.0.21", + "@firebase/component": "0.7.3", + "@firebase/database": "1.1.3", + "@firebase/database-types": "1.0.20", "@firebase/logger": "0.5.1", - "@firebase/util": "1.15.2", + "@firebase/util": "1.15.1", "tslib": "^2.1.0" }, "engines": { "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x", - "@firebase/app-compat": "0.x" - }, - "peerDependenciesMeta": { - "@firebase/app": { - "optional": true - }, - "@firebase/app-compat": { - "optional": true - } } }, "node_modules/@firebase/database-types": { - "version": "1.0.21", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.21.tgz", - "integrity": "sha512-SX1jUqhttKgg/m9dYRTvqU9QvucBooziWfA986r4cpsbi4zlsvewe424j3Vpduwd6DG1MSAMfBVT2VqA61FnkA==", + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.20.tgz", + "integrity": "sha512-kegbOk/w8iU64pr0q6k2ItyNGjnQBMHFhwS7ohdWI4W+pc0/zhhdGXTdFj6X1oxItRjPoYOsSQmERgBkn/ihxw==", "license": "Apache-2.0", "dependencies": { "@firebase/app-types": "0.9.5", - "@firebase/util": "1.15.2" + "@firebase/util": "1.15.1" } }, "node_modules/@firebase/logger": { @@ -2470,9 +2458,9 @@ } }, "node_modules/@firebase/util": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.2.tgz", - "integrity": "sha512-974pWIZVLDMc5GW5YAsj8y0XxULxIy/sPUy7tsxmWbF93KRIyh9xpuHlh0zDL+shUcf5nHDjFOg9YLiQ763eiA==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.1.tgz", + "integrity": "sha512-LUdM4Wg7YM9Pq/49nGYySJA0CSQEKnGffFzWV8+6gXN7mGxn+FL1IqvFbuZUtAQcfZgHYDwCE1wwlK7rB7gl2g==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -2483,9 +2471,9 @@ } }, "node_modules/@google-cloud/firestore": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-8.7.1.tgz", - "integrity": "sha512-Hp/WI8sH569ANitsko6RNvCUkzTCYudHoINOkQjiJq2FBG6kKnofBAW7PtS823cu6RMxHqK2RxRcspdjrRpUFA==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-8.7.0.tgz", + "integrity": "sha512-EvMpZQUXkTRdweSvOu6VL6EEQwHjHAgWz2UYZR+Mj6Ao52S+TWieHbSn15jiNnEw8F8RhbZj7IGXZ1PFB1eA+A==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -2559,21 +2547,6 @@ "node": ">=14" } }, - "node_modules/@google-cloud/storage/node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/@google-cloud/storage/node_modules/google-auth-library": { "version": "9.15.1", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", @@ -2592,16 +2565,6 @@ "node": ">=14" } }, - "node_modules/@google-cloud/storage/node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@grpc/grpc-js": { "version": "1.14.4", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", @@ -2746,9 +2709,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2765,9 +2725,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2784,9 +2741,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2803,9 +2757,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2822,9 +2773,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2841,9 +2789,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2860,9 +2805,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2879,9 +2821,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2898,9 +2837,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2923,9 +2859,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2948,9 +2881,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2973,9 +2903,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2998,9 +2925,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3023,9 +2947,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3048,9 +2969,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3073,9 +2991,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3303,16 +3218,6 @@ "node": ">=8" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@istanbuljs/schema": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", @@ -3510,6 +3415,23 @@ } } }, + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -3699,9 +3621,9 @@ } }, "node_modules/@nodable/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz", + "integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==", "funding": [ { "type": "github", @@ -4211,32 +4133,190 @@ "optional": true }, "node_modules/@protobufjs/utf8": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", - "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "license": "BSD-3-Clause", "optional": true }, "node_modules/@puppeteer/browsers": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", - "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", - "dev": true, + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.0.tgz", + "integrity": "sha512-LlBrE8oqGfU7b1Nk2d5Q1SbuPhZxTj0cJEMDPEws28OjNMELlflekmPPuf4FnK03x0ZRjKaYwJElUcKK4kyqJA==", "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "modern-tar": "^0.8.0", + "yargs": "^18.0.0" }, "bin": { - "browsers": "lib/cjs/main-cli.js" + "browsers": "lib/main-cli.js" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } + } + }, + "node_modules/@puppeteer/browsers/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@puppeteer/browsers/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@puppeteer/browsers/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@puppeteer/browsers/node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@puppeteer/browsers/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/@puppeteer/browsers/node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@puppeteer/browsers/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@puppeteer/browsers/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@puppeteer/browsers/node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@puppeteer/browsers/node_modules/yargs": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz", + "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/@puppeteer/browsers/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/@redis/bloom": { @@ -4356,13 +4436,6 @@ "node": ">= 10" } }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/aws-lambda": { "version": "8.10.161", "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.161.tgz", @@ -4434,7 +4507,6 @@ "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", - "dev": true, "license": "MIT", "dependencies": { "@types/ms": "*" @@ -4513,9 +4585,9 @@ } }, "node_modules/@types/pg": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", - "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.21.0.tgz", + "integrity": "sha512-AYdtudzabjLZgVgRZmAnU8bAnVUXzuJX2IYHeSIiIHm68olD+LgQYCGWdtcNYnP0uq9c4S4NibVG3Ni7VbKW7Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4633,17 +4705,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -4747,9 +4808,9 @@ } }, "node_modules/anynum": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.0.tgz", - "integrity": "sha512-xjR9/zBVnUOP6ztMIIgShjsxui80nQUQH+5xJnvrYLs+90bF25/KJqaAi8mk+B4RDtX1Nspi6fmp4YTEts8SfA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", "funding": [ { "type": "github", @@ -4776,7 +4837,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4799,19 +4859,6 @@ "dev": true, "license": "MIT" }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/async-mutex": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", @@ -4876,9 +4923,9 @@ } }, "node_modules/b4a": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", - "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", "dev": true, "license": "Apache-2.0", "peerDependencies": { @@ -4929,33 +4976,6 @@ "node": ">=8" } }, - "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/babel-plugin-jest-hoist": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", @@ -5079,9 +5099,9 @@ } }, "node_modules/bare-events": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", - "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", "dev": true, "license": "Apache-2.0", "peerDependencies": { @@ -5094,9 +5114,9 @@ } }, "node_modules/bare-fs": { - "version": "4.5.6", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.6.tgz", - "integrity": "sha512-1QovqDrR80Pmt5HPAsMsXTCFcDYr+NSUKW6nd6WO5v0JBmnItc/irNRzm2KOQ5oZ69P37y+AMujNyNtG+1Rggw==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.0.tgz", + "integrity": "sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5107,7 +5127,7 @@ "fast-fifo": "^1.3.2" }, "engines": { - "bare": ">=1.16.0" + "bare": ">=1.28.0" }, "peerDependencies": { "bare-buffer": "*" @@ -5118,33 +5138,21 @@ } } }, - "node_modules/bare-os": { - "version": "3.8.6", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.6.tgz", - "integrity": "sha512-l8xaNWWb/bXuzgsrlF5jaa5QYDJ9S0ddd54cP6CH+081+5iPrbJiCfBWQqrWYzmUhCbsH+WR6qxo9MeHVCr0MQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "bare": ">=1.14.0" - } - }, "node_modules/bare-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", - "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-os": "^3.0.1" - } + "license": "Apache-2.0" }, "node_modules/bare-stream": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.12.0.tgz", - "integrity": "sha512-w28i8lkBgREV3rPXGbgK+BO66q+ZpKqRWrZLiCdmmUlLPrQ45CzkvRhN+7lnv00Gpi2zy5naRxnUFAxCECDm9g==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", "dev": true, "license": "Apache-2.0", "dependencies": { + "b4a": "^1.8.1", "streamx": "^2.25.0", "teex": "^1.0.1" }, @@ -5166,9 +5174,9 @@ } }, "node_modules/bare-url": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz", - "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.2.tgz", + "integrity": "sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5217,16 +5225,6 @@ "node": ">=6.0.0" } }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/bcryptjs": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", @@ -5408,16 +5406,6 @@ "node": ">=20.19.0" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -5537,29 +5525,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chalk/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -5638,15 +5603,17 @@ } }, "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", - "dev": true, + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, "peerDependencies": { "devtools-protocol": "*" } @@ -5655,7 +5622,6 @@ "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -5703,7 +5669,6 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", "integrity": "sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg==", - "dev": true, "license": "MIT", "dependencies": { "for-own": "^0.1.3", @@ -5808,7 +5773,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/concat-stream": { @@ -5914,33 +5878,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/cosmiconfig": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", - "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -6012,16 +5949,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -6058,27 +5985,11 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -6117,10 +6028,9 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1581282", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", - "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", - "dev": true, + "version": "0.0.1653615", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1653615.tgz", + "integrity": "sha512-pGVkY3T/qXxAp2nFPodwYqOevk6ncNMSmvL8QfRCx5ZWGd6Vor7AFNmyaA8Zs6uJyP1QAfjuLandCgvSix1BNA==", "license": "BSD-3-Clause" }, "node_modules/dezalgo": { @@ -6261,9 +6171,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.404", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.404.tgz", - "integrity": "sha512-3WJtd7/lVq2Jnuz6wed1l9+1ZD2u2Tet1/1NBc4Iedkmgbu+I7YuAqdAQ8T+VZtnwysMsAf3IqSq9D1gyZjA2g==", + "version": "1.5.405", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.405.tgz", + "integrity": "sha512-bNglH7lPH5l+yHOes7Zr4VqxhOy4BQ9ZBUX4VdoFgxMpzJk7W1ZoO3Vgd9Pxa9PyjQ76sfm2aKH/nzEcCNRlew==", "dev": true, "license": "ISC" }, @@ -6313,8 +6223,8 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "devOptional": true, "license": "MIT", + "optional": true, "dependencies": { "once": "^1.4.0" } @@ -6404,16 +6314,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -6470,9 +6370,9 @@ } }, "node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -6483,39 +6383,38 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.1", - "@esbuild/android-arm": "0.28.1", - "@esbuild/android-arm64": "0.28.1", - "@esbuild/android-x64": "0.28.1", - "@esbuild/darwin-arm64": "0.28.1", - "@esbuild/darwin-x64": "0.28.1", - "@esbuild/freebsd-arm64": "0.28.1", - "@esbuild/freebsd-x64": "0.28.1", - "@esbuild/linux-arm": "0.28.1", - "@esbuild/linux-arm64": "0.28.1", - "@esbuild/linux-ia32": "0.28.1", - "@esbuild/linux-loong64": "0.28.1", - "@esbuild/linux-mips64el": "0.28.1", - "@esbuild/linux-ppc64": "0.28.1", - "@esbuild/linux-riscv64": "0.28.1", - "@esbuild/linux-s390x": "0.28.1", - "@esbuild/linux-x64": "0.28.1", - "@esbuild/netbsd-arm64": "0.28.1", - "@esbuild/netbsd-x64": "0.28.1", - "@esbuild/openbsd-arm64": "0.28.1", - "@esbuild/openbsd-x64": "0.28.1", - "@esbuild/openharmony-arm64": "0.28.1", - "@esbuild/sunos-x64": "0.28.1", - "@esbuild/win32-arm64": "0.28.1", - "@esbuild/win32-ia32": "0.28.1", - "@esbuild/win32-x64": "0.28.1" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -6537,52 +6436,6 @@ "node": ">=8" } }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -6645,19 +6498,6 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/execa/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -6751,38 +6591,6 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extract-zip/node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "node_modules/fast-content-type-parse": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", @@ -6827,9 +6635,9 @@ "license": "MIT" }, "node_modules/fast-xml-builder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", - "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.0.tgz", + "integrity": "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==", "funding": [ { "type": "github", @@ -6839,14 +6647,14 @@ "license": "MIT", "optional": true, "dependencies": { - "path-expression-matcher": "^1.5.0", - "xml-naming": "^0.1.0" + "path-expression-matcher": "^1.6.2", + "xml-naming": "^0.3.0" } }, "node_modules/fast-xml-parser": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.9.0.tgz", - "integrity": "sha512-duBuXbyIhEeNO4GjFuVqr0nF047oNwr18aum+zJyqo0MUG/n7Afgs3Qv3D6VN3ONedUKxiuFlPiMGIa0Z11chA==", + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.10.1.tgz", + "integrity": "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw==", "funding": [ { "type": "github", @@ -6856,12 +6664,12 @@ "license": "MIT", "optional": true, "dependencies": { - "@nodable/entities": "^2.2.0", + "@nodable/entities": "^3.0.0", "fast-xml-builder": "^1.2.0", - "is-unsafe": "^1.0.1", - "path-expression-matcher": "^1.5.0", - "strnum": "^2.4.0", - "xml-naming": "^0.1.0" + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", + "strnum": "^2.4.1", + "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -6889,16 +6697,6 @@ "bser": "2.1.1" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -7069,7 +6867,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7079,7 +6876,6 @@ "version": "0.1.5", "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", "integrity": "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==", - "dev": true, "license": "MIT", "dependencies": { "for-in": "^1.0.1" @@ -7226,7 +7022,6 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -7241,7 +7036,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -7293,134 +7087,28 @@ } }, "node_modules/gcp-metadata": { - "version": "8.1.4", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.4.tgz", - "integrity": "sha512-iJ9KMsiu+xKtNRX0PmGLSaIU3bUBAyzWTyqKemKPzNPsmmsBCQYmlNg+brEbES7IHSXtdVwzBPzx1vz3FAaipw==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", "license": "Apache-2.0", "optional": true, "dependencies": { - "gaxios": "7.1.3", - "google-logging-utils": "1.1.3", + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" }, "engines": { - "node": ">=18" - } - }, - "node_modules/gcp-metadata/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT", - "optional": true - }, - "node_modules/gcp-metadata/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", - "license": "MIT", - "optional": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/gcp-metadata/node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 12" + "node": ">=14" } }, - "node_modules/gcp-metadata/node_modules/gaxios": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", - "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "node_modules/gcp-metadata/node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", "license": "Apache-2.0", "optional": true, - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2", - "rimraf": "^5.0.1" - }, "engines": { - "node": ">=18" - } - }, - "node_modules/gcp-metadata/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "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", - "optional": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/gcp-metadata/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "license": "ISC", - "optional": true, - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/gcp-metadata/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "optional": true, - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/gcp-metadata/node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=14" } }, "node_modules/gensync": { @@ -7437,12 +7125,23 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "devOptional": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -7491,42 +7190,23 @@ } }, "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "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", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -7560,14 +7240,12 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/glob/node_modules/brace-expansion": { "version": "1.1.18", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -7578,7 +7256,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -7724,6 +7401,37 @@ "node": ">=18" } }, + "node_modules/google-gax/node_modules/gcp-metadata": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.4.tgz", + "integrity": "sha512-iJ9KMsiu+xKtNRX0PmGLSaIU3bUBAyzWTyqKemKPzNPsmmsBCQYmlNg+brEbES7IHSXtdVwzBPzx1vz3FAaipw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "gaxios": "7.1.3", + "google-logging-utils": "1.1.3", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/gcp-metadata/node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/google-gax/node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -7955,7 +7663,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/groq-sdk": { @@ -8003,13 +7710,13 @@ } }, "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/has-symbols": { @@ -8145,8 +7852,8 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "devOptional": true, "license": "MIT", + "optional": true, "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -8206,23 +7913,6 @@ "dev": true, "license": "ISC" }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -8258,7 +7948,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -8313,7 +8002,6 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true, "license": "MIT" }, "node_modules/is-core-module": { @@ -8336,7 +8024,6 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8399,7 +8086,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, "license": "MIT", "dependencies": { "isobject": "^3.0.1" @@ -8428,9 +8114,9 @@ } }, "node_modules/is-unsafe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-1.0.1.tgz", - "integrity": "sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz", + "integrity": "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==", "funding": [ { "type": "github", @@ -8451,7 +8137,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8477,21 +8162,31 @@ "node": ">=8" } }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" + "semver": "^6.3.0" }, "engines": { - "node": ">=10" + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, "node_modules/istanbul-lib-report": { @@ -8509,29 +8204,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-report/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/istanbul-lib-source-maps": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", @@ -9144,16 +8816,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/jest-worker/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -9171,9 +8833,9 @@ } }, "node_modules/jose": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.8.tgz", - "integrity": "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.4.tgz", + "integrity": "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -9261,7 +8923,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -9352,7 +9013,6 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, "license": "MIT", "dependencies": { "is-buffer": "^1.1.5" @@ -9375,7 +9035,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9412,6 +9071,18 @@ "url": "https://github.com/sponsors/dmonad" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/limiter": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", @@ -9595,7 +9266,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.3.tgz", "integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==", - "dev": true, "license": "MIT", "dependencies": { "arr-union": "^3.1.0", @@ -9698,13 +9368,13 @@ } }, "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -9727,14 +9397,12 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true, "license": "MIT" }, "node_modules/mixin-object": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", "integrity": "sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==", - "dev": true, "license": "MIT", "dependencies": { "for-in": "^0.1.3", @@ -9748,12 +9416,20 @@ "version": "0.1.8", "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", "integrity": "sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/modern-tar": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.4.tgz", + "integrity": "sha512-gN54ddmyzEg10orwZ2u4OOv+bjpMWdIl5jIkodK97bMq8QBSL5c0D7YX0lT1Ooz+99S7+PvFbnxzdjgHo1r41g==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/mongodb": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.5.0.tgz", @@ -9989,16 +9665,6 @@ "node": ">= 0.6" } }, - "node_modules/netmask": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/new-find-package-json": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-2.0.0.tgz", @@ -10092,9 +9758,9 @@ } }, "node_modules/nodemailer": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.5.tgz", - "integrity": "sha512-wvjiKvjczmsN7U/8006JOdXubgBk2XFAbioDMbT+sM7cPs0QrhJTa6KBRX7P5REGGkDcLUz/EarWidb8G8C1jQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.1.tgz", + "integrity": "sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -10129,6 +9795,29 @@ "url": "https://opencollective.com/nodemon" } }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -10309,40 +9998,6 @@ "node": ">=6" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "license": "MIT", - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -10350,19 +10005,6 @@ "license": "BlueOak-1.0.0", "optional": true }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -10451,9 +10093,9 @@ } }, "node_modules/path-expression-matcher": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", - "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", "funding": [ { "type": "github", @@ -10470,7 +10112,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10753,16 +10394,6 @@ "fsevents": "2.3.3" } }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -10827,43 +10458,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/proxy-agent/node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true, - "license": "MIT" - }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -10880,17 +10474,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -10901,51 +10484,47 @@ } }, "node_modules/puppeteer": { - "version": "24.40.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.40.0.tgz", - "integrity": "sha512-IxQbDq93XHVVLWHrAkFP7F7iHvb9o0mgfsSIMlhHb+JM+JjM1V4v4MNSQfcRWJopx9dsNOr9adYv0U5fm9BJBQ==", - "dev": true, + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.6.0.tgz", + "integrity": "sha512-TXUolDddU4AwISjOOrGk2AhJDpbM/ZDt2KvGIqz74EOk+8bKwXFo+acUvP1sQx3hUda7owOeNuuT1UnJT1o0qA==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.0", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1581282", - "puppeteer-core": "24.40.0", - "typed-query-selector": "^2.12.1" + "@puppeteer/browsers": "3.2.0", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1653615", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.6.0", + "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { - "version": "24.40.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.40.0.tgz", - "integrity": "sha512-MWL3XbUCfVgGR0gRsidzT6oKJT2QydPLhMITU6HoVWiiv4gkb6gJi3pcdAa8q4HwjBTbqISOWVP4aJiiyUJvag==", - "dev": true, + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.6.0.tgz", + "integrity": "sha512-GJ67rjZdVQzZmD2Ab0cgttfQN9j387QYMv3t6MN3/4nmjursNt6M5Utj4/T/4y0AwNrSwJzjw6Q/zuWFEIizOg==", "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.0", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1581282", - "typed-query-selector": "^2.12.1", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.19.0" + "@puppeteer/browsers": "3.2.0", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1653615", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.2", + "ws": "^8.21.1" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-extra": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/puppeteer-extra/-/puppeteer-extra-3.3.6.tgz", "integrity": "sha512-rsLBE/6mMxAjlLd06LuGacrukP2bqbzKCLzV1vrhHFavqQE/taQ2UXv3H5P0Ls7nsrASa+6x3bDbXHpqMwq+7A==", - "dev": true, "license": "MIT", "dependencies": { "@types/debug": "^4.1.0", @@ -10976,7 +10555,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/puppeteer-extra-plugin/-/puppeteer-extra-plugin-3.2.3.tgz", "integrity": "sha512-6RNy0e6pH8vaS3akPIKGg28xcryKscczt4wIl0ePciZENGE2yoaQJNd17UiEbdmh5/6WW6dPcfRWT9lxBwCi2Q==", - "dev": true, "license": "MIT", "dependencies": { "@types/debug": "^4.1.0", @@ -11003,7 +10581,6 @@ "version": "2.11.2", "resolved": "https://registry.npmjs.org/puppeteer-extra-plugin-stealth/-/puppeteer-extra-plugin-stealth-2.11.2.tgz", "integrity": "sha512-bUemM5XmTj9i2ZerBzsk2AN5is0wHMNE6K0hXBzBXOzP5m5G3Wl0RHhiqKeHToe/uIH8AoZiGhc1tCkLZQPKTQ==", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.1", @@ -11030,7 +10607,6 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/puppeteer-extra-plugin-user-data-dir/-/puppeteer-extra-plugin-user-data-dir-2.4.1.tgz", "integrity": "sha512-kH1GnCcqEDoBXO7epAse4TBPJh9tEpVEK/vkedKfjOVOhZAvLkHGc9swMs5ChrJbRnf8Hdpug6TJlEuimXNQ+g==", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.1", @@ -11058,7 +10634,6 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/puppeteer-extra-plugin-user-preferences/-/puppeteer-extra-plugin-user-preferences-2.4.1.tgz", "integrity": "sha512-i1oAZxRbc1bk8MZufKCruCEC3CCafO9RKMkkodZltI4OqibLFXF3tj6HZ4LZ9C5vCXZjYcDWazgtY69mnmrQ9A==", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.1", @@ -11307,7 +10882,7 @@ "node": ">=8" } }, - "node_modules/resolve-cwd/node_modules/resolve-from": { + "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", @@ -11317,16 +10892,6 @@ "node": ">=8" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/resolve.exports": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", @@ -11367,7 +10932,6 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -11450,9 +11014,9 @@ } }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -11516,7 +11080,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", "integrity": "sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw==", - "dev": true, "license": "MIT", "dependencies": { "is-extendable": "^0.1.1", @@ -11532,7 +11095,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", "integrity": "sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==", - "dev": true, "license": "MIT", "dependencies": { "is-buffer": "^1.0.2" @@ -11545,7 +11107,6 @@ "version": "0.2.7", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11600,18 +11161,6 @@ } } }, - "node_modules/sharp/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -11750,17 +11299,6 @@ "node": ">=8" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, "node_modules/socket.io": { "version": "4.8.3", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", @@ -11845,36 +11383,6 @@ "node": ">= 0.6" } }, - "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ip-address": "^10.0.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -11962,9 +11470,9 @@ } }, "node_modules/streamx": { - "version": "2.25.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", - "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", "dev": true, "license": "MIT", "dependencies": { @@ -12088,9 +11596,9 @@ } }, "node_modules/strnum": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.0.tgz", - "integrity": "sha512-sHrVyWWdq28RbhjuJdZsA1SnGRJV6NiXbk6AXBxDOsgAcA+lmpUZCYjOdLBxkXMwis6RRe7dlZt4VlIWFVzkmg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz", + "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==", "funding": [ { "type": "github", @@ -12100,7 +11608,7 @@ "license": "MIT", "optional": true, "dependencies": { - "anynum": "^1.0.0" + "anynum": "^1.0.1" } }, "node_modules/stubs": { @@ -12160,16 +11668,16 @@ } }, "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -12185,25 +11693,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tar-fs": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", - "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" - } - }, "node_modules/tar-stream": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", - "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", "dev": true, "license": "MIT", "dependencies": { @@ -12405,9 +11898,9 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", - "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", "dev": true, "license": "MIT", "dependencies": { @@ -12478,10 +11971,9 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz", - "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==", - "dev": true, + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", "license": "MIT" }, "node_modules/typedarray": { @@ -12586,7 +12078,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" @@ -12702,10 +12193,9 @@ } }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", - "dev": true, + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz", + "integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==", "license": "Apache-2.0" }, "node_modules/webidl-conversions": { @@ -12849,9 +12339,9 @@ } }, "node_modules/ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -12870,9 +12360,9 @@ } }, "node_modules/xml-naming": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", - "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", "funding": [ { "type": "github", @@ -12920,7 +12410,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "devOptional": true, "license": "ISC", "engines": { "node": ">=10" diff --git a/backend/package.json b/backend/package.json index 743f37dc..e5a16711 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,7 +13,7 @@ "dev:watch": "nodemon index.js", "build": "npx prisma generate", "create-meet": "node scripts/meet/create_meet_space.js", - "test": "jest" + "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js" }, "keywords": [ "zync", @@ -45,6 +45,9 @@ "nodemailer": "^9.0.1", "octokit": "^5.0.4", "pg": "^8.16.3", + "puppeteer": "^25.6.0", + "puppeteer-extra": "^3.3.6", + "puppeteer-extra-plugin-stealth": "^2.10.4", "redis": "^5.10.0", "rss-parser": "^3.13.0", "sharp": "^0.35.3", @@ -64,9 +67,6 @@ "mongodb-memory-server": "^11.2.0", "nodemon": "^3.1.9", "prisma": "^5.22.0", - "puppeteer": "^24.35.0", - "puppeteer-extra": "^3.3.6", - "puppeteer-extra-plugin-stealth": "^2.10.4", "supertest": "^7.2.2", "tsx": "^4.21.0", "typescript": "6.0.3" diff --git a/backend/routes/calendarRoutes.js b/backend/routes/calendarRoutes.js index 5b2b7ad2..4bd98f52 100644 --- a/backend/routes/calendarRoutes.js +++ b/backend/routes/calendarRoutes.js @@ -152,54 +152,29 @@ router.get('/holidays', verifyToken, async (req, res) => { // This is an if statement that checks if the 'ok' property of the 'response' object is 'false'. The 'ok' property is 'true' if the HTTP status code is in the 200-299 range, otherwise 'false'. // This checks if the external API request was unsuccessful (e.g., 5xx server error, 4xx client error other than 404), indicating a general failure to retrieve data. if (!response.ok) { - // If the 'response.ok' is 'false', this line sends an HTTP 502 (Bad Gateway) status code to the client with a generic JSON error message. 'return' stops further execution. - // This indicates that the server, acting as a gateway, received an invalid response from the upstream (Nager.Date) server, informing the client of an issue with the external service. - return res.status(502).json({ message: 'Failed to fetch holidays from Nager.Date API.' }); + return res.json([]); } - // Declares a constant variable 'data' and assigns it the result of asynchronously parsing the 'response' body as JSON. 'await' pauses execution until the JSON parsing is complete. - // This extracts the actual holiday data from the successful HTTP response received from the Nager.Date API, converting it from a raw JSON string into a JavaScript object. const data = await response.json(); + if (!Array.isArray(data)) { + return res.json([]); + } - // Declares a constant variable 'holidays' and assigns it a new array. 'data.map()' iterates over each item ('h') in the 'data' array and transforms it into a new object. - // This processes the raw holiday data received from the external API, mapping it to a standardized and potentially simplified format that is more suitable for the application's needs and client consumption. const holidays = data.map((h) => ({ - // Creates a 'date' property in the new object, assigning it the value of the 'date' property from the original holiday object 'h'. - // This extracts the date of the holiday, ensuring it's included in the standardized output. date: h.date, - // Creates a 'localName' property in the new object, assigning it the value of the 'localName' property from the original holiday object 'h'. - // This extracts the local name of the holiday, ensuring it's included in the standardized output. localName: h.localName, - // Creates a 'name' property in the new object, assigning it the value of the 'name' property from the original holiday object 'h'. - // This extracts the common name of the holiday, ensuring it's included in the standardized output. name: h.name, - // Creates a 'countryCode' property in the new object, assigning it the value of the 'countryCode' property from the original holiday object 'h'. - // This extracts the country code associated with the holiday, ensuring it's included in the standardized output. + countryCode: h.countryCode || countryCode, fixed: h.fixed, - // Creates a 'fixed' property in the new object, assigning it the value of the 'fixed' property from the original holiday object 'h'. - // This extracts the boolean indicating if the holiday has a fixed date, ensuring it's included in the standardized output. global: h.global, - // Creates a 'global' property in the new object, assigning it the value of the 'global' property from the original holiday object 'h'. - // This extracts the boolean indicating if the holiday is global for the country, ensuring it's included in the standardized output. types: h.types || [], })); - // Calls the 'set()' method on the 'holidayCache' Map, storing a new key-value pair. The 'cacheKey' is the key, and the value is an object containing the current timestamp (Date.now()) and the processed 'holidays' data. - // This stores the newly fetched and processed holiday data in the cache, along with a timestamp, so that subsequent requests for the same year and country can be served from the cache, improving performance. holidayCache.set(cacheKey, { timestamp: Date.now(), data: holidays }); - - // Sends an HTTP 200 (OK) status code to the client with the processed 'holidays' array as a JSON response. - // This sends the final, formatted holiday data back to the client, successfully fulfilling the API request. res.json(holidays); - // This keyword starts a 'catch' block, which executes if an error occurs in the preceding 'try' block. The 'error' object contains details about the exception. - // This provides a mechanism to gracefully handle any unexpected errors that might occur during the API call or data processing, preventing the server from crashing. } catch (error) { - // Calls the 'error()' method of the 'console' object to log an error message to the console, including a descriptive string and the 'error' object itself. - // This logs detailed error information to the server's console, which is crucial for debugging and monitoring issues in a production environment. - console.error('Error fetching holidays:', error); - // Sends an HTTP 500 (Internal Server Error) status code to the client with a generic JSON error message. - // This informs the client that an unexpected server-side error occurred, providing a general error message without exposing sensitive internal details. - res.status(500).json({ message: 'Server error fetching holidays.' }); + console.warn('Error fetching holidays from external service:', error); + res.json([]); } }); diff --git a/backend/routes/githubAppWebhook.js b/backend/routes/githubAppWebhook.js index 9c5dd120..a1ee500d 100644 --- a/backend/routes/githubAppWebhook.js +++ b/backend/routes/githubAppWebhook.js @@ -112,6 +112,7 @@ router.post('/webhook', verifyGithub, async (req, res) => { // Defines an HTTP P event, // Passes the GitHub event type to the job data. payload: req.body, // Passes the entire request body (the webhook payload) to the job data for processing. getIo: () => req.app.get('io'), // Provides a function to lazily retrieve the Socket.IO instance from the Express app, allowing the worker to emit real-time updates. + getTaskIO: () => req.app.get('taskIO'), // Provides a function to lazily retrieve the task socket namespace, allowing the worker to broadcast per-task Kanban updates (task-updated) to project rooms. }); debugWebhookLog( // Calls the debug logging function to output information about the enqueued webhook. diff --git a/backend/routes/internalMetrics.js b/backend/routes/internalMetrics.js index c4056992..944e7d9e 100644 --- a/backend/routes/internalMetrics.js +++ b/backend/routes/internalMetrics.js @@ -79,6 +79,8 @@ const express = require('express'); // Creates a new router object from Express. // This allows us to define modular, mountable route handlers, grouping related routes here before exporting them to the main application. const router = express.Router(); +// Imports the auth middleware to ensure only authenticated users can access internal metrics. +const authMiddleware = require('../middleware/authMiddleware'); // Uses object destructuring to import the 'getWebhookQueueMetrics' function from the specified module. // This is needed to access the specific function responsible for retrieving real-time metrics about the webhook processing queue. const { getWebhookQueueMetrics } = require('../services/webhookQueue'); @@ -89,7 +91,7 @@ const bytesToMb = (bytes) => Number((bytes / (1024 * 1024)).toFixed(2)); // Defines a GET route handler for the '/metrics' path using the Express router. // This sets up an API endpoint at '/metrics' that, when accessed via a GET request, will execute the provided function to gather and return system and application metrics. -router.get('/metrics', (_req, res) => { +router.get('/metrics', authMiddleware, (_req, res) => { // Calls the Node.js global 'process.memoryUsage()' function to get current memory statistics for the process. // This is needed to collect current memory statistics (like RSS, heapUsed, heapTotal) of the running Node.js application. const memoryUsage = process.memoryUsage(); diff --git a/backend/routes/projectRoutes.js b/backend/routes/projectRoutes.js index 367de948..a8056f70 100644 --- a/backend/routes/projectRoutes.js +++ b/backend/routes/projectRoutes.js @@ -1127,6 +1127,33 @@ router.post( const updatedProject = await getProjectWithSteps(projectId); invalidateProjectCache(project, [assignedTo].filter(Boolean)); + + // Broadcast task-created to all connected members of this project so + // the Kanban board updates live without a manual refresh. + const taskIO = req.app.get('taskIO'); + if (taskIO) { + const createdTask = await ProjectTask.findById(newTask._id).lean(); + taskIO.emitToProject(projectId, 'task-created', { + projectId, + stepId, + taskId: String(newTask._id), + task: createdTask, + actor: req.user.uid, + }); + // If an assignee was set, also notify that user directly so their + // Assigned Tasks view refreshes even if they haven't joined the + // project room yet. + if (assignedTo) { + taskIO.emitToUser(assignedTo, 'task-assigned', { + projectId, + stepId, + taskId: String(newTask._id), + task: createdTask, + actor: req.user.uid, + }); + } + } + res.status(201).json(updatedProject); } catch (error) { console.error('Error creating task:', error); @@ -1477,6 +1504,29 @@ router.post('/:projectId/quick-task', authMiddleware, async (req, res) => { const taskObj = normalizeDoc(newTask.toObject()); invalidateProjectCache(project, [assignedTo].filter(Boolean)); + + // Broadcast task-created to all connected members of this project so + // the Kanban board updates live without a manual refresh. + const taskIO = req.app.get('taskIO'); + if (taskIO) { + taskIO.emitToProject(projectId, 'task-created', { + projectId, + stepId: step._id?.toString() || step.id, + taskId: String(newTask._id), + task: taskObj, + actor: req.user.uid, + }); + if (assignedTo) { + taskIO.emitToUser(assignedTo, 'task-assigned', { + projectId, + stepId: step._id?.toString() || step.id, + taskId: String(newTask._id), + task: taskObj, + actor: req.user.uid, + }); + } + } + res.json({ message: 'Task created', task: taskObj, diff --git a/backend/routes/teamRoutes.js b/backend/routes/teamRoutes.js index 44616698..49db7a65 100644 --- a/backend/routes/teamRoutes.js +++ b/backend/routes/teamRoutes.js @@ -494,6 +494,7 @@ router.delete('/:teamId/members/:memberUid', verifyToken, async (req, res) => { ); // Invalidates the cache entry for the removed member, ensuring fresh data. await cache.invalidate(`user:me:${memberUid}`); + (team.members || []).forEach(mId => cache.invalidate(`user:me:${mId}`)); } // Runs an asynchronous synchronization task to remove the member from the team in Firebase. await runSync('remove-member', () => @@ -761,10 +762,12 @@ router.post('/:teamId/transfer-ownership', verifyToken, async (req, res) => { return res.status(400).json({ message: 'You are already the owner' }); } - await Team.findByIdAndUpdate(teamId, { $set: { ownerId: newOwnerId } }); + const updatedTeam = await Team.findByIdAndUpdate(teamId, { $set: { ownerId: newOwnerId } }, { returnDocument: 'after', lean: true }); await runSync('transfer-ownership', () => transferTeamOwnership(teamId, uid, newOwnerId) ); + + (updatedTeam.members || []).forEach(memberId => cache.invalidate(`user:me:${memberId}`)); res.status(200).json({ message: 'Ownership transferred successfully' }); } catch (error) { @@ -897,6 +900,9 @@ router.post('/:teamId/reject-member', verifyToken, async (req, res) => { { returnDocument: 'after', lean: true } ); + await runSync('reject-member-upsert', () => upsertTeamSnapshot(updatedTeam)); + (updatedTeam.members || []).forEach(memberId => cache.invalidate(`user:me:${memberId}`)); + const user = await User.findOne({ uid: userId }).lean(); if (user && user.email) { try { @@ -938,6 +944,9 @@ router.post('/:teamId/promote-admin', verifyToken, async (req, res) => { { returnDocument: 'after', lean: true } ); + await runSync('promote-admin-upsert', () => upsertTeamSnapshot(updatedTeam)); + (updatedTeam.members || []).forEach(memberId => cache.invalidate(`user:me:${memberId}`)); + res.status(200).json(normalizeDoc(updatedTeam)); } catch (error) { console.error('Error promoting admin:', error); @@ -962,6 +971,9 @@ router.post('/:teamId/demote-admin', verifyToken, async (req, res) => { { returnDocument: 'after', lean: true } ); + await runSync('demote-admin-upsert', () => upsertTeamSnapshot(updatedTeam)); + (updatedTeam.members || []).forEach(memberId => cache.invalidate(`user:me:${memberId}`)); + res.status(200).json(normalizeDoc(updatedTeam)); } catch (error) { console.error('Error demoting admin:', error); diff --git a/backend/services/githubWebhookWorker.js b/backend/services/githubWebhookWorker.js index 62713d34..6440f0a2 100644 --- a/backend/services/githubWebhookWorker.js +++ b/backend/services/githubWebhookWorker.js @@ -202,7 +202,7 @@ const findLinkedProject = async (repository) => { }; // WHAT: Process GitHub webhook payload. WHY: Orchestrates update logic. -const processGithubWebhookJob = async ({ deliveryId, event, payload, getIo }) => { +const processGithubWebhookJob = async ({ deliveryId, event, payload, getIo, getTaskIO }) => { // WHAT: Keep the stored installationId in lockstep with GitHub. // WHY: This is the ONLY place allowed to conclude "the app was uninstalled". // Inferring it from a failed API call is what used to strand users on the @@ -335,6 +335,20 @@ const processGithubWebhookJob = async ({ deliveryId, event, payload, getIo }) => } } await cache.invalidate(`projects:${linkedProject.ownerUid}`); + + // WHAT: Broadcast the status change to everyone viewing this project's board. + // WHY: Without this, the card only moves to "PR Raised" after a manual refresh - + // the owner notification above doesn't refresh anyone's Kanban board. + const taskIO = typeof getTaskIO === 'function' ? getTaskIO() : null; + if (taskIO) { + taskIO.emitToProject(String(linkedProject._id), 'task-updated', { + projectId: String(linkedProject._id), + stepId: String(task.stepId), + taskId: String(task._id), + changes: { status: 'PR Raised', githubPrUrl: pull_request.html_url, githubPrNumber: pull_request.number }, + actor: repository?.sender?.login || payload.sender?.login || 'github', + }); + } } return { processed: true, action: 'pr_raised_linked_to_task' }; @@ -418,7 +432,7 @@ const processGithubWebhookJob = async ({ deliveryId, event, payload, getIo }) => await cacheModule.invalidate(`projects:${task.assignedTo}`); } - const taskIO = req?.app?.get ? req.app.get('taskIO') : null; + const taskIO = typeof getTaskIO === 'function' ? getTaskIO() : null; if (taskIO) { taskIO.emitToProject(String(linkedProject._id), 'task-updated', { projectId: String(linkedProject._id), diff --git a/backend/services/teamFirebaseSync.js b/backend/services/teamFirebaseSync.js index 59331bf4..8ca0ab05 100644 --- a/backend/services/teamFirebaseSync.js +++ b/backend/services/teamFirebaseSync.js @@ -124,6 +124,8 @@ const upsertTeamSnapshot = async (team) => { // WHAT: Upserts a team into Firest const ownerId = extractOwnerUid(team); // WHAT: Extracts the owner's UID. WHY: Crucial for determining who controls the team. const memberIds = safeArray(team.members).map(normalizeUid).filter(Boolean); // WHAT: Normalizes the array of member IDs. WHY: Ensures all IDs are valid strings. const members = Array.from(new Set([...memberIds, ownerId].filter(Boolean))); // WHAT: Creates a deduplicated array of all members including the owner. WHY: Ensures the owner is always treated as a member and no duplicates exist. + const adminIds = safeArray(team.admins).map(normalizeUid).filter(Boolean); // WHAT: Normalizes the array of admin IDs. WHY: Needed for role syncing. + const pendingMemberIds = safeArray(team.pendingMembers).map(normalizeUid).filter(Boolean); // WHAT: Normalizes pending members. WHY: Syncs join requests. const now = new Date().toISOString(); // WHAT: Grabs the current ISO timestamp. WHY: Used for updated and synced timestamps. const payload = { // WHAT: Constructs the payload for Firestore. WHY: Maps the application's team structure to Firestore's schema. @@ -131,6 +133,8 @@ const upsertTeamSnapshot = async (team) => { // WHAT: Upserts a team into Firest ownerId, // WHAT: Sets the ownerId. WHY: Stores the primary owner. leaderId: ownerId, // WHAT: Mirrors ownerId to leaderId. WHY: Compatibility with older schemas or clients. members, // WHAT: Sets the array of member IDs. WHY: Allows querying for all members. + admins: adminIds, // WHAT: Sets the array of admin IDs. WHY: Allows querying for admins. + pendingMembers: pendingMemberIds, // WHAT: Sets the array of pending member IDs. WHY: Syncs pending join requests. inviteCode: team.inviteCode || '', // WHAT: Stores the invite code. WHY: Allows joining via link. logoId: team.logoId || 'rocket', // WHAT: Sets the logo ID. WHY: Default fallback to 'rocket'. type: team.type || 'Other', // WHAT: Sets the team type. WHY: Default fallback to 'Other'. diff --git a/backend/services/webhookQueue.js b/backend/services/webhookQueue.js index 00388170..5b7be352 100644 --- a/backend/services/webhookQueue.js +++ b/backend/services/webhookQueue.js @@ -170,7 +170,7 @@ const registerWebhookProcessor = (processor) => { // WHAT: Sets the processor fu webhookProcessor = processor; // WHAT: Assigns the variable. WHY: State update. }; -const enqueueWebhookJob = ({ deliveryId, event, payload, getIo }) => { // WHAT: Adds a new job to the queue. WHY: Entry point for incoming webhooks. +const enqueueWebhookJob = ({ deliveryId, event, payload, getIo, getTaskIO }) => { // WHAT: Adds a new job to the queue. WHY: Entry point for incoming webhooks. const normalizedDeliveryId = String(deliveryId || '').trim(); // WHAT: Normalizes the ID. WHY: Ensures consistent string matching. if (!normalizedDeliveryId) { // WHAT: Validates the ID. WHY: Deduplication is impossible without it. throw new Error('deliveryId is required for webhook queue idempotency'); // WHAT: Throws if missing. WHY: Enforces strict idempotency constraints. @@ -203,6 +203,7 @@ const enqueueWebhookJob = ({ deliveryId, event, payload, getIo }) => { // WHAT: event: job.event, // WHAT: Passes event. WHY: Might be needed by processor. payload: payload || {}, // WHAT: Passes the actual webhook payload. WHY: The data to be processed. getIo: typeof getIo === 'function' ? getIo : null, // WHAT: Passes the websocket getter. WHY: Allows the processor to emit real-time updates. + getTaskIO: typeof getTaskIO === 'function' ? getTaskIO : null, // WHAT: Passes the task-socket-namespace getter. WHY: Allows the processor to broadcast per-task Kanban updates (task-updated) without holding a reference to the raw `req`. }); pruneOldJobs(); // WHAT: Triggers a cleanup. WHY: Ensures map doesn't grow indefinitely on every enqueue. scheduleDrain(); // WHAT: Kicks off processing. WHY: Ensures the job will eventually be run. diff --git a/backend/test_github_jwt.js b/backend/test_github_jwt.js new file mode 100644 index 00000000..e2c0f067 --- /dev/null +++ b/backend/test_github_jwt.js @@ -0,0 +1,13 @@ +require('dotenv').config(); +const jwt = require('jsonwebtoken'); +const axios = require('axios'); +const now = Math.floor(Date.now() / 1000); +const payload = { iat: now - 60, exp: now + 5 * 60, iss: process.env.GITHUB_APP_ID }; +const privateKey = process.env.GITHUB_PRIVATE_KEY; +const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' }); + +axios.get('https://api.github.com/app', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github.v3+json' } }) + .then(res => console.log('SUCCESS:', res.data.name)) + .catch(err => { + console.error('FAILED:', err.response ? err.response.status + ' ' + err.response.data.message : err.message); + }); diff --git a/backend/tests/__mocks__/octokit.js b/backend/tests/__mocks__/octokit.js new file mode 100644 index 00000000..70cc3916 --- /dev/null +++ b/backend/tests/__mocks__/octokit.js @@ -0,0 +1,12 @@ +let mockRequestFn = jest.fn(async () => ({ data: [] })); + +class MockApp { + getInstallationOctokit() { + return Promise.resolve({ + request: mockRequestFn, + }); + } +} + +module.exports = { App: MockApp }; +module.exports.__setMockRequest = (fn) => { mockRequestFn = fn; }; diff --git a/backend/tests/githubAppWebhookAggregation.test.js b/backend/tests/githubAppWebhookAggregation.test.js index de7a61b7..53c6cd7e 100644 --- a/backend/tests/githubAppWebhookAggregation.test.js +++ b/backend/tests/githubAppWebhookAggregation.test.js @@ -81,6 +81,7 @@ const { } = require('../services/webhookQueue'); jest.mock('../middleware/verifyGithub', () => (_req, _res, next) => next()); +jest.mock('../middleware/authMiddleware', () => (_req, _res, next) => next()); const mockProjectFindOne = jest.fn(); const mockProjectUpdateOne = jest.fn(); diff --git a/backend/tests/githubAppWebhookIdempotency.test.js b/backend/tests/githubAppWebhookIdempotency.test.js index 908997c2..93cb9dfc 100644 --- a/backend/tests/githubAppWebhookIdempotency.test.js +++ b/backend/tests/githubAppWebhookIdempotency.test.js @@ -77,6 +77,7 @@ const request = require('supertest'); const express = require('express'); jest.mock('../middleware/verifyGithub', () => (_req, _res, next) => next()); +jest.mock('../middleware/authMiddleware', () => (_req, _res, next) => next()); const mockProcessGithubWebhookJob = jest.fn(); jest.mock('../services/githubWebhookWorker', () => ({ diff --git a/backend/tests/githubWebhookWorker.test.js b/backend/tests/githubWebhookWorker.test.js new file mode 100644 index 00000000..e42907d0 --- /dev/null +++ b/backend/tests/githubWebhookWorker.test.js @@ -0,0 +1,196 @@ +const mongoose = require('mongoose'); +const { MongoMemoryServer } = require('mongodb-memory-server'); + +jest.setTimeout(30000); +process.env.GEMINI_API_KEY_SECONDARY = 'mock-key'; +process.env.ENCRYPTION_KEY = 'mock-key'; + +jest.mock('../utils/commitAnalysisService.js', () => ({ + analyzeCommit: jest.fn(() => Promise.resolve({ found: false })), +})); +jest.mock('../utils/githubInstallation.js', () => ({ + persistInstallationId: jest.fn(() => Promise.resolve()), + invalidateInstallationCaches: jest.fn(() => Promise.resolve()), +})); +jest.mock('../utils/cache.js', () => ({ + getJson: jest.fn(() => null), + setJson: jest.fn(), + invalidate: jest.fn(() => Promise.resolve()), +})); + +const Project = require('../models/Project'); +const Step = require('../models/Step'); +const ProjectTask = require('../models/ProjectTask'); +const User = require('../models/User'); +const { processGithubWebhookJob } = require('../services/githubWebhookWorker'); + +let mongoServer; + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + if (mongoose.connection.readyState !== 0) await mongoose.disconnect(); + await mongoose.connect(mongoServer.getUri()); +}); + +afterAll(async () => { + await mongoose.connection.dropDatabase(); + await mongoose.connection.close(); + await mongoose.disconnect(); + await mongoServer?.stop(); +}); + +beforeEach(async () => { + jest.clearAllMocks(); + for (const k of Object.keys(mongoose.connection.collections)) + await mongoose.connection.collections[k].deleteMany({}); + const user = await User.create({ uid: 'owner-uid', email: 'o@t.com', displayName: 'Owner' }); + await Project.create({ + name: 'P', description: 'd', ownerId: user._id, ownerUid: 'owner-uid', + team: [], githubRepoOwner: 'owner-gh', githubRepoName: 'repo', + }); +}); + +async function seedTask(overrides = {}) { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + const step = await Step.create({ title: 'Backlog', order: 0, projectId: project._id }); + return ProjectTask.create({ + title: 'Test Task', + status: 'Ready', + assignedTo: 'assignee-uid', + stepId: step._id, + githubBranchName: 'task/test-task-abc123', + completionCommitMessage: 'Complete Task: abc123', + ...overrides, + }); +} + +const mockTaskIO = { emitToProject: jest.fn() }; + +function pushPayload(branchName, commits = []) { + return { + event: 'push', + payload: { + ref: `refs/heads/${branchName}`, + commits, + repository: { id: 99, name: 'repo', full_name: 'owner-gh/repo' }, + sender: { login: 'octocat' }, + }, + getIo: () => ({ emit: jest.fn(), to: () => ({ emit: jest.fn() }) }), + getTaskIO: () => mockTaskIO, + }; +} + +function prPayload(branchName, prNumber = 42) { + return { + event: 'pull_request', + payload: { + action: 'opened', + pull_request: { head: { ref: branchName }, html_url: 'https://github.com/owner-gh/repo/pull/42', number: prNumber }, + repository: { id: 99, name: 'repo', full_name: 'owner-gh/repo' }, + sender: { login: 'octocat' }, + }, + getIo: () => ({ emit: jest.fn(), to: () => ({ emit: jest.fn() }) }), + getTaskIO: () => mockTaskIO, + }; +} + +// W1: push to task/* branch → In Progress +test('W1: push to task/* branch updates status to In Progress', async () => { + const task = await seedTask({ status: 'Ready' }); + const result = await processGithubWebhookJob(pushPayload('task/test-task-abc123', [ + { id: 'abc123def456', message: 'initial commit', added: ['a.js'], modified: [], removed: [] }, + ])); + const updated = await ProjectTask.findById(task._id); + expect(updated.status).toBe('In Progress'); +}); + +// W2: push with completion commit → Done +test('W2: push with completion commit updates status to Done', async () => { + const task = await seedTask({ status: 'In Progress' }); + const result = await processGithubWebhookJob(pushPayload('task/test-task-abc123', [ + { id: 'abc123def456', message: 'Complete Task: abc123', added: [], modified: ['b.js'], removed: [] }, + ])); + const updated = await ProjectTask.findById(task._id); + expect(updated.status).toBe('Done'); + expect(updated.commitCode).toBe('abc123d'); + expect(updated.commitMessage).toBe('Complete Task: abc123'); +}); + +// W3: push when already Done → no regression +test('W3: push when already Done does not regress status', async () => { + const task = await seedTask({ status: 'Done' }); + await processGithubWebhookJob(pushPayload('task/test-task-abc123', [ + { id: 'newcommit12345', message: 'fix something', added: [], modified: ['c.js'], removed: [] }, + ])); + const updated = await ProjectTask.findById(task._id); + expect(updated.status).toBe('Done'); +}); + +// W4: push when already PR Raised → no regression +test('W4: push when already PR Raised does not regress status', async () => { + const task = await seedTask({ status: 'PR Raised' }); + await processGithubWebhookJob(pushPayload('task/test-task-abc123', [ + { id: 'newcommit12345', message: 'fix something', added: [], modified: ['c.js'], removed: [] }, + ])); + const updated = await ProjectTask.findById(task._id); + expect(updated.status).toBe('PR Raised'); +}); + +// W5: push to non-task branch → no task update +test('W5: push to non-task branch does not update any task', async () => { + const task = await seedTask({ status: 'Ready' }); + await processGithubWebhookJob(pushPayload('main', [ + { id: 'abc123def456', message: 'commit on main', added: [], modified: ['d.js'], removed: [] }, + ])); + const updated = await ProjectTask.findById(task._id); + expect(updated.status).toBe('Ready'); +}); + +// W6: PR opened on task branch → PR Raised +test('W6: PR opened on task branch updates status to PR Raised', async () => { + const task = await seedTask({ status: 'Done' }); + await processGithubWebhookJob(prPayload('task/test-task-abc123')); + const updated = await ProjectTask.findById(task._id); + expect(updated.status).toBe('PR Raised'); + expect(updated.githubPrUrl).toBe('https://github.com/owner-gh/repo/pull/42'); + expect(updated.githubPrNumber).toBe(42); +}); + +// W7: PR opened on non-task branch → ignored +test('W7: PR opened on non-task branch is ignored', async () => { + const task = await seedTask({ status: 'Ready' }); + const result = await processGithubWebhookJob(prPayload('feature/some-feature')); + expect(result.ignored).toBe(true); + const updated = await ProjectTask.findById(task._id); + expect(updated.status).toBe('Ready'); +}); + +// W8: push to task/* branch broadcasts task-updated so the Kanban board moves live +test('W8: push to task/* branch emits task-updated via taskIO', async () => { + const task = await seedTask({ status: 'Ready' }); + await processGithubWebhookJob(pushPayload('task/test-task-abc123', [ + { id: 'abc123def456', message: 'initial commit', added: ['a.js'], modified: [], removed: [] }, + ])); + expect(mockTaskIO.emitToProject).toHaveBeenCalledWith( + expect.any(String), + 'task-updated', + expect.objectContaining({ + taskId: String(task._id), + changes: expect.objectContaining({ status: 'In Progress' }), + }) + ); +}); + +// W9: PR opened on task branch broadcasts task-updated so the Kanban board moves live +test('W9: PR opened on task branch emits task-updated via taskIO', async () => { + const task = await seedTask({ status: 'Done' }); + await processGithubWebhookJob(prPayload('task/test-task-abc123')); + expect(mockTaskIO.emitToProject).toHaveBeenCalledWith( + expect.any(String), + 'task-updated', + expect.objectContaining({ + taskId: String(task._id), + changes: expect.objectContaining({ status: 'PR Raised', githubPrNumber: 42 }), + }) + ); +}); diff --git a/backend/tests/internalMetricsAndLoadShedding.test.js b/backend/tests/internalMetricsAndLoadShedding.test.js index a1dbb395..977e75ff 100644 --- a/backend/tests/internalMetricsAndLoadShedding.test.js +++ b/backend/tests/internalMetricsAndLoadShedding.test.js @@ -76,6 +76,8 @@ const request = require('supertest'); const express = require('express'); +jest.mock('../middleware/authMiddleware', () => (_req, _res, next) => next()); + const ORIGINAL_MEMORY_USAGE = process.memoryUsage; describe('internal metrics route', () => { diff --git a/backend/tests/projectHelper.test.js b/backend/tests/projectHelper.test.js new file mode 100644 index 00000000..6286af5a --- /dev/null +++ b/backend/tests/projectHelper.test.js @@ -0,0 +1,81 @@ +const mongoose = require('mongoose'); +const { MongoMemoryServer } = require('mongodb-memory-server'); + +jest.setTimeout(30000); + +const Project = require('../models/Project'); +const Step = require('../models/Step'); +const ProjectTask = require('../models/ProjectTask'); +const User = require('../models/User'); +const { getProjectWithSteps, getProjectsWithSteps } = require('../utils/projectHelper'); + +let mongoServer; + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + if (mongoose.connection.readyState !== 0) await mongoose.disconnect(); + await mongoose.connect(mongoServer.getUri()); +}); + +afterAll(async () => { + await mongoose.connection.dropDatabase(); + await mongoose.connection.close(); + await mongoose.disconnect(); + await mongoServer?.stop(); +}); + +beforeEach(async () => { + for (const k of Object.keys(mongoose.connection.collections)) + await mongoose.connection.collections[k].deleteMany({}); +}); + +async function seedFullProject() { + const user = await User.create({ uid: 'owner-uid', email: 'o@t.com', displayName: 'Owner' }); + const project = await Project.create({ + name: 'TestProject', description: 'desc', + ownerId: user._id, ownerUid: 'owner-uid', team: [], + }); + const step1 = await Step.create({ title: 'Step 1', order: 0, projectId: project._id }); + const step2 = await Step.create({ title: 'Step 2', order: 1, projectId: project._id }); + const task1 = await ProjectTask.create({ title: 'Task A', status: 'Ready', stepId: step1._id, commitCode: '1111111111' }); + const task2 = await ProjectTask.create({ title: 'Task B', status: 'Done', stepId: step2._id, commitCode: '2222222222' }); + return { project, step1, step2, task1, task2, user }; +} + +// P1: getProjectWithSteps returns nested structure +test('P1: getProjectWithSteps returns nested project > steps > tasks', async () => { + const { project, step1, step2, task1, task2 } = await seedFullProject(); + const result = await getProjectWithSteps(project._id); + expect(result).toBeTruthy(); + expect(result.id).toBe(String(project._id)); + expect(result.steps).toHaveLength(2); + expect(result.steps[0].title).toBe('Step 1'); + expect(result.steps[0].tasks).toHaveLength(1); + expect(result.steps[0].tasks[0].title).toBe('Task A'); + expect(result.steps[1].title).toBe('Step 2'); + expect(result.steps[1].tasks[0].title).toBe('Task B'); + expect(result.owner).toBeTruthy(); + expect(result.owner.displayName).toBe('Owner'); +}); + +// P2: getProjectsWithSteps batch fetches efficiently +test('P2: getProjectsWithSteps returns all projects with nested steps and tasks', async () => { + await seedFullProject(); + const user2 = await User.create({ uid: 'owner2', email: 'o2@t.com', displayName: 'Owner2' }); + const proj2 = await Project.create({ name: 'Proj2', ownerId: user2._id, ownerUid: 'owner2' }); + await Step.create({ title: 'S1', order: 0, projectId: proj2._id }); + + const results = await getProjectsWithSteps({}); + expect(results).toHaveLength(2); + const names = results.map(p => p.name).sort(); + expect(names).toEqual(['Proj2', 'TestProject']); + const tp = results.find(p => p.name === 'TestProject'); + expect(tp.steps).toHaveLength(2); + expect(tp.steps[0].tasks).toHaveLength(1); +}); + +// P3: getProjectsWithSteps empty result +test('P3: getProjectsWithSteps returns empty array when no projects match', async () => { + const results = await getProjectsWithSteps({ name: 'Nonexistent' }); + expect(results).toEqual([]); +}); diff --git a/backend/tests/projectRoutesTasks.test.js b/backend/tests/projectRoutesTasks.test.js new file mode 100644 index 00000000..baa4e169 --- /dev/null +++ b/backend/tests/projectRoutesTasks.test.js @@ -0,0 +1,348 @@ +const express = require('express'); +const request = require('supertest'); +const mongoose = require('mongoose'); +const { MongoMemoryServer } = require('mongodb-memory-server'); + +jest.setTimeout(30000); +process.env.GEMINI_API_KEY_SECONDARY = 'mock-key'; +process.env.ENCRYPTION_KEY = 'mock-key'; + +jest.mock('../middleware/authMiddleware.js', () => + jest.fn((req, res, next) => { + req.user = { uid: req.headers['x-test-uid'] || 'owner-uid' }; + next(); + }) +); +jest.mock('../services/mailer.js', () => ({ sendZyncEmail: jest.fn(() => Promise.resolve()) })); +jest.mock('../utils/emailTemplates.js', () => ({ getTaskAssignmentEmailHtml: jest.fn(() => '

mock

') })); +jest.mock('../utils/cache.js', () => ({ getJson: jest.fn(() => null), setJson: jest.fn(), invalidate: jest.fn(() => Promise.resolve()) })); +jest.mock('../utils/githubInstallation.js', () => ({ + getInstallationOctokit: jest.fn(() => Promise.resolve({ request: jest.fn(() => Promise.resolve({ data: {} })) })), + invalidateInstallationCaches: jest.fn(() => Promise.resolve()), +})); + +const { sendZyncEmail } = require('../services/mailer.js'); +const ProjectTask = require('../models/ProjectTask'); +const Project = require('../models/Project'); +const Step = require('../models/Step'); +const User = require('../models/User'); +const projectRoutes = require('../routes/projectRoutes'); + +const mockIo = { emit: jest.fn() }; +const mockTaskIO = { emitToProject: jest.fn(), emitToUser: jest.fn() }; +let app, mongoServer; + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + if (mongoose.connection.readyState !== 0) await mongoose.disconnect(); + await mongoose.connect(mongoServer.getUri()); + app = express(); + app.use(express.json()); + app.get = jest.fn((k) => { + if (k === 'io') return mockIo; + if (k === 'taskIO') return mockTaskIO; + return undefined; + }); + app.use('/projects', projectRoutes); +}); + +afterAll(async () => { + await mongoose.connection.dropDatabase(); + await mongoose.connection.close(); + await mongoose.disconnect(); + await mongoServer?.stop(); +}); + +beforeEach(async () => { + jest.clearAllMocks(); + for (const k of Object.keys(mongoose.connection.collections)) + await mongoose.connection.collections[k].deleteMany({}); + const owner = await User.create({ uid: 'owner-uid', email: 'o@t.com', displayName: 'Owner' }); + await User.create({ uid: 'member-uid', email: 'm@t.com', displayName: 'Member' }); + await Project.create({ + name: 'P', description: 'd', ownerId: owner._id, ownerUid: 'owner-uid', + team: ['member-uid'], + }); +}); + +async function seedStep(title = 'Backlog') { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + return Step.create({ title, order: 0, projectId: project._id }); +} + +async function seedTask(overrides = {}) { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + const step = await Step.findOne({ projectId: project._id }) || await seedStep(); + return ProjectTask.create({ + title: 'Test Task', status: 'Ready', stepId: step._id, + commitCode: String(Date.now()) + Math.floor(Math.random() * 1000), + ...overrides, + }); +} + +// B10: POST task creates task with status Ready +test('B10: POST task creates task with status Ready', async () => { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + const step = await seedStep(); + const res = await request(app) + .post(`/projects/${project._id}/steps/${step._id}/tasks`) + .set('x-test-uid', 'owner-uid') + .send({ title: 'New Task', description: 'Desc' }); + expect(res.status).toBe(201); + const task = await ProjectTask.findOne({ title: 'New Task' }); + expect(task).toBeTruthy(); + expect(task.status).toBe('Ready'); + expect(task.stepId.toString()).toBe(step._id.toString()); +}); + +// B11: POST task rejects missing title +test('B11: POST task rejects missing title', async () => { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + const step = await seedStep(); + const res = await request(app) + .post(`/projects/${project._id}/steps/${step._id}/tasks`) + .set('x-test-uid', 'owner-uid') + .send({ description: 'No title' }); + expect(res.status).toBe(400); + expect(res.body.message).toMatch(/title is required/i); +}); + +// B12: POST task rejects non-team-member +test('B12: POST task rejects non-team-member', async () => { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + const step = await seedStep(); + await User.create({ uid: 'outsider', email: 'out@t.com', displayName: 'Out' }); + const res = await request(app) + .post(`/projects/${project._id}/steps/${step._id}/tasks`) + .set('x-test-uid', 'outsider') + .send({ title: 'X' }); + expect(res.status).toBe(403); +}); + +// B13: PUT task updates status +test('B13: PUT task updates status', async () => { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + const step = await seedStep(); + const task = await seedTask(); + const res = await request(app) + .put(`/projects/${project._id}/steps/${step._id}/tasks/${task._id}`) + .set('x-test-uid', 'owner-uid') + .send({ status: 'In Progress' }); + expect(res.status).toBe(200); + const updated = await ProjectTask.findById(task._id); + expect(updated.status).toBe('In Progress'); +}); + +// B14: PUT task emits socket event task-updated +test('B14: PUT task emits task-updated socket event', async () => { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + const step = await seedStep(); + const task = await seedTask(); + await request(app) + .put(`/projects/${project._id}/steps/${step._id}/tasks/${task._id}`) + .set('x-test-uid', 'owner-uid') + .send({ status: 'Active' }); + expect(mockTaskIO.emitToProject).toHaveBeenCalledWith( + String(project._id), 'task-updated', + expect.objectContaining({ taskId: String(task._id), actor: 'owner-uid' }) + ); +}); + +// B15: PUT task reassigns and sends email +test('B15: PUT task reassigns and sends email', async () => { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + const step = await seedStep(); + const task = await seedTask({ assignedTo: null }); + const res = await request(app) + .put(`/projects/${project._id}/steps/${step._id}/tasks/${task._id}`) + .set('x-test-uid', 'owner-uid') + .send({ assignedTo: 'member-uid', assignedToName: 'Member' }); + expect(res.status).toBe(200); + expect(sendZyncEmail).toHaveBeenCalledWith('m@t.com', expect.any(String), expect.any(String), expect.any(String)); + const updated = await ProjectTask.findById(task._id); + expect(updated.assignedTo).toBe('member-uid'); + expect(updated.assignedToName).toBe('Member'); +}); + +// B16: DELETE task removes task +test('B16: DELETE task removes task', async () => { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + const step = await seedStep(); + const task = await seedTask(); + const res = await request(app) + .delete(`/projects/${project._id}/steps/${step._id}/tasks/${task._id}`) + .set('x-test-uid', 'owner-uid'); + expect(res.status).toBe(200); + expect(await ProjectTask.findById(task._id)).toBeNull(); +}); + +// B17: DELETE task rejects non-owner +test('B17: DELETE task rejects non-owner', async () => { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + const step = await seedStep(); + const task = await seedTask(); + const res = await request(app) + .delete(`/projects/${project._id}/steps/${step._id}/tasks/${task._id}`) + .set('x-test-uid', 'member-uid'); + expect(res.status).toBe(403); + expect(res.body.message).toMatch(/only.*owner/i); +}); + +// B18: DELETE task blocks when PR Raised +test('B18: DELETE task blocks when PR Raised', async () => { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + const step = await seedStep(); + const task = await seedTask({ status: 'PR Raised' }); + const res = await request(app) + .delete(`/projects/${project._id}/steps/${step._id}/tasks/${task._id}`) + .set('x-test-uid', 'owner-uid'); + expect(res.status).toBe(400); + expect(res.body.message).toMatch(/pull request/i); +}); + +// B19: POST quick-task creates task +test('B19: POST quick-task creates task', async () => { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + const res = await request(app) + .post(`/projects/${project._id}/quick-task`) + .set('x-test-uid', 'owner-uid') + .send({ title: 'Quick Task' }); + expect(res.status).toBe(200); + expect(res.body.task).toBeTruthy(); + expect(res.body.task.title).toBe('Quick Task'); + expect(res.body.task.status).toBe('Ready'); +}); + +// B20: POST quick-task auto-creates Backlog step +test('B20: POST quick-task auto-creates Backlog step', async () => { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + const res = await request(app) + .post(`/projects/${project._id}/quick-task`) + .set('x-test-uid', 'owner-uid') + .send({ title: 'Auto Step Task' }); + expect(res.status).toBe(200); + const step = await Step.findOne({ projectId: project._id }); + expect(step).toBeTruthy(); + expect(step.title).toBe('Backlog'); +}); + +// B21: POST quick-task rejects non-team-member +test('B21: POST quick-task rejects non-team-member', async () => { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + await User.create({ uid: 'outsider2', email: 'out2@t.com', displayName: 'Out2' }); + const res = await request(app) + .post(`/projects/${project._id}/quick-task`) + .set('x-test-uid', 'outsider2') + .send({ title: 'X' }); + expect(res.status).toBe(403); +}); + +// B22: PUT task on non-existent task returns 404 +test('B22: PUT task on non-existent task returns 404', async () => { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + const step = await seedStep(); + const fakeId = new mongoose.Types.ObjectId(); + const res = await request(app) + .put(`/projects/${project._id}/steps/${step._id}/tasks/${fakeId}`) + .set('x-test-uid', 'owner-uid') + .send({ status: 'Done' }); + expect(res.status).toBe(404); + expect(res.body.message).toMatch(/task not found/i); +}); + +// B23: DELETE task emits task-deleted socket event +test('B23: DELETE task emits task-deleted socket event', async () => { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + const step = await seedStep(); + const task = await seedTask(); + await request(app) + .delete(`/projects/${project._id}/steps/${step._id}/tasks/${task._id}`) + .set('x-test-uid', 'owner-uid'); + expect(mockTaskIO.emitToProject).toHaveBeenCalledWith( + String(project._id), 'task-deleted', + expect.objectContaining({ taskId: String(task._id), actor: 'owner-uid' }) + ); +}); + +// B24: PUT task emits projectUpdate via io +test('B24: PUT task emits projectUpdate via io', async () => { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + const step = await seedStep(); + const task = await seedTask(); + await request(app) + .put(`/projects/${project._id}/steps/${step._id}/tasks/${task._id}`) + .set('x-test-uid', 'owner-uid') + .send({ status: 'Done' }); + expect(mockIo.emit).toHaveBeenCalledWith('projectUpdate', expect.objectContaining({ projectId: String(project._id) })); +}); + +// B25: POST task emits task-created socket event +test('B25: POST task emits task-created socket event', async () => { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + const step = await seedStep(); + const res = await request(app) + .post(`/projects/${project._id}/steps/${step._id}/tasks`) + .set('x-test-uid', 'owner-uid') + .send({ title: 'Socket Task', description: 'Desc' }); + expect(res.status).toBe(201); + expect(mockTaskIO.emitToProject).toHaveBeenCalledWith( + String(project._id), 'task-created', + expect.objectContaining({ + projectId: String(project._id), + stepId: String(step._id), + actor: 'owner-uid', + }) + ); +}); + +// B26: POST task with assignee emits task-assigned to the assignee +test('B26: POST task with assignee emits task-assigned to assignee', async () => { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + const step = await seedStep(); + const res = await request(app) + .post(`/projects/${project._id}/steps/${step._id}/tasks`) + .set('x-test-uid', 'owner-uid') + .send({ title: 'Assigned Task', assignedTo: 'member-uid', assignedToName: 'Member' }); + expect(res.status).toBe(201); + expect(mockTaskIO.emitToUser).toHaveBeenCalledWith( + 'member-uid', 'task-assigned', + expect.objectContaining({ + projectId: String(project._id), + actor: 'owner-uid', + }) + ); +}); + +// B27: POST quick-task emits task-created socket event +test('B27: POST quick-task emits task-created socket event', async () => { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + const res = await request(app) + .post(`/projects/${project._id}/quick-task`) + .set('x-test-uid', 'owner-uid') + .send({ title: 'Quick Socket Task' }); + expect(res.status).toBe(200); + expect(mockTaskIO.emitToProject).toHaveBeenCalledWith( + String(project._id), 'task-created', + expect.objectContaining({ + projectId: String(project._id), + actor: 'owner-uid', + }) + ); +}); + +// B28: POST quick-task with assignee emits task-assigned to assignee +test('B28: POST quick-task with assignee emits task-assigned to assignee', async () => { + const project = await Project.findOne({ ownerUid: 'owner-uid' }); + const res = await request(app) + .post(`/projects/${project._id}/quick-task`) + .set('x-test-uid', 'owner-uid') + .send({ title: 'Quick Assigned', assignedTo: 'member-uid', assignedToName: 'Member' }); + expect(res.status).toBe(200); + expect(mockTaskIO.emitToUser).toHaveBeenCalledWith( + 'member-uid', 'task-assigned', + expect.objectContaining({ + projectId: String(project._id), + actor: 'owner-uid', + }) + ); +}); diff --git a/backend/tests/taskRoutes.test.js b/backend/tests/taskRoutes.test.js new file mode 100644 index 00000000..976cef6c --- /dev/null +++ b/backend/tests/taskRoutes.test.js @@ -0,0 +1,205 @@ +const express = require('express'); +const request = require('supertest'); +const mongoose = require('mongoose'); +const { MongoMemoryServer } = require('mongodb-memory-server'); + +jest.setTimeout(30000); +process.env.GEMINI_API_KEY_SECONDARY = 'mock-key'; +process.env.ENCRYPTION_KEY = 'mock-key'; +process.env.GITHUB_APP_ID = 'mock'; +process.env.GITHUB_PRIVATE_KEY = 'mock'; + +jest.mock('../middleware/authMiddleware.js', () => + jest.fn((req, res, next) => { + req.user = { uid: req.headers['x-test-uid'] || 'owner-uid' }; + next(); + }) +); +jest.mock('../services/mailer.js', () => ({ sendZyncEmail: jest.fn(() => Promise.resolve()) })); +jest.mock('../services/pushNotificationService.js', () => ({ sendPushNotification: jest.fn(() => Promise.resolve()) })); +jest.mock('../utils/emailTemplates.js', () => ({ getTaskAssignmentEmailHtml: jest.fn(() => '

mock

') })); +jest.mock('../utils/cache.js', () => ({ getJson: jest.fn(() => null), setJson: jest.fn(), invalidate: jest.fn(() => Promise.resolve()) })); +jest.mock('../utils/githubInstallation.js', () => ({ getInstallationOctokit: jest.fn(), invalidateInstallationCaches: jest.fn() })); + +const octokitMock = require('octokit'); + +const { sendZyncEmail } = require('../services/mailer.js'); +const { sendPushNotification } = require('../services/pushNotificationService.js'); +const { getInstallationOctokit } = require('../utils/githubInstallation.js'); +const ProjectTask = require('../models/ProjectTask'); +const Project = require('../models/Project'); +const Step = require('../models/Step'); +const User = require('../models/User'); +const Team = require('../models/Team'); +const Session = require('../models/Session'); +const taskRoutes = require('../routes/taskRoutes'); + +const mockTaskIO = { emitToProject: jest.fn(), emitToUser: jest.fn() }; +let app, mongoServer; + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + if (mongoose.connection.readyState !== 0) await mongoose.disconnect(); + await mongoose.connect(mongoServer.getUri()); + app = express(); + app.use(express.json()); + app.get = jest.fn((k) => (k === 'taskIO' ? mockTaskIO : undefined)); + app.use('/tasks', taskRoutes); +}); + +afterAll(async () => { + await mongoose.connection.dropDatabase(); + await mongoose.connection.close(); + await mongoose.disconnect(); + await mongoServer?.stop(); +}); + +beforeEach(async () => { + jest.clearAllMocks(); + for (const k of Object.keys(mongoose.connection.collections)) + await mongoose.connection.collections[k].deleteMany({}); + await User.create({ uid: 'owner-uid', email: 'o@t.com', displayName: 'Owner', githubIntegration: { installationId: 123, username: 'owner-gh' } }); + await User.create({ uid: 'assignee-uid', email: 'a@t.com', displayName: 'Assignee', githubIntegration: { installationId: 456, username: 'assignee-gh' } }); + await Team.create({ name: 'T', inviteCode: 'CODE123', ownerId: 'owner-uid', members: ['owner-uid', 'assignee-uid'] }); +}); + +async function seedProject() { + const owner = await User.findOne({ uid: 'owner-uid' }); + return Project.create({ name: 'P', description: 'd', ownerId: owner._id, ownerUid: 'owner-uid', team: ['assignee-uid'], githubRepoOwner: 'owner-gh', githubRepoName: 'repo' }); +} + +function mockOctokit(logins = ['assignee-gh']) { + octokitMock.__setMockRequest(jest.fn(async (route) => { + if (route.includes('collaborators')) return { data: logins.map(l => ({ login: l })) }; + if (route.includes('invitations')) return { data: [] }; + return { data: {} }; + })); +} + +// B1: creates task with correct fields +test('B1: POST /assign creates task with correct fields', async () => { + const proj = await seedProject(); + mockOctokit(); + const res = await request(app) + .post('/tasks/assign') + .set('x-test-uid', 'owner-uid') + .send({ projectId: proj._id, taskName: 'My Task', description: 'Desc', assignedUserId: 'assignee-uid' }); + expect(res.status).toBe(200); + expect(res.body.tasks).toHaveLength(1); + const t = res.body.tasks[0]; + expect(t.title).toBe('My Task'); + expect(t.status).toBe('Pending'); + expect(t.assignedTo).toBe('assignee-uid'); + expect(t.commitCode).toMatch(/^\d{10}$/); + const db = await ProjectTask.findById(t.id); + expect(db.stepId).toBeDefined(); +}); + +// B2: rejects self-assignment +test('B2: POST /assign rejects self-assignment', async () => { + const proj = await seedProject(); + mockOctokit(); + const res = await request(app) + .post('/tasks/assign') + .set('x-test-uid', 'owner-uid') + .send({ projectId: proj._id, taskName: 'Self', assignedUserId: 'owner-uid' }); + expect(res.status).toBe(400); + expect(res.body.message).toMatch(/cannot assign.*yourself/i); +}); + +// B3: rejects non-owner +test('B3: POST /assign rejects non-owner', async () => { + const proj = await seedProject(); + mockOctokit(); + const res = await request(app) + .post('/tasks/assign') + .set('x-test-uid', 'assignee-uid') + .send({ projectId: proj._id, taskName: 'X', assignedUserId: 'owner-uid' }); + expect(res.status).toBe(403); + expect(res.body.message).toMatch(/only.*owner/i); +}); + +// B4: rejects non-team-member assignee +test('B4: POST /assign rejects non-team-member assignee', async () => { + const proj = await seedProject(); + await User.create({ uid: 'outsider', email: 'out@t.com', displayName: 'Out', githubIntegration: { installationId: 789, username: 'out-gh' } }); + mockOctokit(['owner-gh']); + const res = await request(app) + .post('/tasks/assign') + .set('x-test-uid', 'owner-uid') + .send({ projectId: proj._id, taskName: 'X', assignedUserId: 'outsider' }); + expect(res.status).toBe(400); +}); + +// B5: rejects non-collaborator +test('B5: POST /assign rejects non-collaborator', async () => { + const proj = await seedProject(); + mockOctokit(['owner-gh']); + const res = await request(app) + .post('/tasks/assign') + .set('x-test-uid', 'owner-uid') + .send({ projectId: proj._id, taskName: 'X', assignedUserId: 'assignee-uid' }); + expect(res.status).toBe(400); + expect(res.body.message).toMatch(/collaborator/i); +}); + +// B6: rejects multiple assignees +test('B6: POST /assign rejects multiple assignees', async () => { + const proj = await seedProject(); + mockOctokit(); + const res = await request(app) + .post('/tasks/assign') + .set('x-test-uid', 'owner-uid') + .send({ projectId: proj._id, taskName: 'X', assignedUserIds: ['assignee-uid', 'owner-uid'] }); + expect(res.status).toBe(400); + expect(res.body.message).toMatch(/only one assignee/i); +}); + +// B7: sends email + push notification + session log +test('B7: POST /assign sends email, push notification, and logs session', async () => { + const proj = await seedProject(); + mockOctokit(); + const res = await request(app) + .post('/tasks/assign') + .set('x-test-uid', 'owner-uid') + .send({ projectId: proj._id, taskName: 'Notify Task', assignedUserId: 'assignee-uid' }); + expect(res.status).toBe(200); + await new Promise(r => setTimeout(r, 100)); + expect(sendZyncEmail).toHaveBeenCalledWith('a@t.com', expect.any(String), expect.any(String), expect.any(String)); + expect(sendPushNotification).toHaveBeenCalledWith('assignee-uid', expect.objectContaining({ title: 'New Task Assigned' })); + const sessions = await Session.find({ eventType: 'task-assigned' }); + expect(sessions).toHaveLength(1); + expect(sessions[0].userId).toBe('assignee-uid'); +}); + +// B8: emits socket events task-created and task-assigned +test('B8: POST /assign emits socket events', async () => { + const proj = await seedProject(); + mockOctokit(); + const res = await request(app) + .post('/tasks/assign') + .set('x-test-uid', 'owner-uid') + .send({ projectId: proj._id, taskName: 'Socket Task', assignedUserId: 'assignee-uid' }); + expect(res.status).toBe(200); + expect(mockTaskIO.emitToProject).toHaveBeenCalledWith( + String(proj._id), 'task-created', + expect.objectContaining({ projectId: String(proj._id), actor: 'owner-uid' }) + ); + expect(mockTaskIO.emitToUser).toHaveBeenCalledWith( + 'assignee-uid', 'task-assigned', + expect.objectContaining({ projectId: String(proj._id) }) + ); +}); + +// B9: invalidates cache for owner + assignee +test('B9: POST /assign invalidates cache for owner and assignee', async () => { + const proj = await seedProject(); + mockOctokit(); + const res = await request(app) + .post('/tasks/assign') + .set('x-test-uid', 'owner-uid') + .send({ projectId: proj._id, taskName: 'Cache Task', assignedUserId: 'assignee-uid' }); + expect(res.status).toBe(200); + const cache = require('../utils/cache.js'); + expect(cache.invalidate).toHaveBeenCalledWith('projects:owner-uid', 'projects:assignee-uid'); +}); diff --git a/backend/utils/githubAppAuth.js b/backend/utils/githubAppAuth.js index e3393328..b9f4a345 100644 --- a/backend/utils/githubAppAuth.js +++ b/backend/utils/githubAppAuth.js @@ -85,7 +85,7 @@ const getAppJwt = () => { // WHAT: Defines a function to generate a JWT for the const now = Math.floor(Date.now() / 1000); // WHAT: Gets the current time in seconds since epoch. WHY: JWT claims like 'iat' and 'exp' require time in seconds. const payload = { // WHAT: Creates the JWT payload object. WHY: Defines the claims (issued at, expiration, issuer) required by GitHub. iat: now - 60, // WHAT: Sets the 'issued at' time to 60 seconds in the past. WHY: Prevents token rejection due to clock drift between servers. - exp: now + (10 * 60), // WHAT: Sets the 'expiration' time to 10 minutes in the future. WHY: Limits the validity window of the token for security; GitHub enforces a max of 10 minutes. + exp: now + (5 * 60), // WHAT: Sets the 'expiration' time to 5 minutes in the future. WHY: Leaves buffer for clock drift so it doesn't exceed 10m max. iss: appId // WHAT: Sets the 'issuer' to the GitHub App ID. WHY: Tells GitHub which App is presenting the token. }; diff --git a/bugs.md b/bugs.md new file mode 100644 index 00000000..c27db78c --- /dev/null +++ b/bugs.md @@ -0,0 +1,86 @@ +analyze the notes section the share feature is not working. +do not edit any chnages just analyze the code and tell why the feature is not working + +1. The Email Invitation is Stubbed Out (Incomplete) +In + +ShareDialog.tsx +, the handleInvite function (which triggers when clicking the "Invite" button) only displays an informational toast stating that the email invitation feature is coming soon: + +const handleInvite = async () => { + if (!email) {return;} + setLoading(true); + try { + toast.info("Invite by email coming soon! Please share the link."); + } catch (e: any) { + toast.error("Failed to invite user"); + } finally { + setLoading(false); + } +}; +No actual backend call or DB write occurs when inviting someone by email. + +2. Missing User Identity Lookup / Mapping +The permissions store maps user IDs (uid) to roles (viewer, editor, owner). The dialog asks for the colleague's email but does not resolve that email to a corresponding uid in order to update the permissions dictionary correctly. + +Based on the analysis of + +PeopleView.tsx +, the activity status bug is caused by the following logic in lines 930–950: + +let statusText = status; +if (lastSeenDate && !isNaN(lastSeenDate.getTime())) { + try { + const duration = formatDistanceToNow(lastSeenDate, { addSuffix: false }) + .replace('less than a minute', '1m') + .replace(' minutes', 'm') + .replace(' minute', 'm') + .replace(' hours', 'h') + .replace(' hour', 'h') + .replace(' days', 'd') + .replace(' day', 'd'); + + if (status === 'online') { + statusText = `Online (${duration})`; + } else { + statusText = `Offline ${duration}`; + } + } catch (e) { + } +} +Why the Activity Status is Showing Wrong: +Away status is shown as Offline: The status property can be 'online' | 'offline' | 'away'. If a user is away, the code falls into the else block because status !== 'online'. Consequently, an active but idle user who should show as Away is incorrectly labeled as Offline. +Confusing Online durations: When a user is actively online, showing a duration since their last heartbeat (e.g., Online (2h)) is confusing because it makes the active status seem stale or contradictory. +Dirty duration strings: The string replacement on the output of formatDistanceToNow only replaces specific strings (like ' hours') but leaves prefixes like 'about ', 'over ', or 'almost ' intact (e.g., showing as Offline about 2h or Online (almost 15m)). +How to Solve It: +You can fix the conditional blocks to handle the three states properly (online, away, and offline), and optionally clean up the duration prefix. + +Update the logic in + +PeopleView.tsx + to: + +typescript +let statusText = status; +if (lastSeenDate && !isNaN(lastSeenDate.getTime())) { + try { + const duration = formatDistanceToNow(lastSeenDate, { addSuffix: false }) + .replace(/^(about|over|almost)\s+/, '') // Strip prefixes like "about", "over", etc. + .replace('less than a minute', '1m') + .replace(' minutes', 'm') + .replace(' minute', 'm') + .replace(' hours', 'h') + .replace(' hour', 'h') + .replace(' days', 'd') + .replace(' day', 'd'); + if (status === 'online') { + statusText = 'Online'; // Keep it clean for currently active users + } else if (status === 'away') { + statusText = `Away (${duration})`; // Correctly handle the away state + } else { + statusText = `Offline ${duration}`; // Only display Offline when they are actually offline + } + } catch (e) { + // Fallback if parsing fails + } +} \ No newline at end of file diff --git a/docs/bug-fixes/activity-log-visibility-update.md b/docs/bug-fixes/activity-log-visibility-update.md new file mode 100644 index 00000000..c2e2f669 --- /dev/null +++ b/docs/bug-fixes/activity-log-visibility-update.md @@ -0,0 +1,44 @@ +# Activity Log Visibility & Permissions Update +**Date:** August 06, 2026 + +## Overview +This document details the bug fixes and enhancements made to the Zync Activity Log system to address two primary issues: +1. Default profile photos not fetching for OAuth providers (Google, LinkedIn, GitHub). +2. The "Activity Log" sidebar menu item loading late ("popping in") due to asynchronous API data fetching. + +## Changes Implemented + +### 1. OAuth Profile Photo Resolution +**Issue:** User profile photos coming from OAuth providers (Google, GitHub, LinkedIn) or Cloudinary were failing to render or loading broken images inside the Activity Summary Card. + +**Root Cause:** +External image URLs often require correct origin handling and referrer policies, especially when they are fully qualified external URLs (e.g., `https://lh3.googleusercontent.com/...`). The previous implementation was rendering raw avatar strings which sometimes lacked proper formatting or triggered CORS/referrer blocks. + +**Fix:** +- Updated `ActivitySummaryCard.tsx` to wrap the `photoURL` properties using the existing `getFullUrl()` utility function. This ensures all URLs are correctly formatted. +- Added `referrerPolicy="no-referrer"` to the `` tags. This prevents external providers (like Google) from blocking the image request based on the referring origin (Zync). + +**Affected Files:** +- `src/components/views/activity/ActivitySummaryCard.tsx` + +### 2. Activity Log Sidebar Visibility & Pop-In Fix +**Issue:** The "Activity log" item in the sidebar was experiencing a noticeable layout shift. When a user loaded the app, the sidebar rendered without the Activity Log, and then it suddenly "popped in" a split second later. + +**Root Cause:** +The sidebar menu (in both `DesktopView` and `MobileView`) was conditionally rendering the Activity Log using the `canViewActivityLog` boolean. This boolean depended on `myTeams`, which is populated asynchronously via a network request to `/api/teams/mine` on mount. As a result, the sidebar item was delayed until the network request completed. + +**Fix:** +- **Sidebar Unrestricted:** Removed the `canViewActivityLog` check from `DesktopView.tsx` and `MobileView.tsx` sidebar item definitions. The Activity Log tab is now rendered instantly for *all* users, effectively eliminating the layout shift. +- **Conditional Analytics Rendering:** While all users can now access the Activity Log (to view their own personal statistics), we restricted the team-level dropdowns ("Select Team" and "Select Member"). +- In `ActivitySummaryCard.tsx`, the dropdown blocks are now conditionally rendered wrapped in `{normalizedTeamFilterOptions.length > 0 && (...) }`. +- **Dynamic Role Adaptability:** `normalizedTeamFilterOptions` is derived from the teams the user owns or administers. If a regular member is promoted to admin, or creates their own team, the array instantly populates and the dropdowns dynamically appear. + +**Affected Files:** +- `src/components/views/DesktopView.tsx` +- `src/components/views/MobileView.tsx` +- `src/components/views/activity/ActivitySummaryCard.tsx` + +## Testing & Verification +- Linting ran cleanly via `npm run lint` across the repository. +- Layout shift verified fixed. +- Non-admins successfully see their own activity data but are restricted from viewing the broader team activity analytics. diff --git a/package-lock.json b/package-lock.json index e0c35f1a..1e34e998 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,6 +94,7 @@ "devDependencies": { "@babel/core": "^7.24.0", "@babel/preset-env": "^7.24.0", + "@babel/preset-react": "^8.0.1", "@babel/preset-typescript": "^7.24.0", "@emnapi/core": "^1.9.1", "@emnapi/runtime": "^1.9.1", @@ -102,6 +103,7 @@ "@storybook/react": "^10.3.3", "@storybook/react-vite": "^10.3.3", "@tailwindcss/typography": "^0.5.19", + "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.5.2", @@ -1660,6 +1662,337 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-8.0.1.tgz", + "integrity": "sha512-soLishXlkyu6jcICPyO3HEP7A3GCzKEnn7XfvYrImuWEOwFAz93qShmWSYPf5ww0ZkO4By0zsN2bVIDF54fSdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^8.0.1" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "peerDependencies": { + "@babel/core": "^8.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name/node_modules/@babel/helper-plugin-utils": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-8.0.1.tgz", + "integrity": "sha512-3PKFgjTyPlhFhorfP+SjKQxLViIL++zWjFOO4hGriYU+Bsm983DxEM1JmDRJVWXV0O9npu+xXRqz7Pbd3mh70g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "peerDependencies": { + "@babel/core": "^8.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-8.0.1.tgz", + "integrity": "sha512-NgkoF7Uq+30TmOPDdNUimT0Nta02uVjqJRFNlVWKrbOCu/CkzfHa4aMnIs0lMpkMmZmWA1e42Va+F04i/pY1zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^8.0.0", + "@babel/helper-module-imports": "^8.0.0", + "@babel/helper-plugin-utils": "^8.0.1", + "@babel/plugin-syntax-jsx": "^8.0.1", + "@babel/types": "^8.0.0" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "peerDependencies": { + "@babel/core": "^8.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-8.0.1.tgz", + "integrity": "sha512-Hb+HUZpV9KFHjm+F+P3aLDMi8QXU9l3ROCQv20z18Me2sGyW5nNNR5YTevNlgHvCpFek3BnAwhDGq/BRndXViw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^8.0.1" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "peerDependencies": { + "@babel/core": "^8.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/code-frame": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.0.tgz", + "integrity": "sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^8.0.0", + "js-tokens": "^10.0.0" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/generator": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz", + "integrity": "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^8.0.0", + "@babel/types": "^8.0.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "@types/jsesc": "^2.5.0", + "jsesc": "^3.0.2" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/helper-annotate-as-pure": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-8.0.0.tgz", + "integrity": "sha512-NSpMkMsvvZqzThJ0p1B02cbtA2ObEyfBvq950bmNkyxsxvcxwhvvCB036rKhlEnuBBo30bOrk13u3FzlKSoRrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^8.0.0" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/helper-globals": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-8.0.0.tgz", + "integrity": "sha512-lLozHOM6sWWlxNo8CYqHy4MBZeTvHXNgVPBfPOGsjPKUzHC2Az9QwB6gxdQmpwHl6GlQtbGgS+lj5887guDiLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/helper-module-imports": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-8.0.0.tgz", + "integrity": "sha512-NZ7mSS93o4ndX4KrbD7W8Sf3QT8Qe24PrnFyUcuOPDzK6faqDFKjY9RG7he7+I7FdiQ4llpnosFqzrXa+Vy3Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^8.0.0", + "@babel/types": "^8.0.0" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/helper-plugin-utils": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-8.0.1.tgz", + "integrity": "sha512-3PKFgjTyPlhFhorfP+SjKQxLViIL++zWjFOO4hGriYU+Bsm983DxEM1JmDRJVWXV0O9npu+xXRqz7Pbd3mh70g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "peerDependencies": { + "@babel/core": "^8.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/helper-string-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz", + "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/helper-validator-identifier": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", + "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/parser": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz", + "integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^8.0.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/plugin-syntax-jsx": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-8.0.1.tgz", + "integrity": "sha512-n0jtCOxEovhU7METqSQjcZO9pX53nu9uNIjMS+hEt+Nt9jA7oOZoBIgbCxhhASmF6T6rPDGge5UAvh6Z4eFz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^8.0.1" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "peerDependencies": { + "@babel/core": "^8.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/template": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-8.0.0.tgz", + "integrity": "sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^8.0.0", + "@babel/parser": "^8.0.0", + "@babel/types": "^8.0.0" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/traverse": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.4.tgz", + "integrity": "sha512-bZnmqzGG8UZneG1lLxBoWIH0G6Gr1D846Yu4/3XnY6FhCndMR49u26nTY08u/dAxWmLWF9vGQOuC+84FfIUoeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^8.0.0", + "@babel/generator": "^8.0.0", + "@babel/helper-globals": "^8.0.0", + "@babel/parser": "^8.0.4", + "@babel/template": "^8.0.0", + "@babel/types": "^8.0.4", + "obug": "^2.1.1" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/types": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz", + "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^8.0.0", + "@babel/helper-validator-identifier": "^8.0.4" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-8.0.1.tgz", + "integrity": "sha512-7/8UwU8hoPBurXa9tUiTTC8aACTRy5tCqLUtqikHp2eGiWoEB57AduOdbQ71OOMTEvawKrGhv3WfzkDpI+/oSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^8.0.0", + "@babel/helper-plugin-utils": "^8.0.1" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "peerDependencies": { + "@babel/core": "^8.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/helper-annotate-as-pure": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-8.0.0.tgz", + "integrity": "sha512-NSpMkMsvvZqzThJ0p1B02cbtA2ObEyfBvq950bmNkyxsxvcxwhvvCB036rKhlEnuBBo30bOrk13u3FzlKSoRrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^8.0.0" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/helper-plugin-utils": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-8.0.1.tgz", + "integrity": "sha512-3PKFgjTyPlhFhorfP+SjKQxLViIL++zWjFOO4hGriYU+Bsm983DxEM1JmDRJVWXV0O9npu+xXRqz7Pbd3mh70g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "peerDependencies": { + "@babel/core": "^8.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/helper-string-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz", + "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/helper-validator-identifier": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", + "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/types": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz", + "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^8.0.0", + "@babel/helper-validator-identifier": "^8.0.4" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, "node_modules/@babel/plugin-transform-regenerator": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", @@ -1988,6 +2321,50 @@ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/@babel/preset-react": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-8.0.1.tgz", + "integrity": "sha512-jrFuPp/pTddFZbtmWhdLNAYc6UMcpboeUPnw0BBrm4nOmcAko/1TRcFi1PzWCeOFRU+VaSiKmat87W1HvR7mIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^8.0.1", + "@babel/helper-validator-option": "^8.0.0", + "@babel/plugin-transform-react-display-name": "^8.0.1", + "@babel/plugin-transform-react-jsx": "^8.0.1", + "@babel/plugin-transform-react-jsx-development": "^8.0.1", + "@babel/plugin-transform-react-pure-annotations": "^8.0.1" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "peerDependencies": { + "@babel/core": "^8.0.0" + } + }, + "node_modules/@babel/preset-react/node_modules/@babel/helper-plugin-utils": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-8.0.1.tgz", + "integrity": "sha512-3PKFgjTyPlhFhorfP+SjKQxLViIL++zWjFOO4hGriYU+Bsm983DxEM1JmDRJVWXV0O9npu+xXRqz7Pbd3mh70g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "peerDependencies": { + "@babel/core": "^8.0.0" + } + }, + "node_modules/@babel/preset-react/node_modules/@babel/helper-validator-option": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-8.0.0.tgz", + "integrity": "sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, "node_modules/@babel/preset-typescript": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", @@ -8559,6 +8936,78 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/dom/node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", @@ -8871,6 +9320,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -9127,6 +9583,13 @@ "parse5": "^7.0.0" } }, + "node_modules/@types/jsesc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz", + "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -17530,6 +17993,16 @@ "node": ">=12" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", diff --git a/package.json b/package.json index e708d93c..5ea2a692 100644 --- a/package.json +++ b/package.json @@ -129,6 +129,7 @@ "devDependencies": { "@babel/core": "^7.24.0", "@babel/preset-env": "^7.24.0", + "@babel/preset-react": "^8.0.1", "@babel/preset-typescript": "^7.24.0", "@emnapi/core": "^1.9.1", "@emnapi/runtime": "^1.9.1", @@ -137,6 +138,7 @@ "@storybook/react": "^10.3.3", "@storybook/react-vite": "^10.3.3", "@tailwindcss/typography": "^0.5.19", + "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.5.2", diff --git a/src/api/calendar.ts b/src/api/calendar.ts index 19d95f42..04eee60c 100644 --- a/src/api/calendar.ts +++ b/src/api/calendar.ts @@ -94,16 +94,22 @@ export interface Country { } export const fetchHolidays = async (year: number, countryCode: string): Promise => { - const headers = await getAuthHeaders(); - const response = await fetch( - `${API_URL}/holidays?year=${year}&countryCode=${encodeURIComponent(countryCode)}`, - { headers }, - ); - if (!response.ok) { - console.error('Failed to fetch holidays:', response.status); + try { + const headers = await getAuthHeaders(); + const response = await fetch( + `${API_URL}/holidays?year=${year}&countryCode=${encodeURIComponent(countryCode)}`, + { headers }, + ); + if (!response.ok) { + console.warn('Failed to fetch holidays:', response.status); + return []; + } + const data = await response.json(); + return Array.isArray(data) ? data : []; + } catch (err) { + console.warn('Error fetching holidays:', err); return []; } - return response.json(); }; export const fetchCountries = async (): Promise => { diff --git a/src/components/kibo-ui/contribution-graph.tsx b/src/components/kibo-ui/contribution-graph.tsx index bf15a1e0..dd74b562 100644 --- a/src/components/kibo-ui/contribution-graph.tsx +++ b/src/components/kibo-ui/contribution-graph.tsx @@ -143,9 +143,10 @@ const ContributionGraph = ({ interface ContributionGraphCalendarProps { children: (props: { activity: Activity; dayIndex: number; weekIndex: number }) => ReactNode; + maxWeeks?: number; } -const ContributionGraphCalendar = ({ children }: ContributionGraphCalendarProps) => { +const ContributionGraphCalendar = ({ children, maxWeeks }: ContributionGraphCalendarProps) => { const { data, blockSize, blockMargin } = useContributionGraph(); if (data.length === 0) { @@ -184,22 +185,30 @@ const ContributionGraphCalendar = ({ children }: ContributionGraphCalendarProps) weeks.push(week); } + // 150 days is ~21 weeks. Default mobile view to 21 weeks if maxWeeks is unspecified. + const isMobileViewport = typeof window !== 'undefined' && window.innerWidth < 768; + const effectiveMaxWeeks = maxWeeks ?? (isMobileViewport ? 21 : undefined); + + const displayWeeks = + effectiveMaxWeeks && weeks.length > effectiveMaxWeeks + ? weeks.slice(-effectiveMaxWeeks) + : weeks; + const height = 7 * (blockSize + blockMargin); - const width = weeks.length * (blockSize + blockMargin); - const marginLeft = 30; + const width = displayWeeks.length * (blockSize + blockMargin); + const marginLeft = 26; const months: { name: string; weekIndex: number }[] = []; let lastMonth = -1; - weeks.forEach((week, weekIndex) => { + displayWeeks.forEach((week, weekIndex) => { const firstDayOfWeek = parseLocalDate(week[0].date); const month = firstDayOfWeek.getMonth(); if (month !== lastMonth) { - - - - if (firstDayOfWeek < startDate) { - lastMonth = month; - return; + if (months.length > 0) { + const lastAdded = months[months.length - 1]; + if (weekIndex - lastAdded.weekIndex < 3) { + months.pop(); + } } months.push({ name: firstDayOfWeek.toLocaleString('en-US', { month: 'short' }), @@ -210,45 +219,47 @@ const ContributionGraphCalendar = ({ children }: ContributionGraphCalendarProps) }); return ( -
- {} -
- {months.map((month, idx) => ( - - {month.name} - - ))} -
- -
- {} +
+
+ {/* Month Labels */}
- Sum - Mon - Tue - Wed - Thu - Fri - Sat + {months.map((month, idx) => ( + + {month.name} + + ))}
- - {weeks.map((week, weekIndex) => - week.map((activity, dayIndex) => children({ activity, dayIndex, weekIndex })) - )} - +
+ {/* Day Labels */} +
+ Sum + Mon + Tue + Wed + Thu + Fri + Sat +
+ + + {displayWeeks.map((week, weekIndex) => + week.map((activity, dayIndex) => children({ activity, dayIndex, weekIndex })) + )} + +
); @@ -335,13 +346,15 @@ const ContributionGraphTotalCount = ({ className, ...props }: ComponentProps<'sp const ContributionGraphLegend = ({ className, ...props }: ComponentProps<'div'>) => { return ( -
+
Less -
-
-
-
-
+
+
+
+
+
+
+
More
); diff --git a/src/components/landing/CTASection.tsx b/src/components/landing/CTASection.tsx index 3cb6f1d9..d9112319 100644 --- a/src/components/landing/CTASection.tsx +++ b/src/components/landing/CTASection.tsx @@ -105,12 +105,12 @@ const CTASection = () => { return (
{/* Isometric Architectural Matrix */} -
+
{/* Massive Typography */}

diff --git a/src/components/landing/FeaturesSection.tsx b/src/components/landing/FeaturesSection.tsx index 0fc93b82..bb2f7f76 100644 --- a/src/components/landing/FeaturesSection.tsx +++ b/src/components/landing/FeaturesSection.tsx @@ -131,64 +131,66 @@ const FeaturesSection = () => { ]; return ( -
+
{} -
-

- Everything to ship faster -

-

- From AI-powered planning to GitHub integration—the tools your team needs, - without the bloat. -

-
- - {/* Bento Box Grid */} -
- - {/* AI Project Setup - Interactive Walkthrough */} -
-
- -
-
- -
-

- AI Project Setup -

-

- Describe your idea and get a complete project structure, workflows, and task breakdown in seconds. Watch it happen live. -

-
- -
- -
+ {/* Everything to ship faster Header & Bento Box Grid (Hidden on mobile) */} +
+
+

+ Everything to ship faster +

+

+ From AI-powered planning to GitHub integration—the tools your team needs, + without the bloat. +

- {/* GitHub Sync - Interactive Walkthrough */} -
-
-
- + {/* Bento Box Grid */} +
+ {/* AI Project Setup - Interactive Walkthrough */} +
+
+ +
+
+ +
+

+ AI Project Setup +

+

+ Describe your idea and get a complete project structure, workflows, and task breakdown in seconds. Watch it happen live. +

-

- GitHub Sync -

-

- Connect repositories and auto-complete tasks when commits are pushed. Your code drives your workflow. -

-
- +
+
+
+ + {/* GitHub Sync - Interactive Walkthrough */} +
+
+
+ +
+

+ GitHub Sync +

+

+ Connect repositories and auto-complete tasks when commits are pushed. Your code drives your workflow. +

+ +
+ +
+
{/* Real-Time Notes */} -
+
@@ -215,7 +217,7 @@ const FeaturesSection = () => {
{/* Smart Calendar */} -
+

Smart Calendar @@ -240,7 +242,7 @@ const FeaturesSection = () => {

{/* Built-in Chat */} -
+
@@ -267,7 +269,7 @@ const FeaturesSection = () => {
{/* Focused Notifications */} -
+

Focused Notifications diff --git a/src/components/landing/Footer.tsx b/src/components/landing/Footer.tsx index 46edd121..734ec5f9 100644 --- a/src/components/landing/Footer.tsx +++ b/src/components/landing/Footer.tsx @@ -105,16 +105,16 @@ const Footer = () => { }; return ( -

); diff --git a/src/components/landing/MobileAppSection.tsx b/src/components/landing/MobileAppSection.tsx index df336ace..a8dccc50 100644 --- a/src/components/landing/MobileAppSection.tsx +++ b/src/components/landing/MobileAppSection.tsx @@ -108,7 +108,7 @@ const MobileAppSection = () => { }; return ( -
+
diff --git a/src/components/landing/MobilePreview.tsx b/src/components/landing/MobilePreview.tsx index acff5330..d0c9073a 100644 --- a/src/components/landing/MobilePreview.tsx +++ b/src/components/landing/MobilePreview.tsx @@ -190,7 +190,7 @@ const MobilePreview = () => { {mockProjects.map(project => (
@@ -238,7 +238,7 @@ const MobilePreview = () => {

People

{mockPeople.map(person => ( - +
@@ -263,7 +263,7 @@ const MobilePreview = () => { {activeTab === "calendar" && (

January 2026

- +
{["S", "M", "T", "W", "T", "F", "S"].map((d, i) => (
{d}
@@ -293,7 +293,7 @@ const MobilePreview = () => { })}
- +
Sprint Planning
@@ -312,7 +312,7 @@ const MobilePreview = () => {
{mockNotes.map(note => ( - +
@@ -331,7 +331,7 @@ const MobilePreview = () => {

My Tasks

{mockTasks.map(task => ( - +
diff --git a/src/components/layout/MobileLayout.tsx b/src/components/layout/MobileLayout.tsx index d14b335e..0bc733c3 100644 --- a/src/components/layout/MobileLayout.tsx +++ b/src/components/layout/MobileLayout.tsx @@ -2,6 +2,10 @@ * @fileoverview MobileLayout.tsx * @module MobileLayout * + * Premium Mobile Layout component for Zync. + * Features a 5-item bottom navigation bar: Home, People, + (center), Tasks, Meet. + * Includes glassmorphism, fluid micro-interactions, and a sleek user side drawer. + * * ============================================================================ * ZYNC ENTERPRISE ARCHITECTURE DOCUMENTATION * ============================================================================ @@ -74,8 +78,7 @@ * ============================================================================ */ import React from 'react'; -import { Plus, Home, CheckSquare, FileText, Folder, Users, Calendar, Video } from 'lucide-react'; -import { Button } from '@/components/ui/button'; +import { Plus, Home, Users, CheckSquare, Video } from 'lucide-react'; import { Sheet, SheetContent, SheetTrigger, SheetHeader, SheetTitle, SheetDescription } from '@/components/ui/sheet'; import { cn } from '@/lib/utils'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; @@ -106,47 +109,64 @@ export const MobileLayout = ({ user, onFabClick, rightHeaderAction, - hideActivityLog }: MobileLayoutProps) => { const [isDrawerOpen, setIsDrawerOpen] = React.useState(false); const { hasCheckedStatus, requiresInstallWall, isIOS, isAndroid } = useAppInstallStatus(); - if (hasCheckedStatus && requiresInstallWall) { - return ; - } + // if (hasCheckedStatus && requiresInstallWall) { + // return ; + // } + // Bottom Navigation Items strictly matching user specification: Home, People, +, Tasks, Meet const leftNavItems = [ { id: 'Home', icon: Home, label: 'Home' }, { id: 'People', icon: Users, label: 'People' }, - { id: 'Calendar', icon: Calendar, label: 'Cal' }, ]; const rightNavItems = [ - { id: 'Notes', icon: FileText, label: 'Notes' }, { id: 'Tasks', icon: CheckSquare, label: 'Tasks' }, { id: 'Meet', icon: Video, label: 'Meet' }, ]; - - const isMainTab = [...leftNavItems, ...rightNavItems].some(item => item.id === activeTab); - return ( -
-
-
+
+ {/* Top Header Bar */} +
+
+ Zync + Zync + + Zync + +
+ +
{rightHeaderAction} + + {/* User Profile Avatar / Drawer Trigger */} - - + Navigation Menu @@ -155,14 +175,20 @@ export const MobileLayout = ({
{user && ( -
- +
+ - {user.displayName?.substring(0, 1) || 'U'} + + {user.displayName?.substring(0, 1) || 'U'} + -
- {user.displayName} - {user.email} +
+ + {user.displayName || 'User'} + + + {user.email} +
)} @@ -183,14 +209,15 @@ export const MobileLayout = ({
- {} -
+ {/* Main View Area */} +
{children}
- {/* Bottom Navigation */} -