diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..87e0055b --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,15 @@ +{ + "permissions": { + "allow": [ + "Bash(npm test:*)", + "Bash(npm run:*)", + "Bash(npx jest:*)", + "Bash(npm install:*)", + "Bash(git fetch:*)", + "Bash(git rebase:*)", + "Bash(git push:*)", + "Bash(git status:*)", + "Bash(git log:*)" + ] + } +} diff --git a/.firebaserc b/.firebaserc index 1e2ee7a7..c88ff116 100644 --- a/.firebaserc +++ b/.firebaserc @@ -2,5 +2,22 @@ "projects": { "staging": "mfgt-flights", "production": "project-8979611309653332047" - } -} + }, + "targets": { + "cypress-testing": { + "database": { + "main": [ + "cypress-testing-eu" + ] + } + }, + "lspl-test": { + "database": { + "main": [ + "lspl-test-default-rtdb" + ] + } + } + }, + "etags": {} +} \ No newline at end of file diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index d300267f..fd5ccb29 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -19,7 +19,7 @@ jobs: (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) runs-on: ubuntu-latest permissions: - contents: read + contents: write pull-requests: read issues: read id-token: write @@ -28,7 +28,15 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: - fetch-depth: 1 + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install dependencies + run: npm ci - name: Run Claude Code id: claude diff --git a/.github/workflows/firebase-hosting-dev.yml b/.github/workflows/firebase-hosting-dev.yml index 6a9b873b..4c4e66b9 100644 --- a/.github/workflows/firebase-hosting-dev.yml +++ b/.github/workflows/firebase-hosting-dev.yml @@ -11,21 +11,19 @@ jobs: environment: - lszt_test - lszm_test + - lspl_test - lspv_test - lszo_test - lsze_test environment: ${{ matrix.environment }} steps: - run: echo 'Running deplyoment for project ${{ vars.PROJECT }}' - - uses: actions/checkout@v2 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci - run: npm run build --project=${{ vars.PROJECT }} - - uses: actions/setup-node@v4 - with: - node-version: 22 - uses: w9jds/setup-firebase@main with: project_id: ${{ vars.FIREBASE_PROJECT }} @@ -40,6 +38,7 @@ jobs: environment: - lszt_test - lszm_test + - lspl_test - lspv_test - lszo_test - lsze_test diff --git a/.github/workflows/firebase-hosting-prod.yml b/.github/workflows/firebase-hosting-prod.yml index 553c6ae9..6ff17b1f 100644 --- a/.github/workflows/firebase-hosting-prod.yml +++ b/.github/workflows/firebase-hosting-prod.yml @@ -17,15 +17,12 @@ jobs: environment: ${{ matrix.environment }} steps: - run: echo 'Running deplyoment for project ${{ vars.PROJECT }}' - - uses: actions/checkout@v2 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci - run: npm run build:prod --project=${{ vars.PROJECT }} - - uses: actions/setup-node@v4 - with: - node-version: 22 - uses: w9jds/setup-firebase@main with: project_id: ${{ vars.FIREBASE_PROJECT }} diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 9d87eb97..fa28ce7f 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -5,8 +5,8 @@ jobs: if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci diff --git a/LICENSE b/LICENSE index 7e11fb83..f5e7edf5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,15 @@ -MIT License - -Copyright (c) 2016-present, open digital Switzerland - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Copyright (c) 2016-present, Open Digital Switzerland. All rights reserved. + +This software and associated documentation files (the "Software") are the +proprietary property of Open Digital Switzerland. Unauthorized copying, +modification, distribution, or use of the Software, in whole or in part, +is strictly prohibited without the prior written consent of Open Digital +Switzerland. + +The Software is provided "as is", without warranty of any kind, express or +implied, including but not limited to the warranties of merchantability, +fitness for a particular purpose, and noninfringement. In no event shall +Open Digital Switzerland be liable for any claim, damages, or other +liability, whether in an action of contract, tort, or otherwise, arising +from, out of, or in connection with the Software or the use or other +dealings in the Software. diff --git a/cypress/CYPRESS_TESTING_SETUP.md b/cypress/CYPRESS_TESTING_SETUP.md index 7c133525..7e72a499 100644 --- a/cypress/CYPRESS_TESTING_SETUP.md +++ b/cypress/CYPRESS_TESTING_SETUP.md @@ -14,6 +14,20 @@ Auth endpoint: `https://europe-west1-cypress-testing.cloudfunctions.net/auth` | `foo` | `bar` | user | | `admin` | `12345` | admin | +## Email user login (for `loginEmail` command) + +The `cy.loginEmail()` command authenticates as `cypress-pilot@example.com` via the +`createTestEmailToken` Cloud Function. This function is gated behind a config flag +that must be enabled on the `cypress-testing` project: + +```bash +firebase functions:config:set testing.enabled=true --project cypress-testing +firebase deploy --only functions --project cypress-testing +``` + +The function always creates a token for the hardcoded email `cypress-pilot@example.com` +and returns 403 on any project where `testing.enabled` is not set. + ## Aerodrome settings (already in place) - Runway `36` with `departureRoute` options including `west` diff --git a/cypress/integration/movements/email_user_movement_list_spec.js b/cypress/integration/movements/email_user_movement_list_spec.js new file mode 100644 index 00000000..2e9e9b00 --- /dev/null +++ b/cypress/integration/movements/email_user_movement_list_spec.js @@ -0,0 +1,75 @@ +describe('movements', () => { + describe('email user movement list', () => { + let createdDepartureKey; + + before(() => { + cy.visit('#/departure/new'); + cy.loginEmail(); + }); + + after(() => { + // Clean up as admin (email user may lack delete permission) + cy.logout(); + cy.loginAdmin(); + + if (createdDepartureKey) { + cy.window().then(win => { + win.firebase.getRef(`/departures/${createdDepartureKey}`).remove(); + }); + } + + cy.logout(); + }); + + it('shows departure in movement list for email user', () => { + // Create a departure via the UI + cy.get(`[data-cy=immatriculation]`).type('HBKOF'); + cy.get(`[data-cy=aircraftType]`).type('DR40'); + cy.get(`[data-cy=mtow]`).type('1000'); + cy.get(`[data-cy=aircraftCategory]`).click(); + cy.get(`[data-cy=aircraftCategory-option-Flugzeug]`).click(); + cy.get(`[data-cy=next-button]`).click(); + + cy.get(`[data-cy=lastname]`).type('Cypress'); + cy.get(`[data-cy=firstname]`).type('Pilot'); + cy.get(`[data-cy=email]`).type('pilot@example.com'); + cy.get(`[data-cy=phone]`).type('0790000000'); + cy.get(`[data-cy=next-button]`).click(); + + cy.get(`[data-cy=passengerCount-increment]`).click(); + cy.get(`[data-cy=carriageVoucher-yes]`).click(); + cy.get(`[data-cy=next-button]`).click(); + + cy.get(`[data-cy=date]`).click(); + cy.get(`.DayPicker-Day[aria-disabled=false]`).last().click(); + cy.get(`[data-cy=time-minutes-increment]`).click(); + cy.get(`[data-cy=location]`).type('LSZR'); + cy.get(`[data-cy=duration-hours-increment]`).click(); + cy.get(`[data-cy=duration-minutes-increment]`).click(); + cy.get(`[data-cy=next-button]`).click(); + cy.get(`[data-cy=location-confirmation-dialog-confirm]`).click(); + + cy.get(`[data-cy=flightType-private]`).click(); + cy.get(`[data-cy=runway-36]`).click(); + cy.get(`[data-cy=departureRoute-west]`).click(); + cy.get(`[data-cy=route]`).type('Testroute'); + cy.get(`[data-cy=remarks]`).type('E2E email user test'); + cy.get(`[data-cy=next-button]`).click(); + cy.get(`[data-cy=commit-requirement-dialog-confirm]`).click(); + + cy.get(`[data-cy=finish-button]`).click(); + cy.hash().should('eq', '#/'); + + // Navigate to movement list and verify exactly one departure appears + cy.visit('#/movements'); + cy.get('[data-id]').should('have.length', 1); + cy.get('[data-id]') + .contains('HBKOF') + .closest('[data-id]') + .invoke('attr', 'data-id') + .then(key => { + createdDepartureKey = key; + }); + }); + }); +}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index e26642a4..dfbd1a70 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -14,6 +14,15 @@ Cypress.Commands.add('login', () => login('foo', 'bar')); Cypress.Commands.add('loginAdmin', () => login('admin', '12345')); +Cypress.Commands.add('loginEmail', () => { + cy.request('POST', 'https://europe-west1-cypress-testing.cloudfunctions.net/createTestEmailToken') + .then((response) => { + cy.window().then(win => { + return win.firebase.authenticate(response.body.token); + }); + }); +}); + Cypress.Commands.add('logout', () => { cy.window().then(win => { win.firebase.unauth(); diff --git a/firebase-rules-template.json b/firebase-rules-template.json index b05faf55..fb62a7c9 100644 --- a/firebase-rules-template.json +++ b/firebase-rules-template.json @@ -5,7 +5,8 @@ ".indexOn": [ "dateTime", "negativeTimestamp", - "immatriculation" + "immatriculation", + "createdBy_orderKey" ], "$departure_id": { ".write": "auth !== null && (!root.child('settings/lockDate').exists() || (!data.exists() && newData.exists() && newData.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24) || (data.exists() && !newData.exists() && data.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24) || (data.exists() && newData.exists() && data.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24 && newData.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24))", @@ -91,6 +92,9 @@ "customsFormUrl": { ".validate": "newData.isString()" }, + "privacyPolicyAcceptedAt": { + ".validate": "newData.isString() && newData.val().matches(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/)" + }, "$other": { ".validate": false } @@ -101,7 +105,8 @@ ".indexOn": [ "dateTime", "negativeTimestamp", - "immatriculation" + "immatriculation", + "createdBy_orderKey" ], "$arrival_id": { ".write": "auth !== null && (!root.child('settings/lockDate').exists() || (!data.exists() && newData.exists() && newData.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24) || (data.exists() && !newData.exists() && data.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24) || (data.exists() && newData.exists() && data.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24 && newData.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24))", @@ -223,6 +228,9 @@ "customsFormUrl": { ".validate": "newData.isString()" }, + "privacyPolicyAcceptedAt": { + ".validate": "newData.isString() && newData.val().matches(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/)" + }, "$other": { ".validate": false } @@ -287,6 +295,21 @@ ".read": "auth !== null && root.child('admins/' + auth.uid).exists()", ".write": "auth !== null && root.child('admins/' + auth.uid).exists()" }, + "privacyPolicyUrl": { + ".read": true, + ".write": "auth !== null && root.child('admins/' + auth.uid).exists()", + ".validate": "newData.isString()" + }, + "movementRetentionDays": { + ".read": "auth !== null && root.child('admins/' + auth.uid).exists()", + ".write": "auth !== null && root.child('admins/' + auth.uid).exists()", + ".validate": "newData.isNumber() && newData.val() > 0" + }, + "messageRetentionDays": { + ".read": "auth !== null && root.child('admins/' + auth.uid).exists()", + ".write": "auth !== null && root.child('admins/' + auth.uid).exists()", + ".validate": "newData.isNumber() && newData.val() > 0" + }, "$other": { ".validate": false } @@ -423,6 +446,11 @@ } } }, + "signInCodes": { + ".read": false, + ".write": false, + ".indexOn": ["email"] + }, "profiles": { "$profile_id": { ".read": "auth !== null && $profile_id === auth.uid", @@ -454,6 +482,9 @@ "mtow": { ".validate": "newData.isNumber() && newData.val() > 0" }, + "privacyPolicyAcceptedAt": { + ".validate": "newData.isString() && newData.val().matches(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/)" + }, "$other": { ".validate": false } diff --git a/firebase.json b/firebase.json index f227d607..9af05257 100644 --- a/firebase.json +++ b/firebase.json @@ -5,18 +5,42 @@ }], "hosting": { "public": "build", - "rewrites": [{ - "source": "/api/**", - "function": "api", - "region": "europe-west1" - }], + "rewrites": [ + { + "source": "/api/**", + "function": "api", + "region": "europe-west1" + }, + { + "source": "**", + "destination": "/index.html" + } + ], "headers": [ + { + "source": "/", + "headers": [ + { "key": "Cache-Control", "value": "no-cache" } + ] + }, { "source": "/index.html", "headers": [ { "key": "Cache-Control", "value": "no-cache" } ] }, + { + "source": "/service-worker.js", + "headers": [ + { "key": "Cache-Control", "value": "no-cache" } + ] + }, + { + "source": "/favicons/manifest.json", + "headers": [ + { "key": "Cache-Control", "value": "no-cache" } + ] + }, { "source": "**/*.js", "headers": [ diff --git a/functions/anonymizeMovements.js b/functions/anonymizeMovements.js new file mode 100644 index 00000000..007369a6 --- /dev/null +++ b/functions/anonymizeMovements.js @@ -0,0 +1,99 @@ +'use strict'; + +const functions = require('firebase-functions'); +const admin = require('firebase-admin'); + +const SCHEDULE = '0 2 * * *'; // Every day at 2 AM +const TIMEZONE = 'Europe/Zurich'; + +const PII_FIELDS = [ + 'firstname', + 'lastname', + 'email', + 'phone', + 'memberNr', + 'immatriculation', + 'remarks', + 'createdBy', + 'createdBy_orderKey', + 'customsFormId', + 'customsFormUrl', + 'privacyPolicyAcceptedAt', +]; + +function getAnonymizationUpdates(snapshot, cutoffIso) { + const updates = {}; + + snapshot.forEach(child => { + const val = child.val(); + + if (val.dateTime <= cutoffIso && !val.anonymized) { + PII_FIELDS.forEach(field => { + if (val[field] !== undefined) { + updates[`${child.key}/${field}`] = null; + } + }); + if (val.paymentMethod && val.paymentMethod.invoiceRecipientName !== undefined) { + updates[`${child.key}/paymentMethod/invoiceRecipientName`] = null; + } + updates[`${child.key}/anonymized`] = true; + } + }); + + return updates; +} + +exports.scheduledAnonymizeMovements = functions + .region('europe-west1') + .pubsub + .schedule(SCHEDULE) + .timeZone(TIMEZONE) + .onRun(async () => { + const db = admin.database(); + + const retentionSnap = await db.ref('/settings/movementRetentionDays').once('value'); + const retentionDays = retentionSnap.val(); + + if (!retentionDays) { + console.log('No movementRetentionDays configured, skipping'); + return; + } + + const cutoff = new Date(); + cutoff.setDate(cutoff.getDate() - retentionDays); + const cutoffIso = cutoff.toISOString(); + + console.log(`Anonymizing movements older than ${cutoffIso} (retention: ${retentionDays} days)`); + + // Note: endAt fetches all records up to cutoffIso, including already-anonymized ones. + // These are filtered out client-side via the !val.anonymized check below. + // Over time this may transfer growing amounts of anonymized data on each run, + // but is acceptable given bounded aerodrome data volumes. + const departuresSnap = await db.ref('/departures') + .orderByChild('dateTime') + .endAt(cutoffIso) + .once('value'); + + const departureUpdates = getAnonymizationUpdates(departuresSnap, cutoffIso); + + if (Object.keys(departureUpdates).length > 0) { + await db.ref('/departures').update(departureUpdates); + console.log(`Anonymized ${Object.keys(departureUpdates).filter(k => k.endsWith('/anonymized')).length} departures`); + } + + const arrivalsSnap = await db.ref('/arrivals') + .orderByChild('dateTime') + .endAt(cutoffIso) + .once('value'); + + const arrivalUpdates = getAnonymizationUpdates(arrivalsSnap, cutoffIso); + + if (Object.keys(arrivalUpdates).length > 0) { + await db.ref('/arrivals').update(arrivalUpdates); + console.log(`Anonymized ${Object.keys(arrivalUpdates).filter(k => k.endsWith('/anonymized')).length} arrivals`); + } + + if (Object.keys(departureUpdates).length === 0 && Object.keys(arrivalUpdates).length === 0) { + console.log('No movements to anonymize'); + } + }); diff --git a/functions/anonymizeMovements.spec.js b/functions/anonymizeMovements.spec.js new file mode 100644 index 00000000..f97f2d61 --- /dev/null +++ b/functions/anonymizeMovements.spec.js @@ -0,0 +1,232 @@ +'use strict'; + +let mockRefData = {}; +let mockCurrentRefPath = ''; + +const mockUpdate = jest.fn().mockResolvedValue(); +const mockOnce = jest.fn().mockImplementation(() => { + return Promise.resolve(mockRefData[mockCurrentRefPath] || { forEach: () => {} }); +}); +const mockEndAt = jest.fn().mockReturnValue({ once: mockOnce }); +const mockOrderByChild = jest.fn().mockReturnValue({ endAt: mockEndAt }); +const mockRef = jest.fn().mockImplementation(path => { + mockCurrentRefPath = path; + return { orderByChild: mockOrderByChild, update: mockUpdate, once: mockOnce }; +}); + +jest.mock('firebase-admin', () => ({ + database: jest.fn().mockReturnValue({ ref: mockRef }), + initializeApp: jest.fn(), +})); + +jest.mock('firebase-functions', () => ({ + region: jest.fn().mockReturnThis(), + pubsub: { + schedule: jest.fn().mockReturnValue({ + timeZone: jest.fn().mockReturnValue({ + onRun: jest.fn(fn => fn), + }), + }), + }, +})); + +function createSnapshot(data) { + const entries = Object.entries(data); + return { + forEach(cb) { + entries.forEach(([key, val]) => { + cb({ key, val: () => val }); + }); + }, + }; +} + +function createValueSnapshot(val) { + return { val: () => val }; +} + +describe('anonymizeMovements', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + mockRefData = {}; + mockCurrentRefPath = ''; + }); + + it('should skip when movementRetentionDays is not set', async () => { + mockRefData['/settings/movementRetentionDays'] = createValueSnapshot(null); + + const { scheduledAnonymizeMovements } = require('./anonymizeMovements'); + await scheduledAnonymizeMovements(); + + expect(mockOrderByChild).not.toHaveBeenCalled(); + expect(mockUpdate).not.toHaveBeenCalled(); + }); + + it('should anonymize movements older than configured retention', async () => { + mockRefData['/settings/movementRetentionDays'] = createValueSnapshot(730); + + const oldDate = '2023-01-15T10:00:00.000Z'; + const movementSnapshot = createSnapshot({ + 'mov1': { + dateTime: oldDate, + firstname: 'Hans', + lastname: 'Muster', + email: 'hans@example.com', + phone: '+41791234567', + memberNr: '123', + immatriculation: 'HB-ABC', + aircraftType: 'C172', + mtow: 1111, + remarks: 'test', + createdBy: 'uid1', + createdBy_orderKey: 'uid1_123', + privacyPolicyAcceptedAt: '2023-01-15T09:55:00.000Z', + location: 'LSZT', + flightType: 'private', + negativeTimestamp: -1673776800000, + }, + }); + + mockRefData['/departures'] = movementSnapshot; + mockRefData['/arrivals'] = movementSnapshot; + + const { scheduledAnonymizeMovements } = require('./anonymizeMovements'); + await scheduledAnonymizeMovements(); + + expect(mockUpdate).toHaveBeenCalledTimes(2); + + const departureUpdates = mockUpdate.mock.calls[0][0]; + + expect(departureUpdates['mov1/firstname']).toBeNull(); + expect(departureUpdates['mov1/lastname']).toBeNull(); + expect(departureUpdates['mov1/email']).toBeNull(); + expect(departureUpdates['mov1/phone']).toBeNull(); + expect(departureUpdates['mov1/memberNr']).toBeNull(); + expect(departureUpdates['mov1/immatriculation']).toBeNull(); + expect(departureUpdates['mov1/remarks']).toBeNull(); + expect(departureUpdates['mov1/createdBy']).toBeNull(); + expect(departureUpdates['mov1/createdBy_orderKey']).toBeNull(); + expect(departureUpdates['mov1/privacyPolicyAcceptedAt']).toBeNull(); + expect(departureUpdates['mov1/anonymized']).toBe(true); + + // Non-PII fields are preserved + expect(departureUpdates['mov1/aircraftType']).toBeUndefined(); + expect(departureUpdates['mov1/mtow']).toBeUndefined(); + expect(departureUpdates['mov1/location']).toBeUndefined(); + expect(departureUpdates['mov1/flightType']).toBeUndefined(); + expect(departureUpdates['mov1/dateTime']).toBeUndefined(); + + const arrivalUpdates = mockUpdate.mock.calls[1][0]; + expect(arrivalUpdates['mov1/firstname']).toBeNull(); + expect(arrivalUpdates['mov1/anonymized']).toBe(true); + expect(arrivalUpdates['mov1/location']).toBeUndefined(); + }); + + it('should only null out PII fields that are present on the record', async () => { + mockRefData['/settings/movementRetentionDays'] = createValueSnapshot(730); + + // Movement without carriageVoucher, customsFormId, customsFormUrl + const snapshot = createSnapshot({ + 'mov1': { + dateTime: '2023-01-15T10:00:00.000Z', + firstname: 'Hans', + lastname: 'Muster', + location: 'LSZT', + }, + }); + + mockRefData['/departures'] = snapshot; + mockRefData['/arrivals'] = { forEach: () => {} }; + + const { scheduledAnonymizeMovements } = require('./anonymizeMovements'); + await scheduledAnonymizeMovements(); + + expect(mockUpdate).toHaveBeenCalledTimes(1); + + const updates = mockUpdate.mock.calls[0][0]; + expect(updates['mov1/firstname']).toBeNull(); + expect(updates['mov1/lastname']).toBeNull(); + expect(updates['mov1/anonymized']).toBe(true); + + // Fields absent from the record must not appear in the update object + expect(updates['mov1/customsFormId']).toBeUndefined(); + expect(updates['mov1/customsFormUrl']).toBeUndefined(); + expect(updates['mov1/email']).toBeUndefined(); + expect(updates['mov1/phone']).toBeUndefined(); + }); + + it('should anonymize invoiceRecipientName from paymentMethod', async () => { + mockRefData['/settings/movementRetentionDays'] = createValueSnapshot(730); + + const snapshot = createSnapshot({ + 'mov1': { + dateTime: '2023-01-15T10:00:00.000Z', + firstname: 'Hans', + lastname: 'Muster', + paymentMethod: { + method: 'invoice', + invoiceRecipientName: 'Fluggruppe Thurgau', + }, + location: 'LSZT', + }, + }); + + mockRefData['/departures'] = { forEach: () => {} }; + mockRefData['/arrivals'] = snapshot; + + const { scheduledAnonymizeMovements } = require('./anonymizeMovements'); + await scheduledAnonymizeMovements(); + + expect(mockUpdate).toHaveBeenCalledTimes(1); + + const updates = mockUpdate.mock.calls[0][0]; + expect(updates['mov1/paymentMethod/invoiceRecipientName']).toBeNull(); + // payment method itself is preserved + expect(updates['mov1/paymentMethod']).toBeUndefined(); + }); + + it('should not add paymentMethod update when invoiceRecipientName is absent', async () => { + mockRefData['/settings/movementRetentionDays'] = createValueSnapshot(730); + + const snapshot = createSnapshot({ + 'mov1': { + dateTime: '2023-01-15T10:00:00.000Z', + firstname: 'Hans', + paymentMethod: { method: 'cash' }, + location: 'LSZT', + }, + }); + + mockRefData['/departures'] = { forEach: () => {} }; + mockRefData['/arrivals'] = snapshot; + + const { scheduledAnonymizeMovements } = require('./anonymizeMovements'); + await scheduledAnonymizeMovements(); + + expect(mockUpdate).toHaveBeenCalledTimes(1); + + const updates = mockUpdate.mock.calls[0][0]; + expect(updates['mov1/paymentMethod/invoiceRecipientName']).toBeUndefined(); + }); + + it('should skip already anonymized movements', async () => { + mockRefData['/settings/movementRetentionDays'] = createValueSnapshot(730); + + const snapshot = createSnapshot({ + 'mov1': { + dateTime: '2023-01-15T10:00:00.000Z', + anonymized: true, + location: 'LSZT', + }, + }); + + mockRefData['/departures'] = snapshot; + mockRefData['/arrivals'] = snapshot; + + const { scheduledAnonymizeMovements } = require('./anonymizeMovements'); + await scheduledAnonymizeMovements(); + + expect(mockUpdate).not.toHaveBeenCalled(); + }); +}); diff --git a/functions/api/basicAuth.spec.js b/functions/api/basicAuth.spec.js new file mode 100644 index 00000000..e7937682 --- /dev/null +++ b/functions/api/basicAuth.spec.js @@ -0,0 +1,133 @@ +'use strict'; + +// basicAuth.js captures functions.config() at require time, so each describe +// block resets modules and requires the module with the relevant config. + +const makeReq = (authHeader) => ({ + headers: { authorization: authHeader || '' }, +}); + +const makeRes = () => ({ + status: jest.fn().mockReturnThis(), + send: jest.fn().mockReturnThis(), +}); + +describe('functions/api/basicAuth', () => { + describe('when config is missing', () => { + let basicAuth; + + beforeEach(() => { + jest.resetModules(); + jest.mock('firebase-functions', () => ({ + config: jest.fn(() => ({})), + })); + basicAuth = require('./basicAuth'); + }); + + it('returns 401 when api config is absent', () => { + const next = jest.fn(); + const res = makeRes(); + basicAuth(makeReq(), res, next); + expect(res.status).toHaveBeenCalledWith(401); + expect(res.send).toHaveBeenCalledWith('Unauthorized'); + expect(next).not.toHaveBeenCalled(); + }); + }); + + describe('when config is missing username', () => { + let basicAuth; + + beforeEach(() => { + jest.resetModules(); + jest.mock('firebase-functions', () => ({ + config: jest.fn(() => ({ api: { serviceuser: { password: 'pass' } } })), + })); + basicAuth = require('./basicAuth'); + }); + + it('returns 401', () => { + const next = jest.fn(); + const res = makeRes(); + basicAuth(makeReq(), res, next); + expect(res.status).toHaveBeenCalledWith(401); + expect(next).not.toHaveBeenCalled(); + }); + }); + + describe('when config is missing password', () => { + let basicAuth; + + beforeEach(() => { + jest.resetModules(); + jest.mock('firebase-functions', () => ({ + config: jest.fn(() => ({ api: { serviceuser: { username: 'user' } } })), + })); + basicAuth = require('./basicAuth'); + }); + + it('returns 401', () => { + const next = jest.fn(); + const res = makeRes(); + basicAuth(makeReq(), res, next); + expect(res.status).toHaveBeenCalledWith(401); + expect(next).not.toHaveBeenCalled(); + }); + }); + + describe('with valid config', () => { + let basicAuth; + + beforeEach(() => { + jest.resetModules(); + jest.mock('firebase-functions', () => ({ + config: jest.fn(() => ({ + api: { serviceuser: { username: 'admin', password: 's3cret' } }, + })), + })); + basicAuth = require('./basicAuth'); + }); + + it('calls next() when credentials are valid', () => { + const next = jest.fn(); + const res = makeRes(); + const credentials = Buffer.from('admin:s3cret').toString('base64'); + basicAuth(makeReq(`Basic ${credentials}`), res, next); + expect(next).toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalled(); + }); + + it('returns 401 when password is wrong', () => { + const next = jest.fn(); + const res = makeRes(); + const credentials = Buffer.from('admin:wrongpass').toString('base64'); + basicAuth(makeReq(`Basic ${credentials}`), res, next); + expect(res.status).toHaveBeenCalledWith(401); + expect(next).not.toHaveBeenCalled(); + }); + + it('returns 401 when username is wrong', () => { + const next = jest.fn(); + const res = makeRes(); + const credentials = Buffer.from('wronguser:s3cret').toString('base64'); + basicAuth(makeReq(`Basic ${credentials}`), res, next); + expect(res.status).toHaveBeenCalledWith(401); + expect(next).not.toHaveBeenCalled(); + }); + + it('returns 401 when no Authorization header is provided', () => { + const next = jest.fn(); + const res = makeRes(); + basicAuth(makeReq(''), res, next); + expect(res.status).toHaveBeenCalledWith(401); + expect(next).not.toHaveBeenCalled(); + }); + + it('returns 401 when auth type is Bearer instead of Basic', () => { + const next = jest.fn(); + const res = makeRes(); + basicAuth(makeReq('Bearer sometoken'), res, next); + expect(res.status).toHaveBeenCalledWith(401); + expect(next).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/functions/api/fetchAerodromeStatus.spec.js b/functions/api/fetchAerodromeStatus.spec.js new file mode 100644 index 00000000..6f68e1e4 --- /dev/null +++ b/functions/api/fetchAerodromeStatus.spec.js @@ -0,0 +1,50 @@ +'use strict'; + +const fetchAerodromeStatus = require('./fetchAerodromeStatus'); + +describe('functions/api/fetchAerodromeStatus', () => { + const makeFirebase = (val) => ({ + ref: jest.fn().mockReturnValue({ + orderByChild: jest.fn().mockReturnThis(), + limitToLast: jest.fn().mockReturnThis(), + once: jest.fn().mockResolvedValue({ val: () => val }), + }), + }); + + it('returns formatted status when data exists', async () => { + const timestamp = new Date('2026-01-15T10:00:00Z').getTime(); + const firebase = makeFirebase({ + abc: { status: 'open', timestamp, by: 'pilot@example.com', details: 'All clear' }, + }); + + const result = await fetchAerodromeStatus(firebase); + + expect(result).toEqual({ + status: 'open', + last_update_date: '2026-01-15T10:00:00.000Z', + last_update_by: 'pilot@example.com', + message: 'All clear', + }); + }); + + it('returns empty object when no data exists', async () => { + const firebase = makeFirebase(null); + const result = await fetchAerodromeStatus(firebase); + expect(result).toEqual({}); + }); + + it('queries /status ref ordered by timestamp limited to last 1', async () => { + const mockRef = { + orderByChild: jest.fn().mockReturnThis(), + limitToLast: jest.fn().mockReturnThis(), + once: jest.fn().mockResolvedValue({ val: () => null }), + }; + const firebase = { ref: jest.fn().mockReturnValue(mockRef) }; + + await fetchAerodromeStatus(firebase); + + expect(firebase.ref).toHaveBeenCalledWith('/status'); + expect(mockRef.orderByChild).toHaveBeenCalledWith('timestamp'); + expect(mockRef.limitToLast).toHaveBeenCalledWith(1); + }); +}); diff --git a/functions/api/fetchUserInvoiceRecipients.spec.js b/functions/api/fetchUserInvoiceRecipients.spec.js new file mode 100644 index 00000000..f8237f91 --- /dev/null +++ b/functions/api/fetchUserInvoiceRecipients.spec.js @@ -0,0 +1,58 @@ +'use strict'; + +const fetchUserInvoiceRecipients = require('./fetchUserInvoiceRecipients'); + +describe('functions/api/fetchUserInvoiceRecipients', () => { + const makeFirebase = (val) => ({ + ref: jest.fn().mockReturnValue({ + once: jest.fn().mockResolvedValue({ val: () => val }), + }), + }); + + it('returns empty array when authEmail is falsy', async () => { + const firebase = makeFirebase(null); + expect(await fetchUserInvoiceRecipients(firebase, null)).toEqual([]); + expect(await fetchUserInvoiceRecipients(firebase, '')).toEqual([]); + expect(firebase.ref).not.toHaveBeenCalled(); + }); + + it('returns empty array when no invoice recipients exist in db', async () => { + const firebase = makeFirebase(null); + const result = await fetchUserInvoiceRecipients(firebase, 'user@example.com'); + expect(result).toEqual([]); + }); + + it('returns recipient names matching the authEmail', async () => { + const firebase = makeFirebase([ + { name: 'Recipient A', emails: ['user@example.com', 'other@example.com'] }, + { name: 'Recipient B', emails: ['other@example.com'] }, + { name: 'Recipient C', emails: ['user@example.com'] }, + ]); + const result = await fetchUserInvoiceRecipients(firebase, 'user@example.com'); + expect(result).toEqual(['Recipient A', 'Recipient C']); + }); + + it('returns empty array when no recipient matches authEmail', async () => { + const firebase = makeFirebase([ + { name: 'Recipient A', emails: ['other@example.com'] }, + ]); + const result = await fetchUserInvoiceRecipients(firebase, 'user@example.com'); + expect(result).toEqual([]); + }); + + it('handles recipients without emails field', async () => { + const firebase = makeFirebase([ + { name: 'Recipient A' }, + { name: 'Recipient B', emails: ['user@example.com'] }, + ]); + const result = await fetchUserInvoiceRecipients(firebase, 'user@example.com'); + expect(result).toEqual(['Recipient B']); + }); + + it('queries /settings/invoiceRecipients ref', async () => { + const mockRef = { once: jest.fn().mockResolvedValue({ val: () => null }) }; + const firebase = { ref: jest.fn().mockReturnValue(mockRef) }; + await fetchUserInvoiceRecipients(firebase, 'user@example.com'); + expect(firebase.ref).toHaveBeenCalledWith('/settings/invoiceRecipients'); + }); +}); diff --git a/functions/associatedMovements/setAssociatedMovementsTriggers.js b/functions/associatedMovements/setAssociatedMovementsTriggers.js index 7fbdc82b..0765632c 100644 --- a/functions/associatedMovements/setAssociatedMovementsTriggers.js +++ b/functions/associatedMovements/setAssociatedMovementsTriggers.js @@ -132,6 +132,9 @@ const updateOnWrite = async (change, type) => { // — those are handled by the dedicated onCreate and onDelete triggers. if (!change.before.val() || !change.after.val()) return + // Skip anonymized movements (PII has been stripped by the retention job) + if (change.after.val().anonymized) return + const snap = change.after functions.logger.log(`Setting associated movement for updated ${type} ${snap.ref.key}`) diff --git a/functions/auth/cleanupExpiredSignInCodes.js b/functions/auth/cleanupExpiredSignInCodes.js new file mode 100644 index 00000000..b911d202 --- /dev/null +++ b/functions/auth/cleanupExpiredSignInCodes.js @@ -0,0 +1,34 @@ +'use strict'; + +const functions = require('firebase-functions'); +const admin = require('firebase-admin'); + +const MAX_ATTEMPTS = 5; + +exports.cleanupExpiredSignInCodes = functions + .region('europe-west1') + .pubsub + .schedule('every 60 minutes') + .onRun(async () => { + const db = admin.database(); + const codesRef = db.ref('/signInCodes'); + const snapshot = await codesRef.once('value'); + + if (!snapshot.exists()) { + return; + } + + const updates = {}; + const now = Date.now(); + + snapshot.forEach(child => { + const val = child.val(); + if (val.expiry <= now || val.attempts >= MAX_ATTEMPTS) { + updates[child.key] = null; + } + }); + + if (Object.keys(updates).length > 0) { + await codesRef.update(updates); + } + }); diff --git a/functions/auth/cleanupExpiredSignInCodes.spec.js b/functions/auth/cleanupExpiredSignInCodes.spec.js new file mode 100644 index 00000000..aa73a2a6 --- /dev/null +++ b/functions/auth/cleanupExpiredSignInCodes.spec.js @@ -0,0 +1,131 @@ +describe('functions', () => { + describe('auth/cleanupExpiredSignInCodes', () => { + let mockAdmin; + let mockFunctions; + let capturedHandler; + let mockCodesRef; + + const now = Date.now(); + + beforeEach(() => { + jest.resetModules(); + capturedHandler = null; + + mockCodesRef = { + once: jest.fn(), + update: jest.fn().mockResolvedValue(undefined), + }; + + mockAdmin = { + database: jest.fn().mockReturnValue({ + ref: jest.fn().mockReturnValue(mockCodesRef) + }) + }; + + const mockSchedule = { + onRun: jest.fn().mockImplementation(handler => { capturedHandler = handler; }) + }; + + mockFunctions = { + pubsub: { + schedule: jest.fn().mockReturnValue(mockSchedule) + } + }; + mockFunctions.region = jest.fn(() => mockFunctions); + + jest.mock('firebase-admin', () => mockAdmin); + jest.mock('firebase-functions', () => mockFunctions); + + require('./cleanupExpiredSignInCodes'); + }); + + const makeSnapshot = (entries) => ({ + exists: () => entries.length > 0, + forEach: (cb) => entries.forEach(({ key, val }) => cb({ key, val: () => val })), + }); + + it('does nothing when no codes exist', async () => { + mockCodesRef.once.mockResolvedValue(makeSnapshot([])); + await capturedHandler(); + expect(mockCodesRef.update).not.toHaveBeenCalled(); + }); + + it('deletes expired codes', async () => { + const snapshot = makeSnapshot([ + { key: 'k1', val: { expiry: now - 1000 } }, + { key: 'k2', val: { expiry: now - 5000 } }, + ]); + mockCodesRef.once.mockResolvedValue(snapshot); + + await capturedHandler(); + + expect(mockCodesRef.update).toHaveBeenCalledWith({ k1: null, k2: null }); + }); + + it('keeps non-expired codes', async () => { + const snapshot = makeSnapshot([ + { key: 'k1', val: { expiry: now + 60000 } }, + { key: 'k2', val: { expiry: now + 600000 } }, + ]); + mockCodesRef.once.mockResolvedValue(snapshot); + + await capturedHandler(); + + expect(mockCodesRef.update).not.toHaveBeenCalled(); + }); + + it('only deletes expired codes when mix of expired and non-expired', async () => { + const snapshot = makeSnapshot([ + { key: 'k1', val: { expiry: now - 1000 } }, + { key: 'k2', val: { expiry: now + 60000 } }, + { key: 'k3', val: { expiry: now - 5000 } }, + ]); + mockCodesRef.once.mockResolvedValue(snapshot); + + await capturedHandler(); + + expect(mockCodesRef.update).toHaveBeenCalledWith({ k1: null, k3: null }); + }); + + it('deletes exhausted codes (attempts >= 5) that have not yet expired', async () => { + const snapshot = makeSnapshot([ + { key: 'k1', val: { expiry: now + 60000, attempts: 5 } }, + { key: 'k2', val: { expiry: now + 60000, attempts: 6 } }, + ]); + mockCodesRef.once.mockResolvedValue(snapshot); + + await capturedHandler(); + + expect(mockCodesRef.update).toHaveBeenCalledWith({ k1: null, k2: null }); + }); + + it('keeps non-exhausted non-expired codes', async () => { + const snapshot = makeSnapshot([ + { key: 'k1', val: { expiry: now + 60000, attempts: 4 } }, + { key: 'k2', val: { expiry: now + 60000, attempts: 0 } }, + ]); + mockCodesRef.once.mockResolvedValue(snapshot); + + await capturedHandler(); + + expect(mockCodesRef.update).not.toHaveBeenCalled(); + }); + + it('deletes both expired and exhausted codes in one pass', async () => { + const snapshot = makeSnapshot([ + { key: 'k1', val: { expiry: now - 1000, attempts: 0 } }, + { key: 'k2', val: { expiry: now + 60000, attempts: 5 } }, + { key: 'k3', val: { expiry: now + 60000, attempts: 2 } }, + ]); + mockCodesRef.once.mockResolvedValue(snapshot); + + await capturedHandler(); + + expect(mockCodesRef.update).toHaveBeenCalledWith({ k1: null, k2: null }); + }); + + it('is scheduled to run every 60 minutes', () => { + expect(mockFunctions.pubsub.schedule).toHaveBeenCalledWith('every 60 minutes'); + }); + }); +}); diff --git a/functions/auth/createTestEmailToken.js b/functions/auth/createTestEmailToken.js new file mode 100644 index 00000000..356ca9d2 --- /dev/null +++ b/functions/auth/createTestEmailToken.js @@ -0,0 +1,41 @@ +'use strict'; + +const functions = require('firebase-functions'); +const admin = require('firebase-admin'); +const cors = require('cors')({origin: true}); + +const TEST_EMAIL = 'cypress-pilot@example.com'; + +exports.createTestEmailToken = functions.region('europe-west1').https.onRequest((req, res) => { + return cors(req, res, async () => { + try { + const config = functions.config(); + if (!config.testing || !config.testing.enabled) { + return res.status(403).json({ error: 'Testing endpoints are not enabled' }); + } + + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + let uid; + try { + const userRecord = await admin.auth().getUserByEmail(TEST_EMAIL); + uid = userRecord.uid; + } catch (e) { + if (e.code === 'auth/user-not-found') { + const newUser = await admin.auth().createUser({ email: TEST_EMAIL }); + uid = newUser.uid; + } else { + throw e; + } + } + + const customToken = await admin.auth().createCustomToken(uid, { email: TEST_EMAIL }); + res.status(200).json({ token: customToken }); + } catch (error) { + console.error('Error creating test email token:', error); + res.status(500).json({ error: 'Failed to create test email token' }); + } + }); +}); diff --git a/functions/auth/emailTemplates.js b/functions/auth/emailTemplates.js index c766170f..9090e869 100644 --- a/functions/auth/emailTemplates.js +++ b/functions/auth/emailTemplates.js @@ -14,9 +14,9 @@ const readTemplate = (templateName, format) => { return fs.readFileSync(templatePath, 'utf8'); }; -const getSignInEmailContent = ({ signInLink, airportName, themeColor }) => { +const getSignInEmailContent = ({ signInCode, airportName, themeColor }) => { const replacements = { - signInLink, + signInCode, airportName, themeColor }; diff --git a/functions/auth/emailTemplates.spec.js b/functions/auth/emailTemplates.spec.js index 839a3a16..e03b0039 100644 --- a/functions/auth/emailTemplates.spec.js +++ b/functions/auth/emailTemplates.spec.js @@ -11,9 +11,9 @@ describe('functions', () => { beforeEach(() => { fs.readFileSync.mockImplementation((filePath) => { if (filePath.endsWith('.html')) { - return '{{airportName}}{{themeColor}}'; + return '

{{signInCode}}

{{airportName}}{{themeColor}}'; } - return 'Sign in at {{signInLink}} for {{airportName}}'; + return 'Code: {{signInCode}} for {{airportName}}'; }); }); @@ -23,7 +23,7 @@ describe('functions', () => { it('returns subject, html, and text', () => { const result = getSignInEmailContent({ - signInLink: 'https://example.com/signin', + signInCode: '123456', airportName: 'Thun Airport', themeColor: '#003863' }); @@ -32,19 +32,19 @@ describe('functions', () => { expect(result.text).toBeDefined(); }); - it('replaces signInLink placeholder in html', () => { + it('replaces signInCode placeholder in html', () => { const result = getSignInEmailContent({ - signInLink: 'https://example.com/signin', + signInCode: '123456', airportName: 'Thun', themeColor: '#003863' }); - expect(result.html).toContain('https://example.com/signin'); - expect(result.html).not.toContain('{{signInLink}}'); + expect(result.html).toContain('123456'); + expect(result.html).not.toContain('{{signInCode}}'); }); it('replaces airportName placeholder in html', () => { const result = getSignInEmailContent({ - signInLink: 'https://example.com/signin', + signInCode: '123456', airportName: 'Thun Airport', themeColor: '#003863' }); @@ -54,7 +54,7 @@ describe('functions', () => { it('replaces themeColor placeholder in html', () => { const result = getSignInEmailContent({ - signInLink: 'https://example.com/signin', + signInCode: '123456', airportName: 'Thun', themeColor: '#003863' }); @@ -62,30 +62,30 @@ describe('functions', () => { expect(result.html).not.toContain('{{themeColor}}'); }); - it('replaces signInLink placeholder in text', () => { + it('replaces signInCode placeholder in text', () => { const result = getSignInEmailContent({ - signInLink: 'https://example.com/signin', + signInCode: '123456', airportName: 'Thun', themeColor: '#003863' }); - expect(result.text).toContain('https://example.com/signin'); - expect(result.text).not.toContain('{{signInLink}}'); + expect(result.text).toContain('123456'); + expect(result.text).not.toContain('{{signInCode}}'); }); it('leaves unknown placeholders unchanged', () => { - fs.readFileSync.mockReturnValue('{{unknownKey}} {{signInLink}}'); + fs.readFileSync.mockReturnValue('{{unknownKey}} {{signInCode}}'); const result = getSignInEmailContent({ - signInLink: 'https://example.com', + signInCode: '654321', airportName: 'Thun', themeColor: '#003863' }); expect(result.html).toContain('{{unknownKey}}'); - expect(result.html).toContain('https://example.com'); + expect(result.html).toContain('654321'); }); it('reads html template from correct path', () => { getSignInEmailContent({ - signInLink: 'https://example.com', + signInCode: '123456', airportName: 'Thun', themeColor: '#003863' }); @@ -97,7 +97,7 @@ describe('functions', () => { it('reads text template from correct path', () => { getSignInEmailContent({ - signInLink: 'https://example.com', + signInCode: '123456', airportName: 'Thun', themeColor: '#003863' }); diff --git a/functions/auth/generateSignInCode.js b/functions/auth/generateSignInCode.js new file mode 100644 index 00000000..7d336493 --- /dev/null +++ b/functions/auth/generateSignInCode.js @@ -0,0 +1,66 @@ +'use strict'; + +const functions = require('firebase-functions'); +const admin = require('firebase-admin'); +const crypto = require('crypto'); +const cors = require('cors')({origin: true}); +const { sendSignInEmail } = require('./sendSignInEmail'); + +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +const CODE_EXPIRY_MS = 10 * 60 * 1000; // 10 minutes + +const hashCode = (code) => { + return crypto.createHash('sha256').update(code).digest('hex'); +}; + +const validateRequest = (method, body) => { + if (method !== 'POST') { + return { error: 'Method not allowed', status: 405 }; + } + + const { email } = body; + + if (!email) { + return { error: 'Email is required', status: 400 }; + } + + if (!EMAIL_REGEX.test(email)) { + return { error: 'Invalid email format', status: 400 }; + } + + return null; +}; + +exports.generateSignInCode = functions.region('europe-west1').https.onRequest((req, res) => { + return cors(req, res, async () => { + try { + const validationError = validateRequest(req.method, req.body); + if (validationError) { + return res.status(validationError.status).json({ error: validationError.error }); + } + + const { email, airportName, themeColor } = req.body; + const normalizedEmail = email.toLowerCase(); + + const code = String(crypto.randomInt(0, 1000000)).padStart(6, '0'); + const codeHash = hashCode(code); + const expiry = Date.now() + CODE_EXPIRY_MS; + + await admin.database().ref('/signInCodes').push({ + email: normalizedEmail, + codeHash, + expiry, + attempts: 0, + }); + + await sendSignInEmail({ email: normalizedEmail, signInCode: code, airportName, themeColor }); + + res.status(200).json({ success: true }); + } catch (error) { + console.error('Error generating sign-in code:', error); + res.status(500).json({ + error: 'Failed to send sign-in code', + }); + } + }); +}); diff --git a/functions/auth/generateSignInCode.spec.js b/functions/auth/generateSignInCode.spec.js new file mode 100644 index 00000000..07ff5224 --- /dev/null +++ b/functions/auth/generateSignInCode.spec.js @@ -0,0 +1,194 @@ +describe('functions', () => { + describe('auth/generateSignInCode', () => { + let mockAdmin; + let mockFunctions; + let mockSendSignInEmail; + let capturedHandler; + let mockCors; + let mockPush; + let mockCrypto; + + beforeEach(() => { + jest.resetModules(); + capturedHandler = null; + + mockPush = jest.fn().mockResolvedValue({ key: 'code-key-123' }); + + mockAdmin = { + database: jest.fn().mockReturnValue({ + ref: jest.fn().mockReturnValue({ push: mockPush }) + }) + }; + + mockCors = jest.fn().mockImplementation((req, res, cb) => cb()); + + mockFunctions = { + https: { + onRequest: jest.fn().mockImplementation(handler => { capturedHandler = handler; }) + } + }; + mockFunctions.region = jest.fn(() => mockFunctions); + + mockSendSignInEmail = jest.fn().mockResolvedValue('msg123'); + + jest.mock('firebase-admin', () => mockAdmin); + jest.mock('firebase-functions', () => mockFunctions); + jest.mock('cors', () => () => mockCors); + jest.mock('./sendSignInEmail', () => ({ sendSignInEmail: mockSendSignInEmail })); + + require('./generateSignInCode'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + const makeReq = (method, body) => ({ method, body }); + const makeRes = () => ({ + status: jest.fn().mockReturnThis(), + json: jest.fn() + }); + + it('returns 405 for GET request', async () => { + const req = makeReq('GET', {}); + const res = makeRes(); + await capturedHandler(req, res); + expect(res.status).toHaveBeenCalledWith(405); + expect(res.json).toHaveBeenCalledWith({ error: 'Method not allowed' }); + }); + + it('returns 400 when email is missing', async () => { + const req = makeReq('POST', {}); + const res = makeRes(); + await capturedHandler(req, res); + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ error: 'Email is required' }); + }); + + it('returns 400 for invalid email format', async () => { + const req = makeReq('POST', { email: 'not-an-email' }); + const res = makeRes(); + await capturedHandler(req, res); + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ error: 'Invalid email format' }); + }); + + it('stores code hash in database (not plaintext code)', async () => { + const req = makeReq('POST', { + email: 'user@example.com', + airportName: 'Thun', + themeColor: '#003863' + }); + const res = makeRes(); + await capturedHandler(req, res); + + expect(mockPush).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'user@example.com', + codeHash: expect.any(String), + expiry: expect.any(Number), + attempts: 0, + }) + ); + + // Verify that the pushed object does NOT contain the plaintext code + const pushedData = mockPush.mock.calls[0][0]; + expect(pushedData).not.toHaveProperty('code'); + }); + + it('normalizes email to lowercase before storing', async () => { + const req = makeReq('POST', { email: 'User@Example.COM' }); + const res = makeRes(); + await capturedHandler(req, res); + + const pushedData = mockPush.mock.calls[0][0]; + expect(pushedData.email).toBe('user@example.com'); + }); + + it('sends email server-side with correct parameters', async () => { + const req = makeReq('POST', { + email: 'user@example.com', + airportName: 'Thun', + themeColor: '#003863' + }); + const res = makeRes(); + await capturedHandler(req, res); + + expect(mockSendSignInEmail).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'user@example.com', + signInCode: expect.any(String), + airportName: 'Thun', + themeColor: '#003863', + }) + ); + }); + + it('returns success without code in response', async () => { + const req = makeReq('POST', { + email: 'user@example.com', + airportName: 'Thun', + themeColor: '#003863' + }); + const res = makeRes(); + await capturedHandler(req, res); + + expect(res.status).toHaveBeenCalledWith(200); + const responseData = res.json.mock.calls[0][0]; + expect(responseData).toEqual({ success: true }); + expect(responseData).not.toHaveProperty('code'); + }); + + it('returns 500 without error details when sendSignInEmail throws', async () => { + mockSendSignInEmail.mockRejectedValue(new Error('SMTP connection failed: password=secret123')); + + const req = makeReq('POST', { email: 'user@example.com' }); + const res = makeRes(); + await capturedHandler(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + const responseData = res.json.mock.calls[0][0]; + expect(responseData).toEqual({ error: 'Failed to send sign-in code' }); + expect(responseData).not.toHaveProperty('details'); + }); + + it('returns 500 without error details when database push throws', async () => { + mockPush.mockRejectedValue(new Error('DB permission denied')); + + const req = makeReq('POST', { email: 'user@example.com' }); + const res = makeRes(); + await capturedHandler(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + const responseData = res.json.mock.calls[0][0]; + expect(responseData).toEqual({ error: 'Failed to send sign-in code' }); + expect(responseData).not.toHaveProperty('details'); + }); + + it('sets expiry approximately 10 minutes in the future', async () => { + const before = Date.now(); + const req = makeReq('POST', { email: 'user@example.com' }); + const res = makeRes(); + await capturedHandler(req, res); + const after = Date.now(); + + const pushedData = mockPush.mock.calls[0][0]; + const tenMinutesMs = 10 * 60 * 1000; + expect(pushedData.expiry).toBeGreaterThanOrEqual(before + tenMinutesMs); + expect(pushedData.expiry).toBeLessThanOrEqual(after + tenMinutesMs); + }); + + it('stores SHA-256 hash of code, not plaintext', async () => { + const crypto = require('crypto'); + const req = makeReq('POST', { email: 'user@example.com' }); + const res = makeRes(); + await capturedHandler(req, res); + + const pushedData = mockPush.mock.calls[0][0]; + // The sent signInCode should hash to the stored codeHash + const sentCode = mockSendSignInEmail.mock.calls[0][0].signInCode; + const expectedHash = crypto.createHash('sha256').update(sentCode).digest('hex'); + expect(pushedData.codeHash).toBe(expectedHash); + }); + }); +}); diff --git a/functions/auth/sendSignInEmail.js b/functions/auth/sendSignInEmail.js index 0c57d968..1b5aed85 100644 --- a/functions/auth/sendSignInEmail.js +++ b/functions/auth/sendSignInEmail.js @@ -1,30 +1,17 @@ -const functions = require('firebase-functions'); +'use strict'; + const admin = require('firebase-admin'); -const cors = require('cors')({origin: true}); const nodemailer = require('nodemailer'); const { getSignInEmailContent } = require('./emailTemplates'); const REQUIRED_SMTP_SETTINGS = ['host', 'port', 'user', 'password', 'fromEmail', 'fromName']; -const validateRequest = (method, body) => { - if (method !== 'POST') { - return { error: 'Method not allowed', status: 405 }; - } - - const { email, signInLink } = body; - if (!email || !signInLink) { - return { error: 'Email and signInLink are required', status: 400 }; - } - - return null; -}; - const loadSmtpSettings = async () => { const snapshot = await admin.database().ref('/settings/emailSmtp').once('value'); const settings = snapshot.val(); if (!settings) { - throw new Error('SMTP settings not found in database at /settings/emailSmtp'); + throw new Error('SMTP settings not found in database'); } const missingSettings = REQUIRED_SMTP_SETTINGS.filter(setting => !settings[setting]); @@ -47,41 +34,20 @@ const createTransporter = (settings) => { }); }; -exports.sendSignInEmail = functions.region('europe-west1').https.onRequest((req, res) => { - return cors(req, res, async () => { - try { - const validationError = validateRequest(req.method, req.body); - if (validationError) { - return res.status(validationError.status).json({ error: validationError.error }); - } - - const { email, signInLink, airportName, themeColor } = req.body; - - const smtpSettings = await loadSmtpSettings(); - const emailContent = getSignInEmailContent({ signInLink, airportName, themeColor }); - const transporter = createTransporter(smtpSettings); - - const info = await transporter.sendMail({ - from: `"${smtpSettings.fromName}" <${smtpSettings.fromEmail}>`, - to: email, - subject: emailContent.subject, - text: emailContent.text, - html: emailContent.html, - }); +const sendSignInEmail = async ({ email, signInCode, airportName, themeColor }) => { + const smtpSettings = await loadSmtpSettings(); + const emailContent = getSignInEmailContent({ signInCode, airportName, themeColor }); + const transporter = createTransporter(smtpSettings); + + const info = await transporter.sendMail({ + from: `"${smtpSettings.fromName}" <${smtpSettings.fromEmail}>`, + to: email, + subject: emailContent.subject, + text: emailContent.text, + html: emailContent.html, + }); - console.log('Sign-in email sent successfully:', info.messageId); + return info.messageId; +}; - res.status(200).json({ - success: true, - message: 'Sign-in email sent successfully', - messageId: info.messageId - }); - } catch (error) { - console.error('Error sending sign-in email:', error); - res.status(500).json({ - error: 'Failed to send sign-in email', - details: error.message - }); - } - }); -}); +module.exports = { sendSignInEmail, loadSmtpSettings, createTransporter }; diff --git a/functions/auth/sendSignInEmail.spec.js b/functions/auth/sendSignInEmail.spec.js index 1e981749..a77db389 100644 --- a/functions/auth/sendSignInEmail.spec.js +++ b/functions/auth/sendSignInEmail.spec.js @@ -1,25 +1,28 @@ describe('functions', () => { describe('auth/sendSignInEmail', () => { let mockAdmin; - let mockFunctions; let mockNodemailer; let mockEmailTemplates; - let capturedHandler; - let mockCors; + let mockSendMail; + let mockTransporter; + let sendSignInEmail; + let loadSmtpSettings; + let createTransporter; + + const smtpSettings = { + host: 'smtp.example.com', + port: '587', + user: 'user@example.com', + password: 'secret', + fromEmail: 'noreply@example.com', + fromName: 'Flightbox' + }; beforeEach(() => { jest.resetModules(); - capturedHandler = null; const mockOnce = jest.fn().mockResolvedValue({ - val: () => ({ - host: 'smtp.example.com', - port: '587', - user: 'user@example.com', - password: 'secret', - fromEmail: 'noreply@example.com', - fromName: 'Flightbox' - }) + val: () => smtpSettings }); mockAdmin = { @@ -28,17 +31,8 @@ describe('functions', () => { }) }; - mockCors = jest.fn().mockImplementation((req, res, cb) => cb()); - - mockFunctions = { - https: { - onRequest: jest.fn().mockImplementation(handler => { capturedHandler = handler; }) - } - }; - mockFunctions.region = jest.fn(() => mockFunctions); - - const mockSendMail = jest.fn().mockResolvedValue({ messageId: 'msg123' }); - const mockTransporter = { sendMail: mockSendMail }; + mockSendMail = jest.fn().mockResolvedValue({ messageId: 'msg123' }); + mockTransporter = { sendMail: mockSendMail }; mockNodemailer = { createTransport: jest.fn().mockReturnValue(mockTransporter) @@ -47,117 +41,126 @@ describe('functions', () => { mockEmailTemplates = { getSignInEmailContent: jest.fn().mockReturnValue({ subject: 'Sign in', - html: '

Click here

', - text: 'Click here' + html: '

Your code: 123456

', + text: 'Your code: 123456' }) }; jest.mock('firebase-admin', () => mockAdmin); - jest.mock('firebase-functions', () => mockFunctions); - jest.mock('cors', () => () => mockCors); jest.mock('nodemailer', () => mockNodemailer); jest.mock('./emailTemplates', () => mockEmailTemplates); - require('./sendSignInEmail'); - }); - - const makeReq = (method, body) => ({ method, body }); - const makeRes = () => ({ - status: jest.fn().mockReturnThis(), - json: jest.fn() + const module = require('./sendSignInEmail'); + sendSignInEmail = module.sendSignInEmail; + loadSmtpSettings = module.loadSmtpSettings; + createTransporter = module.createTransporter; }); - it('returns 405 for GET request', async () => { - const req = makeReq('GET', {}); - const res = makeRes(); - await capturedHandler(req, res); - expect(res.status).toHaveBeenCalledWith(405); - }); + describe('sendSignInEmail', () => { + it('sends email with correct parameters', async () => { + const messageId = await sendSignInEmail({ + email: 'user@example.com', + signInCode: '123456', + airportName: 'Thun', + themeColor: '#003863' + }); + + expect(mockSendMail).toHaveBeenCalledWith( + expect.objectContaining({ + to: 'user@example.com', + subject: 'Sign in', + }) + ); + expect(messageId).toBe('msg123'); + }); - it('returns 400 when email is missing', async () => { - const req = makeReq('POST', { signInLink: 'https://link' }); - const res = makeRes(); - await capturedHandler(req, res); - expect(res.status).toHaveBeenCalledWith(400); - expect(res.json).toHaveBeenCalledWith({ error: 'Email and signInLink are required' }); - }); + it('calls getSignInEmailContent with correct params', async () => { + await sendSignInEmail({ + email: 'user@example.com', + signInCode: '123456', + airportName: 'Thun', + themeColor: '#003' + }); + + expect(mockEmailTemplates.getSignInEmailContent).toHaveBeenCalledWith({ + signInCode: '123456', + airportName: 'Thun', + themeColor: '#003' + }); + }); - it('returns 400 when signInLink is missing', async () => { - const req = makeReq('POST', { email: 'user@example.com' }); - const res = makeRes(); - await capturedHandler(req, res); - expect(res.status).toHaveBeenCalledWith(400); - }); + it('throws when SMTP settings are missing', async () => { + mockAdmin.database().ref().once.mockResolvedValue({ val: () => null }); - it('sends email and returns 200 on success', async () => { - const req = makeReq('POST', { - email: 'user@example.com', - signInLink: 'https://sign-in.example.com', - airportName: 'Thun', - themeColor: '#003863' + await expect(sendSignInEmail({ + email: 'user@example.com', + signInCode: '123456' + })).rejects.toThrow('SMTP settings not found'); }); - const res = makeRes(); - await capturedHandler(req, res); - expect(res.status).toHaveBeenCalledWith(200); - expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ - success: true, - messageId: 'msg123' - })); - }); - it('calls getSignInEmailContent with correct params', async () => { - const req = makeReq('POST', { - email: 'user@example.com', - signInLink: 'https://link', - airportName: 'Thun', - themeColor: '#003' + it('throws when required SMTP settings are incomplete', async () => { + mockAdmin.database().ref().once.mockResolvedValue({ + val: () => ({ host: 'smtp.example.com' }) + }); + + await expect(sendSignInEmail({ + email: 'user@example.com', + signInCode: '123456' + })).rejects.toThrow('Missing SMTP settings'); }); - const res = makeRes(); - await capturedHandler(req, res); - expect(mockEmailTemplates.getSignInEmailContent).toHaveBeenCalledWith({ - signInLink: 'https://link', - airportName: 'Thun', - themeColor: '#003' + + it('uses correct from address', async () => { + await sendSignInEmail({ + email: 'user@example.com', + signInCode: '123456' + }); + + const mailArgs = mockSendMail.mock.calls[0][0]; + expect(mailArgs.from).toBe('"Flightbox" '); }); }); - it('returns 500 when SMTP settings are missing', async () => { - mockAdmin.database().ref().once.mockResolvedValue({ val: () => null }); - const req = makeReq('POST', { - email: 'user@example.com', - signInLink: 'https://link' + describe('loadSmtpSettings', () => { + it('resolves with settings when all fields present', async () => { + const settings = await loadSmtpSettings(); + expect(settings).toEqual(smtpSettings); }); - const res = makeRes(); - await capturedHandler(req, res); - expect(res.status).toHaveBeenCalledWith(500); - }); - it('returns 500 when required SMTP settings are incomplete', async () => { - mockAdmin.database().ref().once.mockResolvedValue({ - val: () => ({ host: 'smtp.example.com' }) // missing port, user, etc. + it('throws when settings are null', async () => { + mockAdmin.database().ref().once.mockResolvedValue({ val: () => null }); + await expect(loadSmtpSettings()).rejects.toThrow('SMTP settings not found'); }); - const req = makeReq('POST', { - email: 'user@example.com', - signInLink: 'https://link' + + it('throws when settings are incomplete', async () => { + mockAdmin.database().ref().once.mockResolvedValue({ + val: () => ({ host: 'smtp.example.com' }) + }); + await expect(loadSmtpSettings()).rejects.toThrow('Missing SMTP settings'); }); - const res = makeRes(); - await capturedHandler(req, res); - expect(res.status).toHaveBeenCalledWith(500); }); - it('creates transporter with correct SMTP settings', async () => { - const req = makeReq('POST', { - email: 'user@example.com', - signInLink: 'https://link' + describe('createTransporter', () => { + it('creates transporter with correct settings', () => { + createTransporter(smtpSettings); + expect(mockNodemailer.createTransport).toHaveBeenCalledWith( + expect.objectContaining({ + host: 'smtp.example.com', + port: 587, + secure: true, + auth: { + user: 'user@example.com', + pass: 'secret' + } + }) + ); + }); + + it('parses port as integer', () => { + createTransporter({ ...smtpSettings, port: '465' }); + const callArgs = mockNodemailer.createTransport.mock.calls[0][0]; + expect(callArgs.port).toBe(465); + expect(typeof callArgs.port).toBe('number'); }); - const res = makeRes(); - await capturedHandler(req, res); - expect(mockNodemailer.createTransport).toHaveBeenCalledWith( - expect.objectContaining({ - host: 'smtp.example.com', - secure: true - }) - ); }); }); }); diff --git a/functions/auth/templates/signin.html b/functions/auth/templates/signin.html index e1773044..5df00a02 100644 --- a/functions/auth/templates/signin.html +++ b/functions/auth/templates/signin.html @@ -18,26 +18,23 @@

Flightbox

Hallo!

-

Klicken Sie auf die Schaltfläche unten, um sich sicher bei Ihrem Flightbox-Konto anzumelden:

+

Geben Sie den folgenden Code in der Flightbox-App ein, um sich anzumelden:

- +
- - Bei Flightbox anmelden - +
+

Ihr Anmelde-Code

+

{{signInCode}}

+
-

Falls die Schaltfläche nicht funktioniert, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

-

{{signInLink}}

-
-

🔒 Sicherheitshinweis

+

Sicherheitshinweis

- • Dieser Link läuft in 1 Stunde aus Sicherheitsgründen ab
- • Falls Sie diesen Anmelde-Link nicht angefordert haben, können Sie diese E-Mail ignorieren
- • Teilen Sie diesen Link niemals mit anderen + • Dieser Code ist 10 Minuten gültig
+ • Falls Sie diesen Code nicht angefordert haben, können Sie diese E-Mail ignorieren
+ • Teilen Sie diesen Code niemals mit anderen

diff --git a/functions/auth/templates/signin.txt b/functions/auth/templates/signin.txt index 4112a2f3..9b1f4455 100644 --- a/functions/auth/templates/signin.txt +++ b/functions/auth/templates/signin.txt @@ -1,13 +1,13 @@ Hallo! -Willkommen bei Flightbox! Klicken Sie auf den folgenden Link, um sich sicher bei Ihrem Konto anzumelden: +Geben Sie den folgenden Code in der Flightbox-App ein, um sich anzumelden: -{{signInLink}} + {{signInCode}} SICHERHEITSHINWEIS: -• Dieser Link läuft in 1 Stunde aus Sicherheitsgründen ab -• Falls Sie diesen Anmelde-Link nicht angefordert haben, können Sie diese E-Mail ignorieren -• Teilen Sie diesen Link niemals mit anderen +- Dieser Code ist 10 Minuten gültig +- Falls Sie diesen Code nicht angefordert haben, können Sie diese E-Mail ignorieren +- Teilen Sie diesen Code niemals mit anderen --- Diese E-Mail wurde von der open digital Flightbox gesendet diff --git a/functions/auth/verifySignInCode.js b/functions/auth/verifySignInCode.js new file mode 100644 index 00000000..40f36a0b --- /dev/null +++ b/functions/auth/verifySignInCode.js @@ -0,0 +1,102 @@ +'use strict'; + +const functions = require('firebase-functions'); +const admin = require('firebase-admin'); +const crypto = require('crypto'); +const cors = require('cors')({origin: true}); + +const MAX_ATTEMPTS = 5; + +const hashCode = (code) => { + return crypto.createHash('sha256').update(code).digest('hex'); +}; + +const validateRequest = (method, body) => { + if (method !== 'POST') { + return { error: 'Method not allowed', status: 405 }; + } + + const { email, code } = body; + + if (!email || !code) { + return { error: 'Email and code are required', status: 400 }; + } + + return null; +}; + +exports.verifySignInCode = functions.region('europe-west1').https.onRequest((req, res) => { + return cors(req, res, async () => { + try { + const validationError = validateRequest(req.method, req.body); + if (validationError) { + return res.status(validationError.status).json({ error: validationError.error }); + } + + const { email, code } = req.body; + const normalizedEmail = email.toLowerCase(); + const codeHash = hashCode(code); + const now = Date.now(); + + const db = admin.database(); + const codesRef = db.ref('/signInCodes'); + + const snapshot = await codesRef + .orderByChild('email') + .equalTo(normalizedEmail) + .once('value'); + + if (!snapshot.exists()) { + return res.status(400).json({ error: 'Invalid or expired code' }); + } + + let validKey = null; + const attemptsUpdates = {}; + + snapshot.forEach(child => { + const data = child.val(); + if (data.expiry <= now || data.attempts >= MAX_ATTEMPTS) { + return; + } + if (validKey === null && data.codeHash === codeHash) { + validKey = child.key; + } else { + attemptsUpdates[`${child.key}/attempts`] = (data.attempts || 0) + 1; + } + }); + + if (Object.keys(attemptsUpdates).length > 0) { + await codesRef.update(attemptsUpdates); + } + + if (!validKey) { + return res.status(400).json({ error: 'Invalid or expired code' }); + } + + // Consume the code (delete it so it can only be used once) + await codesRef.child(validKey).remove(); + + // Get or create the Firebase Auth user + let uid; + try { + const userRecord = await admin.auth().getUserByEmail(normalizedEmail); + uid = userRecord.uid; + } catch (e) { + if (e.code === 'auth/user-not-found') { + const newUser = await admin.auth().createUser({ email: normalizedEmail }); + uid = newUser.uid; + } else { + throw e; + } + } + + const customToken = await admin.auth().createCustomToken(uid, { email: normalizedEmail }); + res.status(200).json({ token: customToken }); + } catch (error) { + console.error('Error verifying sign-in code:', error); + res.status(500).json({ + error: 'Failed to verify sign-in code', + }); + } + }); +}); diff --git a/functions/auth/verifySignInCode.spec.js b/functions/auth/verifySignInCode.spec.js new file mode 100644 index 00000000..6f6814a1 --- /dev/null +++ b/functions/auth/verifySignInCode.spec.js @@ -0,0 +1,255 @@ +const crypto = require('crypto'); + +const hashCode = (code) => crypto.createHash('sha256').update(code).digest('hex'); + +describe('functions', () => { + describe('auth/verifySignInCode', () => { + let mockAdmin; + let mockFunctions; + let capturedHandler; + let mockCors; + let mockCodesRef; + let mockAuthAdmin; + + const now = Date.now(); + const futureExpiry = now + 10 * 60 * 1000; + + beforeEach(() => { + jest.resetModules(); + capturedHandler = null; + + mockCors = jest.fn().mockImplementation((req, res, cb) => cb()); + + mockFunctions = { + https: { + onRequest: jest.fn().mockImplementation(handler => { capturedHandler = handler; }) + } + }; + mockFunctions.region = jest.fn(() => mockFunctions); + + mockCodesRef = { + orderByChild: jest.fn().mockReturnThis(), + equalTo: jest.fn().mockReturnThis(), + once: jest.fn(), + update: jest.fn().mockResolvedValue(undefined), + child: jest.fn().mockReturnThis(), + remove: jest.fn().mockResolvedValue(undefined), + }; + + mockAuthAdmin = { + getUserByEmail: jest.fn(), + createUser: jest.fn(), + createCustomToken: jest.fn().mockResolvedValue('custom-token-xyz'), + }; + + mockAdmin = { + database: jest.fn().mockReturnValue({ + ref: jest.fn().mockReturnValue(mockCodesRef) + }), + auth: jest.fn().mockReturnValue(mockAuthAdmin), + }; + + jest.mock('firebase-admin', () => mockAdmin); + jest.mock('firebase-functions', () => mockFunctions); + jest.mock('cors', () => () => mockCors); + + require('./verifySignInCode'); + }); + + const makeReq = (method, body) => ({ method, body }); + const makeRes = () => ({ + status: jest.fn().mockReturnThis(), + json: jest.fn() + }); + + const makeSnapshot = (entries) => { + const exists = entries.length > 0; + return { + exists: () => exists, + forEach: (cb) => entries.forEach(({ key, val }) => cb({ key, val: () => val })), + }; + }; + + it('returns 405 for GET request', async () => { + const req = makeReq('GET', {}); + const res = makeRes(); + await capturedHandler(req, res); + expect(res.status).toHaveBeenCalledWith(405); + }); + + it('returns 400 when email is missing', async () => { + const req = makeReq('POST', { code: '123456' }); + const res = makeRes(); + await capturedHandler(req, res); + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ error: 'Email and code are required' }); + }); + + it('returns 400 when code is missing', async () => { + const req = makeReq('POST', { email: 'user@example.com' }); + const res = makeRes(); + await capturedHandler(req, res); + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ error: 'Email and code are required' }); + }); + + it('returns 400 when no codes exist for email', async () => { + mockCodesRef.once.mockResolvedValue(makeSnapshot([])); + + const req = makeReq('POST', { email: 'user@example.com', code: '123456' }); + const res = makeRes(); + await capturedHandler(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ error: 'Invalid or expired code' }); + }); + + it('returns 400 for wrong code and increments attempts', async () => { + const correctCode = '111111'; + const snapshot = makeSnapshot([ + { key: 'k1', val: { email: 'user@example.com', codeHash: hashCode(correctCode), expiry: futureExpiry, attempts: 0 } }, + ]); + mockCodesRef.once.mockResolvedValue(snapshot); + + const req = makeReq('POST', { email: 'user@example.com', code: '999999' }); + const res = makeRes(); + await capturedHandler(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ error: 'Invalid or expired code' }); + expect(mockCodesRef.update).toHaveBeenCalledWith({ 'k1/attempts': 1 }); + }); + + it('returns 400 for expired code without incrementing attempts', async () => { + const code = '123456'; + const snapshot = makeSnapshot([ + { key: 'k1', val: { email: 'user@example.com', codeHash: hashCode(code), expiry: now - 1000, attempts: 0 } }, + ]); + mockCodesRef.once.mockResolvedValue(snapshot); + + const req = makeReq('POST', { email: 'user@example.com', code }); + const res = makeRes(); + await capturedHandler(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(mockCodesRef.update).not.toHaveBeenCalled(); + }); + + it('returns 400 for exhausted code (max attempts reached)', async () => { + const code = '123456'; + const snapshot = makeSnapshot([ + { key: 'k1', val: { email: 'user@example.com', codeHash: hashCode(code), expiry: futureExpiry, attempts: 5 } }, + ]); + mockCodesRef.once.mockResolvedValue(snapshot); + + const req = makeReq('POST', { email: 'user@example.com', code }); + const res = makeRes(); + await capturedHandler(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(mockCodesRef.update).not.toHaveBeenCalled(); + }); + + it('returns 200 with token on valid code and deletes used code', async () => { + const code = '123456'; + const snapshot = makeSnapshot([ + { key: 'k1', val: { email: 'user@example.com', codeHash: hashCode(code), expiry: futureExpiry, attempts: 0 } }, + ]); + mockCodesRef.once.mockResolvedValue(snapshot); + mockAuthAdmin.getUserByEmail.mockResolvedValue({ uid: 'user-uid-123' }); + + const req = makeReq('POST', { email: 'user@example.com', code }); + const res = makeRes(); + await capturedHandler(req, res); + + expect(mockCodesRef.child).toHaveBeenCalledWith('k1'); + expect(mockCodesRef.remove).toHaveBeenCalled(); + expect(mockAuthAdmin.createCustomToken).toHaveBeenCalledWith('user-uid-123', { email: 'user@example.com' }); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ token: 'custom-token-xyz' }); + }); + + it('creates new Firebase user if not found', async () => { + const code = '123456'; + const snapshot = makeSnapshot([ + { key: 'k1', val: { email: 'new@example.com', codeHash: hashCode(code), expiry: futureExpiry, attempts: 0 } }, + ]); + mockCodesRef.once.mockResolvedValue(snapshot); + + const authError = Object.assign(new Error('User not found'), { code: 'auth/user-not-found' }); + mockAuthAdmin.getUserByEmail.mockRejectedValue(authError); + mockAuthAdmin.createUser.mockResolvedValue({ uid: 'new-uid-456' }); + + const req = makeReq('POST', { email: 'new@example.com', code }); + const res = makeRes(); + await capturedHandler(req, res); + + expect(mockAuthAdmin.createUser).toHaveBeenCalledWith({ email: 'new@example.com' }); + expect(mockAuthAdmin.createCustomToken).toHaveBeenCalledWith('new-uid-456', { email: 'new@example.com' }); + expect(res.status).toHaveBeenCalledWith(200); + }); + + it('normalizes email to lowercase', async () => { + const code = '123456'; + const snapshot = makeSnapshot([ + { key: 'k1', val: { email: 'user@example.com', codeHash: hashCode(code), expiry: futureExpiry, attempts: 0 } }, + ]); + mockCodesRef.once.mockResolvedValue(snapshot); + mockAuthAdmin.getUserByEmail.mockResolvedValue({ uid: 'uid-123' }); + + const req = makeReq('POST', { email: 'USER@EXAMPLE.COM', code }); + const res = makeRes(); + await capturedHandler(req, res); + + expect(mockAuthAdmin.getUserByEmail).toHaveBeenCalledWith('user@example.com'); + }); + + it('increments attempts for non-matching valid codes when correct code is found', async () => { + const correctCode = '111111'; + const otherCode = '222222'; + const snapshot = makeSnapshot([ + { key: 'k1', val: { email: 'user@example.com', codeHash: hashCode(correctCode), expiry: futureExpiry, attempts: 0 } }, + { key: 'k2', val: { email: 'user@example.com', codeHash: hashCode(otherCode), expiry: futureExpiry, attempts: 1 } }, + ]); + mockCodesRef.once.mockResolvedValue(snapshot); + mockAuthAdmin.getUserByEmail.mockResolvedValue({ uid: 'uid-123' }); + + const req = makeReq('POST', { email: 'user@example.com', code: correctCode }); + const res = makeRes(); + await capturedHandler(req, res); + + expect(res.status).toHaveBeenCalledWith(200); + expect(mockCodesRef.update).toHaveBeenCalledWith({ 'k2/attempts': 2 }); + }); + + it('returns 500 without error details on unexpected error', async () => { + mockCodesRef.once.mockRejectedValue(new Error('internal db secret error')); + + const req = makeReq('POST', { email: 'user@example.com', code: '123456' }); + const res = makeRes(); + await capturedHandler(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + const responseData = res.json.mock.calls[0][0]; + expect(responseData).toEqual({ error: 'Failed to verify sign-in code' }); + expect(responseData).not.toHaveProperty('details'); + }); + + it('rethrows non-user-not-found auth errors', async () => { + const code = '123456'; + const snapshot = makeSnapshot([ + { key: 'k1', val: { email: 'user@example.com', codeHash: hashCode(code), expiry: futureExpiry, attempts: 0 } }, + ]); + mockCodesRef.once.mockResolvedValue(snapshot); + + const authError = Object.assign(new Error('Auth service unavailable'), { code: 'auth/internal-error' }); + mockAuthAdmin.getUserByEmail.mockRejectedValue(authError); + + const req = makeReq('POST', { email: 'user@example.com', code }); + const res = makeRes(); + await capturedHandler(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + }); + }); +}); diff --git a/functions/cleanupMessages.js b/functions/cleanupMessages.js new file mode 100644 index 00000000..2fab43ba --- /dev/null +++ b/functions/cleanupMessages.js @@ -0,0 +1,50 @@ +'use strict'; + +const functions = require('firebase-functions'); +const admin = require('firebase-admin'); + +const SCHEDULE = '0 2 * * *'; // Every day at 2 AM +const TIMEZONE = 'Europe/Zurich'; + +exports.scheduledCleanupMessages = functions + .region('europe-west1') + .pubsub + .schedule(SCHEDULE) + .timeZone(TIMEZONE) + .onRun(async () => { + const db = admin.database(); + + const retentionSnap = await db.ref('/settings/messageRetentionDays').once('value'); + const retentionDays = retentionSnap.val(); + + if (!retentionDays) { + console.log('No messageRetentionDays configured, skipping'); + return; + } + + const cutoff = Date.now() - (retentionDays * 24 * 60 * 60 * 1000); + console.log(`Deleting messages older than ${new Date(cutoff).toISOString()} (retention: ${retentionDays} days)`); + + const snapshot = await db.ref('/messages').once('value'); + + if (!snapshot.exists()) { + console.log('No messages to delete'); + return; + } + + const updates = {}; + + snapshot.forEach(child => { + const val = child.val(); + if (val.timestamp <= cutoff) { + updates[child.key] = null; + } + }); + + const count = Object.keys(updates).length; + + if (count > 0) { + await db.ref('/messages').update(updates); + console.log(`Deleted ${count} messages`); + } + }); diff --git a/functions/cleanupMessages.spec.js b/functions/cleanupMessages.spec.js new file mode 100644 index 00000000..81dff1da --- /dev/null +++ b/functions/cleanupMessages.spec.js @@ -0,0 +1,109 @@ +'use strict'; + +let mockRefData = {}; +let mockCurrentRefPath = ''; + +const mockUpdate = jest.fn().mockResolvedValue(); +const mockOnce = jest.fn().mockImplementation(() => { + return Promise.resolve(mockRefData[mockCurrentRefPath] || { exists: () => false, forEach: () => {} }); +}); +const mockRef = jest.fn().mockImplementation(path => { + mockCurrentRefPath = path; + return { once: mockOnce, update: mockUpdate }; +}); + +jest.mock('firebase-admin', () => ({ + database: jest.fn().mockReturnValue({ ref: mockRef }), + initializeApp: jest.fn(), +})); + +jest.mock('firebase-functions', () => ({ + region: jest.fn().mockReturnThis(), + pubsub: { + schedule: jest.fn().mockReturnValue({ + timeZone: jest.fn().mockReturnValue({ + onRun: jest.fn(fn => fn), + }), + }), + }, +})); + +function createSnapshot(data) { + const entries = Object.entries(data); + return { + exists: () => entries.length > 0, + forEach(cb) { + entries.forEach(([key, val]) => { + cb({ key, val: () => val }); + }); + }, + }; +} + +function createValueSnapshot(val) { + return { val: () => val }; +} + +describe('cleanupMessages', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + mockRefData = {}; + mockCurrentRefPath = ''; + }); + + it('should skip when messageRetentionDays is not set', async () => { + mockRefData['/settings/messageRetentionDays'] = createValueSnapshot(null); + + const { scheduledCleanupMessages } = require('./cleanupMessages'); + await scheduledCleanupMessages(); + + expect(mockRef).not.toHaveBeenCalledWith('/messages'); + expect(mockUpdate).not.toHaveBeenCalled(); + }); + + it('should delete messages older than configured retention', async () => { + mockRefData['/settings/messageRetentionDays'] = createValueSnapshot(90); + + const oldTimestamp = Date.now() - (91 * 24 * 60 * 60 * 1000); + const recentTimestamp = Date.now() - (10 * 24 * 60 * 60 * 1000); + + mockRefData['/messages'] = createSnapshot({ + 'msg1': { name: 'Old', timestamp: oldTimestamp }, + 'msg2': { name: 'Recent', timestamp: recentTimestamp }, + }); + + const { scheduledCleanupMessages } = require('./cleanupMessages'); + await scheduledCleanupMessages(); + + expect(mockUpdate).toHaveBeenCalled(); + + const updates = mockUpdate.mock.calls[0][0]; + expect(updates['msg1']).toBeNull(); + expect(updates['msg2']).toBeUndefined(); + }); + + it('should not call update when no messages exist', async () => { + mockRefData['/settings/messageRetentionDays'] = createValueSnapshot(90); + mockRefData['/messages'] = { exists: () => false, forEach: () => {} }; + + const { scheduledCleanupMessages } = require('./cleanupMessages'); + await scheduledCleanupMessages(); + + expect(mockUpdate).not.toHaveBeenCalled(); + }); + + it('should not call update when all messages are recent', async () => { + mockRefData['/settings/messageRetentionDays'] = createValueSnapshot(90); + + const recentTimestamp = Date.now() - (10 * 24 * 60 * 60 * 1000); + mockRefData['/messages'] = createSnapshot({ + 'msg1': { name: 'Recent', timestamp: recentTimestamp }, + }); + + const { scheduledCleanupMessages } = require('./cleanupMessages'); + await scheduledCleanupMessages(); + + expect(mockUpdate).not.toHaveBeenCalled(); + }); +}); diff --git a/functions/enrichMovements.js b/functions/enrichMovements.js index f6490304..dbb9f5d2 100644 --- a/functions/enrichMovements.js +++ b/functions/enrichMovements.js @@ -99,6 +99,10 @@ const handleUpdate = (change, movementType) => { if (!change.before.exists() || !change.after.exists()) { return null; } + // Skip anonymized movements (PII has been stripped by the retention job) + if (change.after.val().anonymized) { + return null; + } return enrichOnUpdate(change, movementType); }; diff --git a/functions/index.js b/functions/index.js index bed98513..4330f692 100644 --- a/functions/index.js +++ b/functions/index.js @@ -18,16 +18,24 @@ const enrichMovements = require('./enrichMovements'); const auth = require('./auth'); const { generateSignInLink } = require('./auth/generateSignInLink'); -const { sendSignInEmail } = require('./auth/sendSignInEmail'); +const { generateSignInCode } = require('./auth/generateSignInCode'); +const { verifySignInCode } = require('./auth/verifySignInCode'); +const { cleanupExpiredSignInCodes } = require('./auth/cleanupExpiredSignInCodes'); +const { createTestEmailToken } = require('./auth/createTestEmailToken'); const api = require('./api'); const webhook = require('./webhook'); const associatedMovementsTriggers = require('./associatedMovements/setAssociatedMovementsTriggers'); const invoiceRecipientsTrigger = require('./invoiceRecipients/invoiceRecipientsTrigger'); const updateArrivalPaymentStatus = require('./updateArrivalPaymentStatus'); +const { scheduledAnonymizeMovements } = require('./anonymizeMovements'); +const { scheduledCleanupMessages } = require('./cleanupMessages'); exports.auth = auth; exports.generateSignInLink = generateSignInLink; -exports.sendSignInEmail = sendSignInEmail; +exports.generateSignInCode = generateSignInCode; +exports.verifySignInCode = verifySignInCode; +exports.cleanupExpiredSignInCodes = cleanupExpiredSignInCodes; +exports.createTestEmailToken = createTestEmailToken; exports.api = api; exports.webhook = webhook; exports.setAssociatedMovementOnCreatedDeparture = associatedMovementsTriggers.setAssociatedMovementOnCreatedDeparture; @@ -47,3 +55,6 @@ exports.enrichArrivalOnCreate = enrichMovements.enrichArrivalOnCreate; exports.enrichArrivalOnUpdate = enrichMovements.enrichArrivalOnUpdate; exports.updateArrivalPaymentStatusOnCardPaymentUpdate = updateArrivalPaymentStatus.updateArrivalPaymentStatusOnCardPaymentUpdate; + +exports.scheduledAnonymizeMovements = scheduledAnonymizeMovements; +exports.scheduledCleanupMessages = scheduledCleanupMessages; diff --git a/functions/invoiceRecipients/invoiceRecipientsTrigger.spec.js b/functions/invoiceRecipients/invoiceRecipientsTrigger.spec.js new file mode 100644 index 00000000..bf323843 --- /dev/null +++ b/functions/invoiceRecipients/invoiceRecipientsTrigger.spec.js @@ -0,0 +1,169 @@ +'use strict'; + +let capturedHandler; + +jest.mock('firebase-functions', () => { + const mock = { + config: jest.fn(() => ({ rtdb: { instance: 'test-instance' } })), + database: { + instance: jest.fn(() => ({ + ref: jest.fn(() => ({ + onWrite: jest.fn(handler => { capturedHandler = handler; }), + })), + })), + }, + }; + mock.region = jest.fn(() => mock); + return mock; +}); + +const mockAdminDbRef = jest.fn(); +jest.mock('firebase-admin', () => ({ + database: jest.fn(() => ({ ref: mockAdminDbRef })), +})); + +global.fetch = jest.fn(); + +describe('functions/invoiceRecipients/invoiceRecipientsTrigger', () => { + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + capturedHandler = null; + + jest.mock('firebase-functions', () => { + const mock = { + config: jest.fn(() => ({ rtdb: { instance: 'test-instance' } })), + database: { + instance: jest.fn(() => ({ + ref: jest.fn(() => ({ + onWrite: jest.fn(handler => { capturedHandler = handler; }), + })), + })), + }, + }; + mock.region = jest.fn(() => mock); + return mock; + }); + + jest.mock('firebase-admin', () => ({ + database: jest.fn(() => ({ ref: mockAdminDbRef })), + })); + + require('./invoiceRecipientsTrigger'); + }); + + const makeChange = (before, after) => ({ + before: { val: () => before }, + after: { val: () => after }, + }); + + it('does nothing when before and after are equal', async () => { + const change = makeChange([{ name: 'A', emails: [] }], [{ name: 'A', emails: [] }]); + await capturedHandler(change, {}); + expect(mockAdminDbRef).not.toHaveBeenCalled(); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it('does nothing when no customs declaration settings exist', async () => { + mockAdminDbRef.mockReturnValue({ + once: jest.fn().mockResolvedValue({ val: () => null }), + }); + const change = makeChange([{ name: 'A' }], [{ name: 'B' }]); + await capturedHandler(change, {}); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it('does nothing when customs settings have no baseUrl', async () => { + mockAdminDbRef.mockReturnValue({ + once: jest.fn().mockResolvedValue({ val: () => ({ aerodrome: 'LSZT' }) }), + }); + const change = makeChange([{ name: 'A' }], [{ name: 'B' }]); + await capturedHandler(change, {}); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it('sends PUT request with recipient data when settings are present', async () => { + mockAdminDbRef.mockReturnValue({ + once: jest.fn().mockResolvedValue({ + val: () => ({ + baseUrl: 'https://customs.example.com', + aerodrome: 'LSZT', + accessToken: 'tok123', + }), + }), + }); + + global.fetch.mockResolvedValue({ ok: true }); + + const after = [ + { name: 'Alice', emails: ['alice@example.com'] }, + { name: 'Bob', emails: ['bob@example.com', 'bob2@example.com'] }, + ]; + const change = makeChange([{ name: 'Old' }], after); + await capturedHandler(change, {}); + + expect(global.fetch).toHaveBeenCalledWith( + 'https://customs.example.com/api/invoice-recipients?ad=LSZT', + expect.objectContaining({ + method: 'PUT', + headers: expect.objectContaining({ + Authorization: 'Bearer tok123', + 'Content-Type': 'application/json', + }), + body: JSON.stringify([ + { name: 'Alice', emails: ['alice@example.com'] }, + { name: 'Bob', emails: ['bob@example.com', 'bob2@example.com'] }, + ]), + }) + ); + }); + + it('handles null after value by sending empty array', async () => { + mockAdminDbRef.mockReturnValue({ + once: jest.fn().mockResolvedValue({ + val: () => ({ + baseUrl: 'https://customs.example.com', + aerodrome: 'LSZT', + accessToken: 'tok123', + }), + }), + }); + + global.fetch.mockResolvedValue({ ok: true }); + + const change = makeChange([{ name: 'Old' }], null); + await capturedHandler(change, {}); + + expect(global.fetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ body: JSON.stringify([]) }) + ); + }); + + it('logs error when fetch response is not ok', async () => { + mockAdminDbRef.mockReturnValue({ + once: jest.fn().mockResolvedValue({ + val: () => ({ + baseUrl: 'https://customs.example.com', + aerodrome: 'LSZT', + accessToken: 'tok123', + }), + }), + }); + + global.fetch.mockResolvedValue({ + ok: false, + json: jest.fn().mockResolvedValue({ error: 'Not authorized' }), + }); + + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + const change = makeChange([{ name: 'A' }], [{ name: 'B' }]); + await capturedHandler(change, {}); + + expect(consoleSpy).toHaveBeenCalledWith( + 'Failed to update the invoice recipients of the customs app', + expect.anything() + ); + consoleSpy.mockRestore(); + }); +}); diff --git a/gulpfile.js b/gulpfile.js index 7081a94c..f77cf566 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -67,13 +67,68 @@ function copyFavicons(done) { return done(); } - return gulp.src(path.join(faviconDir, '*'), { + return gulp.src([ + path.join(faviconDir, '*'), + '!' + path.join(faviconDir, 'manifest.json'), + '!' + path.join(faviconDir, 'site.webmanifest'), + ], { base: path.join(__dirname, 'theme', projectConf.theme), - allowEmpty: true + allowEmpty: true, + encoding: false }) .pipe(gulp.dest(config.output.path)); } +function generateManifest(done) { + const config = require('./webpack.config.js'); + const projectName = process.env.npm_config_project || 'lszt'; + const projectConf = projects.load(projectName); + + const themeColor = projectConf.themeColor || '#ffffff'; + const shortName = projectConf.shortName || projectConf.title; + const themeName = projectConf.theme; + + const faviconDir = path.join(__dirname, 'theme', themeName, 'favicons'); + const manifestPath = path.join(faviconDir, 'manifest.json'); + + let icons; + if (fs.existsSync(manifestPath)) { + const existing = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + icons = existing.icons || []; + } else { + const pngFiles = fs.existsSync(faviconDir) + ? fs.readdirSync(faviconDir).filter(f => f.startsWith('android-chrome') && f.endsWith('.png')) + : []; + icons = pngFiles.map(f => { + const match = f.match(/(\d+)x(\d+)/); + const size = match ? `${match[1]}x${match[2]}` : '192x192'; + return { src: `/favicons/${f}`, sizes: size, type: 'image/png' }; + }); + } + + const manifest = { + name: projectConf.title, + short_name: shortName, + icons, + theme_color: themeColor, + background_color: '#ffffff', + display: 'standalone', + start_url: '/', + scope: '/', + orientation: 'any', + }; + + const outDir = path.join(config.output.path, 'favicons'); + if (!fs.existsSync(outDir)) { + fs.mkdirSync(outDir, { recursive: true }); + } + fs.writeFileSync( + path.join(outDir, 'manifest.json'), + JSON.stringify(manifest, null, 2) + ); + done(); +} + function buildFirebaseRules() { const config = require('./webpack.config.js'); const projectName = process.env.npm_config_project || 'lszt'; @@ -86,7 +141,7 @@ function buildFirebaseRules() { .pipe(gulp.dest(config.output.path)); } -const assets = gulp.parallel(copyResetCss, copyFavicons, buildFirebaseRules); +const assets = gulp.parallel(copyResetCss, gulp.series(copyFavicons, generateManifest), buildFirebaseRules); exports.clean = clean; exports.build = gulp.series(clean, bundleJS, assets); diff --git a/index.html b/index.html index 64b8c0e5..d023d828 100644 --- a/index.html +++ b/index.html @@ -16,12 +16,14 @@ - + - - + + + +
diff --git a/package-lock.json b/package-lock.json index c7942521..a34ff162 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,7 +76,8 @@ "webpack": "^5.105.3", "webpack-cli": "^6.0.1", "webpack-dev-server": "^5.2.2", - "webpack-stream": "^7.0.0" + "webpack-stream": "^7.0.0", + "workbox-webpack-plugin": "^7.3.0" } }, "node_modules/@adobe/css-tools": { @@ -86,6 +87,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@apideck/better-ajv-errors": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", + "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-schema": "^0.4.0", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", @@ -4255,6 +4274,160 @@ "dev": true, "license": "MIT" }, + "node_modules/@rollup/plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", + "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.10.4", + "@rollup/pluginutils": "^3.1.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve/node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-node-resolve/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/pluginutils/node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true, + "license": "MIT" + }, "node_modules/@sentry-internal/browser-utils": { "version": "10.41.0", "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.41.0.tgz", @@ -4400,6 +4573,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@surma/rollup-plugin-off-main-thread": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", + "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "ejs": "^3.1.6", + "json5": "^2.2.0", + "magic-string": "^0.25.0", + "string.prototype.matchall": "^4.0.6" + } + }, "node_modules/@swc/helpers": { "version": "0.5.18", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", @@ -4930,6 +5116,13 @@ "@types/react-router": "*" } }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/retry": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", @@ -5025,6 +5218,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -5827,6 +6027,23 @@ "node": ">=0.10.0" } }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-differ": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", @@ -5871,6 +6088,28 @@ "node": ">=0.10.0" } }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -5925,6 +6164,13 @@ "node": ">=8" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, "node_modules/async-done": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/async-done/-/async-done-2.0.0.tgz", @@ -5940,6 +6186,16 @@ "node": ">= 10.13.0" } }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/async-settle": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-2.0.0.tgz", @@ -7304,6 +7560,16 @@ "dev": true, "license": "MIT" }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", @@ -7537,6 +7803,60 @@ "node": ">=18" } }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/dateformat": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", @@ -7659,6 +7979,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/del": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/del/-/del-8.0.1.tgz", @@ -7920,6 +8258,22 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.267", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", @@ -8035,6 +8389,75 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -8086,15 +8509,33 @@ "node": ">= 0.4" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, "engines": { - "node": ">=6" - } - }, + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -8167,6 +8608,13 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true, + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -8266,19 +8714,6 @@ "node": ">=8.12.0" } }, - "node_modules/execa/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/executable": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", @@ -8611,6 +9046,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -9031,6 +9499,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -9082,6 +9581,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true, + "license": "ISC" + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -9121,6 +9627,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -9292,6 +9816,23 @@ "node": ">=0.10.0" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/globby": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", @@ -9674,6 +10215,19 @@ "node": ">=0.10.0" } }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -9707,6 +10261,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -9751,19 +10321,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/hasha/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/hasha/node_modules/type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", @@ -10233,6 +10790,21 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/interpret": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", @@ -10291,12 +10863,66 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -10309,6 +10935,23 @@ "node": ">=8" } }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -10336,6 +10979,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -10384,6 +11062,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -10470,17 +11164,50 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-negated-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", - "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-network-error": { + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-network-error": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", @@ -10501,6 +11228,33 @@ "node": ">=0.12.0" } }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-path-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-3.0.0.tgz", @@ -10559,6 +11313,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-relative": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", @@ -10572,6 +11336,83 @@ "node": ">=0.10.0" } }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-typed-array": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", @@ -10630,6 +11471,52 @@ "node": ">=0.10.0" } }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -10655,6 +11542,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -10806,6 +11700,24 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", @@ -10885,19 +11797,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-changed-files/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/jest-changed-files/node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -11979,6 +12878,16 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/jsprim": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", @@ -12266,6 +13175,13 @@ "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", "dev": true }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.template": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", @@ -12428,6 +13344,16 @@ "lz-string": "bin/bin.js" } }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, "node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -12861,6 +13787,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object.defaults": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", @@ -12967,6 +13924,24 @@ "dev": true, "license": "MIT" }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -14040,6 +15015,29 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -14064,6 +15062,27 @@ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "dev": true }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regexpu-core": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", @@ -14424,8 +15443,24 @@ "dev": true, "license": "MIT" }, - "node_modules/rrweb-cssom": { - "version": "0.8.0", + "node_modules/rollup": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", + "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", "dev": true, @@ -14477,12 +15512,49 @@ "tslib": "^2.1.0" } }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-regex-test": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", @@ -14766,6 +15838,37 @@ "node": ">= 0.4" } }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -14936,6 +16039,16 @@ "node": ">=8" } }, + "node_modules/smob": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.1.tgz", + "integrity": "sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -14947,6 +16060,13 @@ "websocket-driver": "^0.7.4" } }, + "node_modules/source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true, + "license": "MIT" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -14975,6 +16095,14 @@ "source-map": "^0.6.0" } }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true, + "license": "MIT" + }, "node_modules/sparkles": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", @@ -15156,18 +16284,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/start-server-and-test/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -15177,6 +16293,20 @@ "node": ">= 0.8" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/stream-browserify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", @@ -15293,6 +16423,108 @@ "node": ">=8" } }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -15329,6 +16561,16 @@ "node": ">=8" } }, + "node_modules/strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -15557,6 +16799,48 @@ "streamx": "^2.12.5" } }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/terser": { "version": "5.44.1", "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", @@ -15935,6 +17219,84 @@ "node": ">= 0.6" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -15976,6 +17338,25 @@ "typescript-compare": "^0.0.2" } }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", @@ -16103,6 +17484,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -16167,6 +17561,17 @@ "node": ">=8" } }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "yarn": "*" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -16898,149 +18303,614 @@ "node": ">= 0.10" } }, - "node_modules/webpack-stream/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/webpack-stream/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/webpack-stream/node_modules/vinyl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "dev": true, + "dependencies": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/workbox-background-sync": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.0.tgz", + "integrity": "sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-broadcast-update": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.0.tgz", + "integrity": "sha512-+eZQwoktlvo62cI0b+QBr40v5XjighxPq3Fzo9AWMiAosmpG5gxRHgTbGGhaJv/q/MFVxwFNGh/UwHZ/8K88lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-build": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.0.tgz", + "integrity": "sha512-Ntk1pWb0caOFIvwz/hfgrov/OJ45wPEhI5PbTywQcYjyZiVhT3UrwwUPl6TRYbTm4moaFYithYnl1lvZ8UjxcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.24.4", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^5.2.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-replace": "^2.4.1", + "@rollup/plugin-terser": "^0.4.3", + "@surma/rollup-plugin-off-main-thread": "^2.2.3", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^11.0.1", + "lodash": "^4.17.20", + "pretty-bytes": "^5.3.0", + "rollup": "^2.79.2", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "7.4.0", + "workbox-broadcast-update": "7.4.0", + "workbox-cacheable-response": "7.4.0", + "workbox-core": "7.4.0", + "workbox-expiration": "7.4.0", + "workbox-google-analytics": "7.4.0", + "workbox-navigation-preload": "7.4.0", + "workbox-precaching": "7.4.0", + "workbox-range-requests": "7.4.0", + "workbox-recipes": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0", + "workbox-streams": "7.4.0", + "workbox-sw": "7.4.0", + "workbox-window": "7.4.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/workbox-build/node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/workbox-build/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/workbox-build/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/workbox-build/node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "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": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/workbox-build/node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/workbox-build/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/workbox-build/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/workbox-build/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/workbox-build/node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workbox-build/node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/workbox-build/node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/workbox-build/node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/workbox-cacheable-response": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.0.tgz", + "integrity": "sha512-0Fb8795zg/x23ISFkAc7lbWes6vbw34DGFIMw31cwuHPgDEC/5EYm6m/ZkylLX0EnEbbOyOCLjKgFS/Z5g0HeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-core": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.0.tgz", + "integrity": "sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-expiration": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.0.tgz", + "integrity": "sha512-V50p4BxYhtA80eOvulu8xVfPBgZbkxJ1Jr8UUn0rvqjGhLDqKNtfrDfjJKnLz2U8fO2xGQJTx/SKXNTzHOjnHw==", "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "idb": "^7.0.1", + "workbox-core": "7.4.0" } }, - "node_modules/webpack-stream/node_modules/vinyl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "node_modules/workbox-google-analytics": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.0.tgz", + "integrity": "sha512-MVPXQslRF6YHkzGoFw1A4GIB8GrKym/A5+jYDUSL+AeJw4ytQGrozYdiZqUW1TPQHW8isBCBtyFJergUXyNoWQ==", "dev": true, + "license": "MIT", "dependencies": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" + "workbox-background-sync": "7.4.0", + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" } }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "node_modules/workbox-navigation-preload": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.0.tgz", + "integrity": "sha512-etzftSgdQfjMcfPgbfaZCfM2QuR1P+4o8uCA2s4rf3chtKTq/Om7g/qvEOcZkG6v7JZOSOxVYQiOu6PbAZgU6w==", "dev": true, + "license": "MIT", "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" + "workbox-core": "7.4.0" } }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "node_modules/workbox-precaching": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.0.tgz", + "integrity": "sha512-VQs37T6jDqf1rTxUJZXRl3yjZMf5JX/vDPhmx2CPgDDKXATzEoqyRqhYnRoxl6Kr0rqaQlp32i9rtG5zTzIlNg==", "dev": true, - "engines": { - "node": ">=0.8.0" + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" } }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "node_modules/workbox-range-requests": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.0.tgz", + "integrity": "sha512-3Vq854ZNuP6Y0KZOQWLaLC9FfM7ZaE+iuQl4VhADXybwzr4z/sMmnLgTeUZLq5PaDlcJBxYXQ3U91V7dwAIfvw==", "dev": true, "license": "MIT", "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" + "workbox-core": "7.4.0" } }, - "node_modules/whatwg-encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "node_modules/workbox-recipes": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.0.tgz", + "integrity": "sha512-kOkWvsAn4H8GvAkwfJTbwINdv4voFoiE9hbezgB1sb/0NLyTG4rE7l6LvS8lLk5QIRIto+DjXLuAuG3Vmt3cxQ==", "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" + "workbox-cacheable-response": "7.4.0", + "workbox-core": "7.4.0", + "workbox-expiration": "7.4.0", + "workbox-precaching": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" } }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "node_modules/workbox-routing": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.0.tgz", + "integrity": "sha512-C/ooj5uBWYAhAqwmU8HYQJdOjjDKBp9MzTQ+otpMmd+q0eF59K+NuXUek34wbL0RFrIXe/KKT+tUWcZcBqxbHQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "workbox-core": "7.4.0" } }, - "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "node_modules/workbox-strategies": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.0.tgz", + "integrity": "sha512-T4hVqIi5A4mHi92+5EppMX3cLaVywDp8nsyUgJhOZxcfSV/eQofcOA6/EMo5rnTNmNTpw0rUgjAI6LaVullPpg==", "dev": true, "license": "MIT", "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" + "workbox-core": "7.4.0" } }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "node_modules/workbox-streams": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.0.tgz", + "integrity": "sha512-QHPBQrey7hQbnTs5GrEVoWz7RhHJXnPT+12qqWM378orDMo5VMJLCkCM1cnCk+8Eq92lccx/VgRZ7WAzZWbSLg==", "dev": true, + "license": "MIT", "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0" } }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "node_modules/workbox-sw": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.0.tgz", + "integrity": "sha512-ltU+Kr3qWR6BtbdlMnCjobZKzeV1hN+S6UvDywBrwM19TTyqA03X66dzw1tEIdJvQ4lYKkBFox6IAEhoSEZ8Xw==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-webpack-plugin": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-7.4.0.tgz", + "integrity": "sha512-NRgx4lYe4JP5I8qqiROmngbc38WyyN3BZh48lUir2XYJ63EuHWN0KpDxgcYQ/fJtQQIBoswwUPmpqwQmaupnxQ==", "dev": true, + "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" + "fast-json-stable-stringify": "^2.1.0", + "pretty-bytes": "^5.4.1", + "upath": "^1.2.0", + "webpack-sources": "^1.4.3", + "workbox-build": "7.4.0" }, "engines": { - "node": ">= 0.4" + "node": ">=20.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "webpack": "^4.4.0 || ^5.91.0" } }, - "node_modules/wildcard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "dev": true + "node_modules/workbox-webpack-plugin/node_modules/webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "node_modules/workbox-window": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.0.tgz", + "integrity": "sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "7.4.0" + } }, "node_modules/wrap-ansi": { "version": "7.0.0", @@ -17265,6 +19135,17 @@ "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", "dev": true }, + "@apideck/better-ajv-errors": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", + "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "dev": true, + "requires": { + "json-schema": "^0.4.0", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + } + }, "@asamuzakjp/css-color": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", @@ -20131,6 +22012,94 @@ "integrity": "sha512-YRCrJdhQLobGIQ8Cj1sta3nn6DrZDTSUnrIYhS2e5V590BmfVDleKoAquclAiKSBKWJwmuXTb+b4BL6rSHnahw==", "dev": true }, + "@rollup/plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", + "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.10.4", + "@rollup/pluginutils": "^3.1.0" + } + }, + "@rollup/plugin-node-resolve": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "dependencies": { + "@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + } + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true + } + } + }, + "@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + } + }, + "@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "requires": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + } + }, + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "dependencies": { + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + } + } + }, "@sentry-internal/browser-utils": { "version": "10.41.0", "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.41.0.tgz", @@ -20234,6 +22203,18 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true }, + "@surma/rollup-plugin-off-main-thread": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", + "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", + "dev": true, + "requires": { + "ejs": "^3.1.6", + "json5": "^2.2.0", + "magic-string": "^0.25.0", + "string.prototype.matchall": "^4.0.6" + } + }, "@swc/helpers": { "version": "0.5.18", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", @@ -20682,6 +22663,12 @@ "@types/react-router": "*" } }, + "@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true + }, "@types/retry": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", @@ -20774,6 +22761,12 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true }, + "@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true + }, "@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -21314,6 +23307,16 @@ "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", "dev": true }, + "array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + } + }, "array-differ": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", @@ -21344,6 +23347,21 @@ "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", "dev": true }, + "arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + } + }, "asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -21382,6 +23400,12 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, + "async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, "async-done": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/async-done/-/async-done-2.0.0.tgz", @@ -21393,6 +23417,12 @@ "stream-exhaust": "^1.0.2" } }, + "async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true + }, "async-settle": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-2.0.0.tgz", @@ -22351,6 +24381,12 @@ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", "dev": true }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true + }, "css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", @@ -22519,6 +24555,39 @@ "whatwg-url": "^14.0.0" } }, + "data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + } + }, + "data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + } + }, + "data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, "dateformat": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", @@ -22591,6 +24660,17 @@ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true }, + "define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, "del": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/del/-/del-8.0.1.tgz", @@ -22792,6 +24872,15 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true }, + "ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "requires": { + "jake": "^10.8.5" + } + }, "electron-to-chromium": { "version": "1.5.267", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", @@ -22877,6 +24966,68 @@ "is-arrayish": "^0.2.1" } }, + "es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + } + }, "es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -22916,6 +25067,17 @@ "hasown": "^2.0.2" } }, + "es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "requires": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + } + }, "escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -22973,6 +25135,12 @@ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true }, + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -23049,12 +25217,6 @@ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", "dev": true - }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true } } }, @@ -23303,7 +25465,36 @@ "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, "requires": { - "escape-string-regexp": "^1.0.5" + "escape-string-regexp": "^1.0.5" + } + }, + "filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } } }, "fill-range": { @@ -23609,6 +25800,26 @@ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true }, + "function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, "generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -23645,6 +25856,12 @@ "math-intrinsics": "^1.1.0" } }, + "get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true + }, "get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -23670,6 +25887,17 @@ "pump": "^3.0.0" } }, + "get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + } + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -23792,6 +26020,16 @@ "which": "^1.2.14" } }, + "globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "requires": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + } + }, "globby": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", @@ -24076,6 +26314,12 @@ } } }, + "has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -24100,6 +26344,15 @@ "es-define-property": "^1.0.0" } }, + "has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "requires": { + "dunder-proto": "^1.0.0" + } + }, "has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -24125,12 +26378,6 @@ "type-fest": "^0.8.0" }, "dependencies": { - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true - }, "type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", @@ -24445,6 +26692,17 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, + "internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + } + }, "interpret": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", @@ -24486,12 +26744,45 @@ "has-tostringtag": "^1.0.2" } }, + "is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + } + }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, + "is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "requires": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + } + }, + "is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "requires": { + "has-bigints": "^1.0.2" + } + }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -24501,6 +26792,16 @@ "binary-extensions": "^2.0.0" } }, + "is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + } + }, "is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -24516,6 +26817,27 @@ "hasown": "^2.0.2" } }, + "is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + } + }, + "is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + } + }, "is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -24548,6 +26870,15 @@ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true }, + "is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "requires": { + "call-bound": "^1.0.3" + } + }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -24601,12 +26932,30 @@ "is-path-inside": "^3.0.2" } }, + "is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, "is-negated-glob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", "dev": true }, + "is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true + }, "is-network-error": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", @@ -24619,6 +26968,22 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true + }, "is-path-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-3.0.0.tgz", @@ -24655,6 +27020,12 @@ "hasown": "^2.0.2" } }, + "is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true + }, "is-relative": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", @@ -24664,6 +27035,48 @@ "is-unc-path": "^1.0.0" } }, + "is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true + }, + "is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "requires": { + "call-bound": "^1.0.3" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + } + }, + "is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + } + }, "is-typed-array": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", @@ -24700,6 +27113,31 @@ "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", "dev": true }, + "is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true + }, + "is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "requires": { + "call-bound": "^1.0.3" + } + }, + "is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + } + }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -24715,6 +27153,12 @@ "is-inside-container": "^1.0.0" } }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -24819,6 +27263,17 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "requires": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + } + }, "jest": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", @@ -24865,12 +27320,6 @@ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true - }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -25656,6 +28105,12 @@ "universalify": "^2.0.0" } }, + "jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true + }, "jsprim": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", @@ -25891,6 +28346,12 @@ "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", "dev": true }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, "lodash.template": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", @@ -26025,6 +28486,15 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true }, + "magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.8" + } + }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -26334,6 +28804,26 @@ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + } + }, "object.defaults": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", @@ -26412,6 +28902,17 @@ "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", "dev": true }, + "own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + } + }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -27176,6 +29677,22 @@ "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "dev": true }, + "reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + } + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -27197,6 +29714,20 @@ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "dev": true }, + "regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + } + }, "regexpu-core": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", @@ -27460,6 +29991,15 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true }, + "rollup": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", + "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, "rrweb-cssom": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", @@ -27490,12 +30030,35 @@ "tslib": "^2.1.0" } }, + "safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, + "safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + } + }, "safe-regex-test": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", @@ -27723,10 +30286,33 @@ "requires": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, + "set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + } + }, + "set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "requires": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" } }, "setprototypeof": { @@ -27850,6 +30436,12 @@ "is-fullwidth-code-point": "^3.0.0" } }, + "smob": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.1.tgz", + "integrity": "sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g==", + "dev": true + }, "sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -27861,6 +30453,12 @@ "websocket-driver": "^0.7.4" } }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -27883,6 +30481,12 @@ "source-map": "^0.6.0" } }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, "sparkles": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", @@ -28016,12 +30620,6 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true - }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true } } }, @@ -28031,6 +30629,16 @@ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true }, + "stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + } + }, "stream-browserify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", @@ -28130,6 +30738,76 @@ "strip-ansi": "^6.0.1" } }, + "string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + } + }, + "string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + } + }, + "string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "requires": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + } + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -28154,6 +30832,12 @@ "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true }, + "strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "dev": true + }, "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -28277,6 +30961,32 @@ "streamx": "^2.12.5" } }, + "temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true + }, + "tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "dev": true, + "requires": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "dependencies": { + "type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true + } + } + }, "terser": { "version": "5.44.1", "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", @@ -28537,6 +31247,59 @@ "mime-types": "~2.1.24" } }, + "typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + } + }, + "typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + } + }, + "typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + } + }, + "typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + } + }, "typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -28567,6 +31330,18 @@ "typescript-compare": "^0.0.2" } }, + "unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + } + }, "unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", @@ -28662,6 +31437,15 @@ "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", "dev": true }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "requires": { + "crypto-random-string": "^2.0.0" + } + }, "universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -28708,6 +31492,12 @@ "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", "dev": true }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true + }, "update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -29301,6 +32091,52 @@ "isexe": "^2.0.0" } }, + "which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "requires": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + } + }, + "which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + } + }, + "which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "requires": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + } + }, "which-typed-array": { "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", @@ -29322,6 +32158,325 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, + "workbox-background-sync": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.0.tgz", + "integrity": "sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w==", + "dev": true, + "requires": { + "idb": "^7.0.1", + "workbox-core": "7.4.0" + } + }, + "workbox-broadcast-update": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.0.tgz", + "integrity": "sha512-+eZQwoktlvo62cI0b+QBr40v5XjighxPq3Fzo9AWMiAosmpG5gxRHgTbGGhaJv/q/MFVxwFNGh/UwHZ/8K88lA==", + "dev": true, + "requires": { + "workbox-core": "7.4.0" + } + }, + "workbox-build": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.0.tgz", + "integrity": "sha512-Ntk1pWb0caOFIvwz/hfgrov/OJ45wPEhI5PbTywQcYjyZiVhT3UrwwUPl6TRYbTm4moaFYithYnl1lvZ8UjxcA==", + "dev": true, + "requires": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.24.4", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^5.2.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-replace": "^2.4.1", + "@rollup/plugin-terser": "^0.4.3", + "@surma/rollup-plugin-off-main-thread": "^2.2.3", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^11.0.1", + "lodash": "^4.17.20", + "pretty-bytes": "^5.3.0", + "rollup": "^2.79.2", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "7.4.0", + "workbox-broadcast-update": "7.4.0", + "workbox-cacheable-response": "7.4.0", + "workbox-core": "7.4.0", + "workbox-expiration": "7.4.0", + "workbox-google-analytics": "7.4.0", + "workbox-navigation-preload": "7.4.0", + "workbox-precaching": "7.4.0", + "workbox-range-requests": "7.4.0", + "workbox-recipes": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0", + "workbox-streams": "7.4.0", + "workbox-sw": "7.4.0", + "workbox-window": "7.4.0" + }, + "dependencies": { + "@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true + }, + "balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true + }, + "brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "requires": { + "balanced-match": "^4.0.2" + } + }, + "glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "dev": true, + "requires": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + } + }, + "jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "requires": { + "@isaacs/cliui": "^9.0.0" + } + }, + "lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true + }, + "minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "requires": { + "brace-expansion": "^5.0.2" + } + }, + "path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "requires": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + } + }, + "source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "requires": { + "whatwg-url": "^7.0.0" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } + } + }, + "workbox-cacheable-response": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.0.tgz", + "integrity": "sha512-0Fb8795zg/x23ISFkAc7lbWes6vbw34DGFIMw31cwuHPgDEC/5EYm6m/ZkylLX0EnEbbOyOCLjKgFS/Z5g0HeQ==", + "dev": true, + "requires": { + "workbox-core": "7.4.0" + } + }, + "workbox-core": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.0.tgz", + "integrity": "sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ==", + "dev": true + }, + "workbox-expiration": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.0.tgz", + "integrity": "sha512-V50p4BxYhtA80eOvulu8xVfPBgZbkxJ1Jr8UUn0rvqjGhLDqKNtfrDfjJKnLz2U8fO2xGQJTx/SKXNTzHOjnHw==", + "dev": true, + "requires": { + "idb": "^7.0.1", + "workbox-core": "7.4.0" + } + }, + "workbox-google-analytics": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.0.tgz", + "integrity": "sha512-MVPXQslRF6YHkzGoFw1A4GIB8GrKym/A5+jYDUSL+AeJw4ytQGrozYdiZqUW1TPQHW8isBCBtyFJergUXyNoWQ==", + "dev": true, + "requires": { + "workbox-background-sync": "7.4.0", + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" + } + }, + "workbox-navigation-preload": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.0.tgz", + "integrity": "sha512-etzftSgdQfjMcfPgbfaZCfM2QuR1P+4o8uCA2s4rf3chtKTq/Om7g/qvEOcZkG6v7JZOSOxVYQiOu6PbAZgU6w==", + "dev": true, + "requires": { + "workbox-core": "7.4.0" + } + }, + "workbox-precaching": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.0.tgz", + "integrity": "sha512-VQs37T6jDqf1rTxUJZXRl3yjZMf5JX/vDPhmx2CPgDDKXATzEoqyRqhYnRoxl6Kr0rqaQlp32i9rtG5zTzIlNg==", + "dev": true, + "requires": { + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" + } + }, + "workbox-range-requests": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.0.tgz", + "integrity": "sha512-3Vq854ZNuP6Y0KZOQWLaLC9FfM7ZaE+iuQl4VhADXybwzr4z/sMmnLgTeUZLq5PaDlcJBxYXQ3U91V7dwAIfvw==", + "dev": true, + "requires": { + "workbox-core": "7.4.0" + } + }, + "workbox-recipes": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.0.tgz", + "integrity": "sha512-kOkWvsAn4H8GvAkwfJTbwINdv4voFoiE9hbezgB1sb/0NLyTG4rE7l6LvS8lLk5QIRIto+DjXLuAuG3Vmt3cxQ==", + "dev": true, + "requires": { + "workbox-cacheable-response": "7.4.0", + "workbox-core": "7.4.0", + "workbox-expiration": "7.4.0", + "workbox-precaching": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" + } + }, + "workbox-routing": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.0.tgz", + "integrity": "sha512-C/ooj5uBWYAhAqwmU8HYQJdOjjDKBp9MzTQ+otpMmd+q0eF59K+NuXUek34wbL0RFrIXe/KKT+tUWcZcBqxbHQ==", + "dev": true, + "requires": { + "workbox-core": "7.4.0" + } + }, + "workbox-strategies": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.0.tgz", + "integrity": "sha512-T4hVqIi5A4mHi92+5EppMX3cLaVywDp8nsyUgJhOZxcfSV/eQofcOA6/EMo5rnTNmNTpw0rUgjAI6LaVullPpg==", + "dev": true, + "requires": { + "workbox-core": "7.4.0" + } + }, + "workbox-streams": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.0.tgz", + "integrity": "sha512-QHPBQrey7hQbnTs5GrEVoWz7RhHJXnPT+12qqWM378orDMo5VMJLCkCM1cnCk+8Eq92lccx/VgRZ7WAzZWbSLg==", + "dev": true, + "requires": { + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0" + } + }, + "workbox-sw": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.0.tgz", + "integrity": "sha512-ltU+Kr3qWR6BtbdlMnCjobZKzeV1hN+S6UvDywBrwM19TTyqA03X66dzw1tEIdJvQ4lYKkBFox6IAEhoSEZ8Xw==", + "dev": true + }, + "workbox-webpack-plugin": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-7.4.0.tgz", + "integrity": "sha512-NRgx4lYe4JP5I8qqiROmngbc38WyyN3BZh48lUir2XYJ63EuHWN0KpDxgcYQ/fJtQQIBoswwUPmpqwQmaupnxQ==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "^2.1.0", + "pretty-bytes": "^5.4.1", + "upath": "^1.2.0", + "webpack-sources": "^1.4.3", + "workbox-build": "7.4.0" + }, + "dependencies": { + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + } + } + }, + "workbox-window": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.0.tgz", + "integrity": "sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==", + "dev": true, + "requires": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "7.4.0" + } + }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index f28a28a5..02306fa1 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "license": "MIT", + "license": "UNLICENSED", "scripts": { "start": "DEV=1 webpack serve --host 0.0.0.0", "build": "gulp build", @@ -80,7 +80,8 @@ "webpack": "^5.105.3", "webpack-cli": "^6.0.1", "webpack-dev-server": "^5.2.2", - "webpack-stream": "^7.0.0" + "webpack-stream": "^7.0.0", + "workbox-webpack-plugin": "^7.3.0" }, "repository": { "type": "git", diff --git a/projects/default.json b/projects/default.json index 14420df7..9914c43f 100644 --- a/projects/default.json +++ b/projects/default.json @@ -30,5 +30,6 @@ "homebasePayment": false, "loginForm": "usernamePassword", "maskContactInformation": true, - "memberManagement": false + "memberManagement": false, + "privacySettings": false } diff --git a/projects/lspl.json b/projects/lspl.json new file mode 100644 index 00000000..a8d3c481 --- /dev/null +++ b/projects/lspl.json @@ -0,0 +1,77 @@ +{ + "aerodrome": { + "name": "Langenthal", + "ICAO": "LSPL", + "runways": [ + { + "name": "05", + "type": "A" + }, + { + "name": "23", + "type": "A" + }, + { + "name": "05R", + "type": "G" + }, + { + "name": "23L", + "type": "G" + } + ], + "noRunwayIfHelicopter": true, + "departureRoutes": [ + { + "name": "O", + "label": "Ost" + }, + { + "name": "W", + "label": "West" + } + ], + "arrivalRoutes": [ + { + "name": "N", + "label": "Nord" + }, + { + "name": "S", + "label": "Süd" + } + ], + "landingFeesStrategy": "lspl" + }, + "environments": { + "test": { + "firebaseProjectId": "lspl-test", + "firebaseDatabaseUrl": "https://lspl-test-default-rtdb.europe-west1.firebasedatabase.app", + "firebaseApiKey": "AIzaSyAMrXeJqssO1Wixyf25T-_UBJFyR8emcdk" + } + }, + "theme": "lspl", + "title": "Flightbox - Flugplatz Langenthal", + "themeColor": "#027ABB", + "shortName": "Flightbox LSPL", + "paymentMethods": [ + "checkout", + "cash", + "invoice" + ], + "homebasePayment": true, + "loginForm": "email", + "enabledFlightTypes": [ + "private", + "instruction", + "aerotow", + "glider_private_aerotow", + "glider_private_winch", + "glider_private_self", + "glider_instruction_aerotow", + "glider_instruction_winch", + "glider_instruction_self" + ], + "maskContactInformation": false, + "privacySettings": true +} diff --git a/projects/lspv.json b/projects/lspv.json index dae599dc..b54b5d7c 100644 --- a/projects/lspv.json +++ b/projects/lspv.json @@ -89,6 +89,8 @@ }, "theme": "lspv", "title": "Flightbox - Flugplatz Wangen-Lachen", + "themeColor": "#1d77d7", + "shortName": "Flightbox LSPV", "departureCommitRequirements": [ { "de": "Keine Starts auf RWY 26 von 12:00h bis 13:30h LT!", diff --git a/projects/lsze.json b/projects/lsze.json index 578b8832..6ee863f2 100644 --- a/projects/lsze.json +++ b/projects/lsze.json @@ -57,6 +57,8 @@ }, "theme": "lsze", "title": "Flightbox - Flugplatz Bad Ragaz", + "themeColor": "#063155", + "shortName": "Flightbox LSZE", "paymentMethods": [ { "name": "checkout", diff --git a/projects/lszk.json b/projects/lszk.json index edf57994..8310c077 100644 --- a/projects/lszk.json +++ b/projects/lszk.json @@ -46,6 +46,8 @@ "flightnetCompany": "fgzo", "theme": "lszk", "title": "FGZO Bewegungen", + "themeColor": "#284496", + "shortName": "Flightbox LSZK", "enabledFlightTypes": [ "private", "instruction", diff --git a/projects/lszm.json b/projects/lszm.json index 368fc24f..6283bcd1 100644 --- a/projects/lszm.json +++ b/projects/lszm.json @@ -64,6 +64,8 @@ }, "theme": "lszm", "title": "Flightbox - Flugplatz Mollis", + "themeColor": "#324158", + "shortName": "Flightbox LSZM", "paymentMethods": ["checkout", "invoice"], "homebasePayment": true, "loginForm": "email", diff --git a/projects/lszo.json b/projects/lszo.json index 5d1afe8b..0e112007 100644 --- a/projects/lszo.json +++ b/projects/lszo.json @@ -49,6 +49,8 @@ }, "theme": "lszo", "title": "Flightbox - Flugplatz Luzern-Beromünster", + "themeColor": "#19295d", + "shortName": "Flightbox LSZO", "paymentMethods": [ "checkout", "cash", diff --git a/projects/lszt.json b/projects/lszt.json index 74fc1d39..b9dd5fa3 100644 --- a/projects/lszt.json +++ b/projects/lszt.json @@ -102,6 +102,8 @@ "flightnetCompany": "mfgt", "theme": "lszt", "title": "Flightbox - Flugplatz Lommis", + "themeColor": "#003863", + "shortName": "Flightbox LSZT", "paymentMethods": ["cash", "card"], "memberManagement": true } diff --git a/src/app.tsx b/src/app.tsx index 00d3419c..b7d7b4f1 100755 --- a/src/app.tsx +++ b/src/app.tsx @@ -23,9 +23,12 @@ import GlobalStyle from './style/global-style'; import * as Sentry from "@sentry/react"; +import ErrorFallback from './components/ErrorFallback'; +import { getMidnightDelayMs } from './util/getMidnightDelay'; +import { shouldReloadOnControllerChange, markReload } from './util/shouldReloadOnControllerChange'; + Sentry.init({ dsn: "https://8a606d82aa68850021fbfac2ffda30b5@o4509293310967808.ingest.de.sentry.io/4509293314113617", - sendDefaultPii: true }); const theme = require('../theme/' + __THEME__); @@ -44,16 +47,36 @@ sagaMiddleware.run(autoRestart(sagas)); createRoot(document.getElementById('app')!).render( - - - - - - + }> + + + + + + + ); -setTimeout( - () => window.location.reload(), - moment('24:00:00', 'hh:mm:ss').diff(moment(), 'milliseconds') -); +setTimeout(() => window.location.reload(), getMidnightDelayMs()); + +if (!__DEV__ && 'serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/service-worker.js').then(registration => { + setInterval(() => { + registration.update().catch(() => {}); + }, 3 * 60 * 1000); + }).catch(err => { + console.error('SW registration failed', err); + }); + }); + + let hasController = !!navigator.serviceWorker.controller; + navigator.serviceWorker.addEventListener('controllerchange', () => { + if (hasController && shouldReloadOnControllerChange()) { + markReload(); + window.location.reload(); + } + hasController = true; + }); +} diff --git a/src/components/AdminPage/AdminNavigation.tsx b/src/components/AdminPage/AdminNavigation.tsx index 88de439a..926332b4 100644 --- a/src/components/AdminPage/AdminNavigation.tsx +++ b/src/components/AdminPage/AdminNavigation.tsx @@ -117,6 +117,7 @@ const AdminNavigation = ({ activeTab, hiddenTabs, onTabChange }) => { { key: 'kiosk-access', label: t('admin.kioskAccess'), icon: 'person_add' }, { key: 'guest-access', label: t('admin.guestAccess'), icon: 'person_add' }, { key: 'import', label: t('admin.import'), icon: 'file_upload' }, + { key: 'privacy', label: t('admin.privacy'), icon: 'security' }, ].filter(item => !hiddenTabs.includes(item.key)); return ( diff --git a/src/components/AdminPage/AdminPage.tsx b/src/components/AdminPage/AdminPage.tsx index 2678f943..ddfd5c80 100644 --- a/src/components/AdminPage/AdminPage.tsx +++ b/src/components/AdminPage/AdminPage.tsx @@ -14,6 +14,7 @@ import AdminAircraftPage from './subpages/AdminAircraftPage'; import AdminInvoiceRecipientsPage from './subpages/AdminInvoiceRecipientsPage'; import AdminGuestAccessPage from './subpages/AdminGuestAccessPage'; import AdminKioskAccessPage from './subpages/AdminKioskAccessPage'; +import AdminPrivacySettingsPage from './subpages/AdminPrivacySettingsPage'; import Content from './Content'; import objectToArray from '../../util/objectToArray'; @@ -77,6 +78,8 @@ class AdminPage extends Component { return ; case 'kiosk-access': return ; + case 'privacy': + return ; default: return ; } @@ -102,6 +105,9 @@ class AdminPage extends Component { if (!memberManagementEnabled) { hiddenTabs.push('import') } + if (__CONF__.privacySettings !== true) { + hiddenTabs.push('privacy') + } return ( diff --git a/src/components/AdminPage/subpages/AdminPrivacySettingsPage.tsx b/src/components/AdminPage/subpages/AdminPrivacySettingsPage.tsx new file mode 100644 index 00000000..4c0d6d6d --- /dev/null +++ b/src/components/AdminPage/subpages/AdminPrivacySettingsPage.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import PrivacySettingsForm from '../../../containers/PrivacySettingsFormContainer'; + +const AdminPrivacySettingsPage = () => { + return ( + + ); +}; + +export default AdminPrivacySettingsPage; diff --git a/src/components/AerodromeStatusPage/AerodromeStatusPage.tsx b/src/components/AerodromeStatusPage/AerodromeStatusPage.tsx index d4e8f7ae..feb13f24 100644 --- a/src/components/AerodromeStatusPage/AerodromeStatusPage.tsx +++ b/src/components/AerodromeStatusPage/AerodromeStatusPage.tsx @@ -7,12 +7,12 @@ import MaterialIcon from '../MaterialIcon' import {getLabel} from '../AerodromeStatusForm/StatusOptions' import dates from '../../util/dates' import newLineToBr from '../../util/newLineToBr' -import { withTranslation } from 'react-i18next' +import {withTranslation} from 'react-i18next' const StyledLogo = styled(Logo)` width: 200px; float: left; - + @media(max-width: 700px) { float: none; text-align: center; @@ -24,7 +24,7 @@ const StyledLogo = styled(Logo)` const StyledContainer = styled.div` margin-left: 250px; - + @media(max-width: 700px) { margin-left: 0; } @@ -67,7 +67,7 @@ class AerodromeStatusPage extends Component { {getLabel(status.status)}
{newLineToBr(status.details)}
- {dates.formatDateTime(status.timestamp)}, {status.by} + {dates.formatDateTime(status.timestamp)}
); diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 668787af..b54362bf 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { withTranslation } from 'react-i18next'; import {Redirect, Route, Switch} from 'react-router-dom'; -import LoginPage from '../../components/LoginPage'; +import LoginPage from '../../containers/LoginPageContainer'; import Centered from '../Centered'; import MaterialIcon from '../MaterialIcon'; import MessagePage from "../../containers/MessagePageContainer"; diff --git a/src/components/ErrorFallback/ErrorFallback.spec.tsx b/src/components/ErrorFallback/ErrorFallback.spec.tsx new file mode 100644 index 00000000..70af7b8d --- /dev/null +++ b/src/components/ErrorFallback/ErrorFallback.spec.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import ErrorFallback from './ErrorFallback'; + +describe('ErrorFallback', () => { + it('should render the error message text', () => { + render(); + expect(screen.getByText('Something went wrong')).toBeDefined(); + expect(screen.getByText('An unexpected error occurred.')).toBeDefined(); + }); + + it('should render a clickable reload button', () => { + // Suppress jsdom "Not implemented: navigation" error from reload() + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + render(); + const button = screen.getByText('Reload'); + expect(button.tagName).toBe('BUTTON'); + // window.location.reload() cannot be mocked in jsdom, but we verify + // the button exists and clicking it does not throw. + fireEvent.click(button); + errorSpy.mockRestore(); + }); +}); diff --git a/src/components/ErrorFallback/ErrorFallback.tsx b/src/components/ErrorFallback/ErrorFallback.tsx new file mode 100644 index 00000000..839be850 --- /dev/null +++ b/src/components/ErrorFallback/ErrorFallback.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +const ErrorFallback = () => ( +
+

+ Something went wrong +

+

+ An unexpected error occurred. +

+ +
+); + +export default ErrorFallback; diff --git a/src/components/ErrorFallback/index.ts b/src/components/ErrorFallback/index.ts new file mode 100644 index 00000000..5f6c0e7f --- /dev/null +++ b/src/components/ErrorFallback/index.ts @@ -0,0 +1 @@ +export { default } from './ErrorFallback'; diff --git a/src/components/LoginPage/EmailLoginForm.tsx b/src/components/LoginPage/EmailLoginForm.tsx index 4a4a301a..f338ad3f 100644 --- a/src/components/LoginPage/EmailLoginForm.tsx +++ b/src/components/LoginPage/EmailLoginForm.tsx @@ -6,6 +6,7 @@ import LabeledComponent from '../LabeledComponent'; import Failure from './Failure'; import Button from '../Button'; import GuestTokenLogin from '../../containers/GuestTokenLoginContainer' +import OtpCodeForm from './OtpCodeForm'; const handleSubmit = (authenticate, email, local, e) => { e.preventDefault(); @@ -61,20 +62,27 @@ const EmailLoginForm = props => { queryToken, guestOnly, sendAuthenticationEmail, - completeEmailAuthentication, + verifyOtpCode, email, submitting, failure, emailSent, - emailLoginParamsPresent, - emailLoginCompletionFailure, - updateEmail + otpVerificationFailure, + updateEmail, + resetOtp } = props; if (emailSent) { - return
{t('login.emailSentPre')} {email} {t('login.emailSentPost')} -
+ return ( + verifyOtpCode(email, code)} + onResend={() => sendAuthenticationEmail(email, !!queryToken)} + onChangeEmail={resetOtp} + /> + ); } const emailInput = ( @@ -90,31 +98,6 @@ const EmailLoginForm = props => { /> ); - if (emailLoginParamsPresent) { - return ( -
-
{t('login.completeLogin')}
- - - {emailLoginCompletionFailure && } - - -
- ) - } - return (
{!guestOnly && ( @@ -159,16 +142,16 @@ const EmailLoginForm = props => { queryToken: PropTypes.string, guestOnly: PropTypes.bool, sendAuthenticationEmail: PropTypes.func.isRequired, - completeEmailAuthentication: PropTypes.func.isRequired, + verifyOtpCode: PropTypes.func.isRequired, updateEmail: PropTypes.func.isRequired, + resetOtp: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired, showCancel: PropTypes.bool.isRequired, email: PropTypes.string.isRequired, submitting: PropTypes.bool.isRequired, failure: PropTypes.bool.isRequired, emailSent: PropTypes.bool.isRequired, - emailLoginParamsPresent: PropTypes.bool.isRequired, - emailLoginCompletionFailure: PropTypes.bool.isRequired, + otpVerificationFailure: PropTypes.bool.isRequired, }; export default EmailLoginForm; diff --git a/src/components/LoginPage/LoginPage.tsx b/src/components/LoginPage/LoginPage.tsx index eec4c0ae..f85106ac 100644 --- a/src/components/LoginPage/LoginPage.tsx +++ b/src/components/LoginPage/LoginPage.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Trans, useTranslation } from 'react-i18next'; import Header from './Header'; import EmailLoginForm from '../../containers/EmailLoginFormContainer' import UsernamePasswordLoginForm from '../../containers/UsernamePasswordLoginFormContainer' @@ -43,7 +44,19 @@ const LoginInnerWrapper = styled.div` } ` -const LoginPage = ({location}) => { +const PrivacyText = styled.p` + margin-top: 2em; + font-size: 0.85em; + color: #666; + + a { + color: #666; + text-decoration: underline; + } +` + +const LoginPage = ({location, privacyPolicyUrl, emailSent}: {location: any, privacyPolicyUrl?: string | null, emailSent?: boolean}) => { + const { t } = useTranslation(); const queryToken = getAuthQueryToken(location) const guestOnly = getGuestOnly(location) return ( @@ -55,6 +68,13 @@ const LoginPage = ({location}) => { ? : } + {privacyPolicyUrl && !emailSent && /^https?:\/\//.test(privacyPolicyUrl) && ( + + + Mit der Anmeldung akzeptieren Sie die Datenschutzerklärung. + + + )} diff --git a/src/components/LoginPage/OtpCodeForm.spec.tsx b/src/components/LoginPage/OtpCodeForm.spec.tsx new file mode 100644 index 00000000..98d90826 --- /dev/null +++ b/src/components/LoginPage/OtpCodeForm.spec.tsx @@ -0,0 +1,394 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { renderWithTheme, screen, fireEvent } from '../../../test/renderWithTheme'; +import OtpCodeForm from './OtpCodeForm'; + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ t: (key: string) => key }), + Trans: ({ i18nKey, values }: { i18nKey: string; values?: Record }) => + values?.email ? `${i18nKey} ${values.email}` : i18nKey, +})); + +jest.mock('../MaterialIcon', () => { + const React = require('react'); + return function MockMaterialIcon({ icon }: { icon: string }) { + return ; + }; +}); + +const baseProps = { + email: 'test@example.com', + submitting: false, + failure: false, + onSubmit: jest.fn(), +}; + +function getDigits() { + return screen.getAllByRole('textbox'); +} + +function fillDigits(digits: string[]) { + const inputs = getDigits(); + digits.forEach((d, i) => { + fireEvent.change(inputs[i], { target: { value: d } }); + }); +} + +function pasteIntoGroup(text: string) { + const group = screen.getByRole('group'); + fireEvent.paste(group, { + clipboardData: { getData: () => text }, + }); +} + +// The resend cooldown ticks 1 second at a time via recursive useEffect+setTimeout. +// Advancing fake time in 1-second increments lets React flush effects between ticks. +function advanceCooldown(seconds = 60) { + for (let i = 0; i < seconds; i++) { + act(() => { jest.advanceTimersByTime(1000); }); + } +} + +describe('OtpCodeForm', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + }); + + afterEach(() => { + act(() => { + jest.runOnlyPendingTimers(); + }); + jest.useRealTimers(); + }); + + // ─── Rendering ──────────────────────────────────────────────────────────── + + describe('rendering', () => { + it('renders 6 digit inputs', () => { + renderWithTheme(); + expect(getDigits()).toHaveLength(6); + }); + + it('renders the instruction containing the email address', () => { + renderWithTheme(); + expect(screen.getByText(/test@example\.com/)).toBeInTheDocument(); + }); + + it('does not render resend button when onResend is not provided', () => { + renderWithTheme(); + expect(screen.queryByText('login.otpResend')).not.toBeInTheDocument(); + expect(screen.queryByText('login.otpResendIn')).not.toBeInTheDocument(); + }); + + it('renders resend button (in cooldown state) when onResend is provided', () => { + renderWithTheme(); + expect(screen.getByText('login.otpResendIn')).toBeInTheDocument(); + }); + + it('submit button is disabled when no digits are entered', () => { + renderWithTheme(); + expect(screen.getByRole('button', { name: /login.loginButton/ })).toBeDisabled(); + }); + + it('all digit inputs are disabled when submitting is true', () => { + renderWithTheme(); + getDigits().forEach(input => expect(input).toBeDisabled()); + }); + + it('submit button is disabled when submitting is true', () => { + renderWithTheme(); + expect(screen.getByRole('button', { name: /login.loginButton/ })).toBeDisabled(); + }); + }); + + // ─── Failure message ────────────────────────────────────────────────────── + + describe('failure message', () => { + it('does not show failure message when failure is false', () => { + renderWithTheme(); + expect(screen.queryByText('login.otpVerificationFailure')).not.toBeInTheDocument(); + }); + + it('shows OTP-specific failure message when failure is true', () => { + renderWithTheme(); + expect(screen.getByText('login.otpVerificationFailure')).toBeInTheDocument(); + }); + }); + + // ─── Digit input ────────────────────────────────────────────────────────── + + describe('digit input', () => { + it('fills a digit when a numeric character is typed', () => { + renderWithTheme(); + const inputs = getDigits(); + fireEvent.change(inputs[0], { target: { value: '7' } }); + expect(inputs[0]).toHaveValue('7'); + }); + + it('ignores non-numeric input', () => { + renderWithTheme(); + const inputs = getDigits(); + fireEvent.change(inputs[0], { target: { value: 'a' } }); + expect(inputs[0]).toHaveValue(''); + }); + + it('moves focus to next input after entering a digit', () => { + renderWithTheme(); + const inputs = getDigits(); + fireEvent.change(inputs[0], { target: { value: '5' } }); + act(() => { jest.runAllTimers(); }); + expect(document.activeElement).toBe(inputs[1]); + }); + + it('does not advance focus when entering a digit in the last input', () => { + renderWithTheme(); + const inputs = getDigits(); + inputs[5].focus(); + fireEvent.change(inputs[5], { target: { value: '9' } }); + // No error and focus stays on last input (or auto-submit fires) + expect(inputs[5]).toHaveValue('9'); + }); + + it('enables submit button when all 6 digits are entered', () => { + renderWithTheme(); + fillDigits(['1', '2', '3', '4', '5', '6']); + expect(screen.getByRole('button', { name: /login.loginButton/ })).not.toBeDisabled(); + }); + }); + + // ─── Backspace ──────────────────────────────────────────────────────────── + + describe('backspace', () => { + it('clears the current digit when backspace is pressed on a filled input', () => { + renderWithTheme(); + const inputs = getDigits(); + fireEvent.change(inputs[2], { target: { value: '4' } }); + fireEvent.keyDown(inputs[2], { key: 'Backspace' }); + expect(inputs[2]).toHaveValue(''); + }); + + it('clears the previous digit and moves focus back when backspace is pressed on an empty input', () => { + renderWithTheme(); + const inputs = getDigits(); + fireEvent.change(inputs[0], { target: { value: '3' } }); + act(() => { jest.runAllTimers(); }); + // Focus is now on inputs[1], which is empty + fireEvent.keyDown(inputs[1], { key: 'Backspace' }); + expect(inputs[0]).toHaveValue(''); + expect(document.activeElement).toBe(inputs[0]); + }); + + it('does not move focus before the first input on backspace', () => { + renderWithTheme(); + const inputs = getDigits(); + inputs[0].focus(); + fireEvent.keyDown(inputs[0], { key: 'Backspace' }); + expect(document.activeElement).toBe(inputs[0]); + }); + }); + + // ─── Arrow key navigation ───────────────────────────────────────────────── + + describe('arrow key navigation', () => { + it('moves focus left on ArrowLeft', () => { + renderWithTheme(); + const inputs = getDigits(); + inputs[3].focus(); + fireEvent.keyDown(inputs[3], { key: 'ArrowLeft' }); + expect(document.activeElement).toBe(inputs[2]); + }); + + it('moves focus right on ArrowRight', () => { + renderWithTheme(); + const inputs = getDigits(); + inputs[2].focus(); + fireEvent.keyDown(inputs[2], { key: 'ArrowRight' }); + expect(document.activeElement).toBe(inputs[3]); + }); + + it('does not move focus left of the first input', () => { + renderWithTheme(); + const inputs = getDigits(); + inputs[0].focus(); + fireEvent.keyDown(inputs[0], { key: 'ArrowLeft' }); + expect(document.activeElement).toBe(inputs[0]); + }); + + it('does not move focus right of the last input', () => { + renderWithTheme(); + const inputs = getDigits(); + inputs[5].focus(); + fireEvent.keyDown(inputs[5], { key: 'ArrowRight' }); + expect(document.activeElement).toBe(inputs[5]); + }); + }); + + // ─── Paste ──────────────────────────────────────────────────────────────── + + describe('paste', () => { + it('fills all 6 digit inputs when a complete numeric code is pasted', () => { + renderWithTheme(); + pasteIntoGroup('123456'); + const inputs = getDigits(); + expect(inputs[0]).toHaveValue('1'); + expect(inputs[1]).toHaveValue('2'); + expect(inputs[2]).toHaveValue('3'); + expect(inputs[3]).toHaveValue('4'); + expect(inputs[4]).toHaveValue('5'); + expect(inputs[5]).toHaveValue('6'); + }); + + it('strips non-numeric characters from pasted text', () => { + renderWithTheme(); + pasteIntoGroup('12-34-56'); + const inputs = getDigits(); + expect(inputs[0]).toHaveValue('1'); + expect(inputs[5]).toHaveValue('6'); + }); + + it('fills only available slots when a partial code is pasted', () => { + renderWithTheme(); + pasteIntoGroup('123'); + const inputs = getDigits(); + expect(inputs[0]).toHaveValue('1'); + expect(inputs[1]).toHaveValue('2'); + expect(inputs[2]).toHaveValue('3'); + expect(inputs[3]).toHaveValue(''); + }); + + it('auto-submits when a complete 6-digit code is pasted', () => { + const onSubmit = jest.fn(); + renderWithTheme(); + pasteIntoGroup('654321'); + act(() => { jest.runAllTimers(); }); + expect(onSubmit).toHaveBeenCalledWith('654321'); + }); + + it('does not auto-submit when a partial code is pasted', () => { + const onSubmit = jest.fn(); + renderWithTheme(); + pasteIntoGroup('123'); + act(() => { jest.runAllTimers(); }); + expect(onSubmit).not.toHaveBeenCalled(); + }); + + it('does not submit when only non-numeric text is pasted', () => { + const onSubmit = jest.fn(); + renderWithTheme(); + pasteIntoGroup('abcdef'); + act(() => { jest.runAllTimers(); }); + expect(onSubmit).not.toHaveBeenCalled(); + }); + }); + + // ─── Auto-submit ────────────────────────────────────────────────────────── + + describe('auto-submit', () => { + it('calls onSubmit with the complete code when all 6 digits are typed', () => { + const onSubmit = jest.fn(); + renderWithTheme(); + fillDigits(['1', '2', '3', '4', '5', '6']); + act(() => { jest.runAllTimers(); }); + expect(onSubmit).toHaveBeenCalledWith('123456'); + }); + + it('does not auto-submit when only 5 digits are entered', () => { + const onSubmit = jest.fn(); + renderWithTheme(); + fillDigits(['1', '2', '3', '4', '5']); + act(() => { jest.runAllTimers(); }); + expect(onSubmit).not.toHaveBeenCalled(); + }); + }); + + // ─── Submit button ──────────────────────────────────────────────────────── + + describe('submit button click', () => { + it('calls onSubmit when the submit button is clicked with all digits filled', () => { + const onSubmit = jest.fn(); + renderWithTheme(); + fillDigits(['9', '8', '7', '6', '5', '4']); + // Flush the auto-submit timer, then reset the mock to test the button separately + act(() => { jest.runAllTimers(); }); + onSubmit.mockClear(); + + fireEvent.click(screen.getByRole('button', { name: /login.loginButton/ })); + expect(onSubmit).toHaveBeenCalledWith('987654'); + }); + }); + + // ─── Change email ───────────────────────────────────────────────────────── + + describe('change email button', () => { + it('does not render change email button when onChangeEmail is not provided', () => { + renderWithTheme(); + expect(screen.queryByText('login.otpChangeEmail')).not.toBeInTheDocument(); + }); + + it('renders change email button when onChangeEmail is provided', () => { + renderWithTheme(); + expect(screen.getByText('login.otpChangeEmail')).toBeInTheDocument(); + }); + + it('calls onChangeEmail when the button is clicked', () => { + const onChangeEmail = jest.fn(); + renderWithTheme(); + fireEvent.click(screen.getByText('login.otpChangeEmail')); + expect(onChangeEmail).toHaveBeenCalledTimes(1); + }); + }); + + // ─── Resend ─────────────────────────────────────────────────────────────── + + describe('resend button', () => { + it('is disabled during the initial cooldown period', () => { + renderWithTheme(); + expect(screen.getByText('login.otpResendIn').closest('button')).toBeDisabled(); + }); + + it('becomes enabled after the cooldown expires', () => { + renderWithTheme(); + advanceCooldown(); + expect(screen.getByText('login.otpResend').closest('button')).not.toBeDisabled(); + }); + + it('calls onResend when clicked after cooldown expires', () => { + const onResend = jest.fn(); + renderWithTheme(); + advanceCooldown(); + fireEvent.click(screen.getByText('login.otpResend')); + expect(onResend).toHaveBeenCalledTimes(1); + }); + + it('resets the cooldown after resend, making the button disabled again', () => { + const onResend = jest.fn(); + renderWithTheme(); + advanceCooldown(); + fireEvent.click(screen.getByText('login.otpResend')); + // Cooldown is reset to 60; the "resendIn" text should be back + expect(screen.getByText('login.otpResendIn').closest('button')).toBeDisabled(); + }); + + it('clears all digit inputs after resend', () => { + const onResend = jest.fn(); + renderWithTheme(); + fillDigits(['1', '2', '3', '4', '5', '6']); + // Flush the auto-submit timer before advancing the cooldown + act(() => { jest.runAllTimers(); }); + advanceCooldown(); + fireEvent.click(screen.getByText('login.otpResend')); + act(() => { jest.runAllTimers(); }); + getDigits().forEach(input => expect(input).toHaveValue('')); + }); + + it('does not call onResend when clicked during the cooldown period', () => { + const onResend = jest.fn(); + renderWithTheme(); + // Only advance 30s (cooldown still active) + act(() => { jest.advanceTimersByTime(30000); }); + fireEvent.click(screen.getByText('login.otpResendIn')); + expect(onResend).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/components/LoginPage/OtpCodeForm.tsx b/src/components/LoginPage/OtpCodeForm.tsx new file mode 100644 index 00000000..1770dc17 --- /dev/null +++ b/src/components/LoginPage/OtpCodeForm.tsx @@ -0,0 +1,286 @@ +import React, { useRef, useState, useCallback, useEffect } from 'react'; +import styled from 'styled-components'; +import { useTranslation, Trans } from 'react-i18next'; +import Button from '../Button'; + +const CODE_LENGTH = 6; +const RESEND_COOLDOWN_SECONDS = 60; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 1.5rem; +`; + +const Instruction = styled.p` + margin: 0; + font-size: 0.95rem; + text-align: center; + color: #444; +`; + +const OtpInputsRow = styled.div` + display: flex; + gap: 0.5rem; + max-width: 100%; +`; + +interface DigitInputProps { + $filled: boolean; +} + +const DigitInput = styled.input` + width: 2.8rem; + height: 3.2rem; + text-align: center; + font-size: 1.5rem; + font-weight: 700; + border: 2px solid ${({ $filled }) => ($filled ? '#aaa' : '#ddd')}; + border-radius: 8px; + outline: none; + caret-color: transparent; + background-color: ${({ $filled }) => ($filled ? '#f8f9fa' : '#fff')}; + transition: border-color 0.15s ease, background-color 0.15s ease; + color: #222; + + &:focus { + border-color: ${({ theme }) => theme.colors.main}; + outline: 3px solid ${({ theme }) => theme.colors.main}33; + outline-offset: -1px; + background-color: #fff; + } + + @media (max-width: 480px) { + width: 2.4rem; + height: 2.9rem; + font-size: 1.3rem; + } + + @media (max-width: 360px) { + width: 2rem; + height: 2.5rem; + font-size: 1.1rem; + } +`; + +const FailureMessage = styled.p` + margin: 0; + font-size: 0.9rem; + color: #ed351c; + text-align: center; +`; + +const Actions = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + width: 100%; +`; + +const ResendButton = styled.button` + background: none; + border: none; + padding: 0; + font-size: 0.85rem; + cursor: pointer; + color: ${({ theme }) => theme.colors.main}; + text-decoration: underline; + + &:disabled { + color: #aaa; + text-decoration: none; + cursor: default; + } +`; + +const ChangeEmailButton = styled.button` + background: none; + border: none; + padding: 0; + font-size: 0.8rem; + cursor: pointer; + color: #777; + text-decoration: underline; + + &:hover { + color: #444; + } +`; + +interface OtpCodeFormProps { + email: string; + submitting: boolean; + failure: boolean; + onSubmit: (code: string) => void; + onResend?: () => void; + onChangeEmail?: () => void; +} + +const OtpCodeForm: React.FC = ({ email, submitting, failure, onSubmit, onResend, onChangeEmail }) => { + const { t } = useTranslation(); + const [digits, setDigits] = useState(Array(CODE_LENGTH).fill('')); + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + const [cooldown, setCooldown] = useState(RESEND_COOLDOWN_SECONDS); + + useEffect(() => { + if (cooldown <= 0) return; + const timer = setTimeout(() => setCooldown(c => c - 1), 1000); + return () => clearTimeout(timer); + }, [cooldown]); + + const focusInput = useCallback((index: number) => { + const el = inputRefs.current[index]; + if (el) { + el.focus(); + el.select(); + } + }, []); + + const handleResend = useCallback(() => { + if (onResend && cooldown === 0) { + onResend(); + setCooldown(RESEND_COOLDOWN_SECONDS); + setDigits(Array(CODE_LENGTH).fill('')); + setTimeout(() => focusInput(0), 0); + } + }, [onResend, cooldown, focusInput]); + + const submitCode = useCallback((code: string) => { + if (code.length === CODE_LENGTH && !submitting) { + onSubmit(code); + } + }, [onSubmit, submitting]); + + const handleChange = useCallback((index: number, value: string) => { + const digit = value.replace(/\D/g, '').slice(-1); + setDigits(prev => { + const next = [...prev]; + next[index] = digit; + if (digit && index < CODE_LENGTH - 1) { + setTimeout(() => focusInput(index + 1), 0); + } + const code = next.join(''); + if (code.length === CODE_LENGTH && next.every(d => d !== '')) { + setTimeout(() => submitCode(code), 0); + } + return next; + }); + }, [focusInput, submitCode]); + + const handleKeyDown = useCallback((index: number, e: React.KeyboardEvent) => { + if (e.key === 'Backspace') { + e.preventDefault(); + if (digits[index]) { + setDigits(prev => { + const next = [...prev]; + next[index] = ''; + return next; + }); + } else if (index > 0) { + setDigits(prev => { + const next = [...prev]; + next[index - 1] = ''; + return next; + }); + focusInput(index - 1); + } + } else if (e.key === 'ArrowLeft' && index > 0) { + focusInput(index - 1); + } else if (e.key === 'ArrowRight' && index < CODE_LENGTH - 1) { + focusInput(index + 1); + } + }, [digits, focusInput]); + + const handlePaste = useCallback((e: React.ClipboardEvent) => { + e.preventDefault(); + const pasted = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, CODE_LENGTH); + if (!pasted) return; + + const next = Array(CODE_LENGTH).fill(''); + for (let i = 0; i < pasted.length; i++) { + next[i] = pasted[i]; + } + setDigits(next); + + const focusIndex = Math.min(pasted.length, CODE_LENGTH - 1); + setTimeout(() => focusInput(focusIndex), 0); + + if (pasted.length === CODE_LENGTH) { + setTimeout(() => submitCode(pasted), 0); + } + }, [focusInput, submitCode]); + + const code = digits.join(''); + const isComplete = code.length === CODE_LENGTH && digits.every(d => d !== ''); + + return ( + + + }} + /> + + + {digits.map((digit, index) => ( + { inputRefs.current[index] = el; }} + type="text" + inputMode="numeric" + pattern="[0-9]*" + maxLength={1} + value={digit} + autoComplete={index === 0 ? 'one-time-code' : 'off'} + autoFocus={index === 0} + disabled={submitting} + $filled={digit !== ''} + onChange={e => handleChange(index, e.target.value)} + onKeyDown={e => handleKeyDown(index, e)} + aria-label={`${t('login.otpDigitLabel')} ${index + 1}`} + data-cy={`otp-digit-${index}`} + /> + ))} + + {failure && {t('login.otpVerificationFailure')}} + +