From 4a6e67a8ed2f7e44134665c850bfd261da855070 Mon Sep 17 00:00:00 2001 From: Pavel Strunkin Date: Sat, 24 Oct 2026 17:17:08 +0300 Subject: [PATCH 1/5] test: check results, not just execution, where logic decides outcomes Mutation testing showed tests that ran this code without checking what it produced. Kept only the ones guarding logic that decides a result: - ignore areas cleared exactly to their edges, and nearest-neighbour scaling, pixel by pixel (compare/utils) - the diff tolerance boundary (equal to the tolerance passes) and size mismatches, in the pixelmatch core and service and in looks-same - stored change-signature validation for bulk approve, and signature shape - users.service hitting Prisma with the user's id (not every user) - role metadata on handler vs class, OIDC role mapping from claims (admin values, separators, non-string claims, nested claims, default role, a missing claim keeping the role) - VLM verdict over pixelmatch status, event queue upkeep on delete, a crashed worker's finished job not re-run Checked by hand: each of "ignore area one row too far", "diff equal to the tolerance fails", "delete without the id filter" passes master's tests and fails these. Co-Authored-By: Claude Opus 5.5 --- src/auth/guards/role.guard.spec.ts | 35 ++++ src/compare/diff-worker-pool.workers.spec.ts | 32 ++++ .../looks-same.service.boundaries.spec.ts | 58 +++++++ .../pixelmatch.core.boundaries.spec.ts | 91 ++++++++++ .../pixelmatch.service.boundaries.spec.ts | 76 +++++++++ .../libs/pixelmatch/signature.core.spec.ts | 14 ++ src/compare/libs/vlm/vlm.service.spec.ts | 14 ++ src/compare/utils/index.spec.ts | 116 ++++++++++++- src/shared/events/events.gateway.spec.ts | 13 ++ src/test-runs/test-runs.service.spec.ts | 66 ++++++++ src/users/oidc/oidcusers.service.spec.ts | 157 ++++++++++++++++++ src/users/users.service.spec.ts | 10 +- 12 files changed, 680 insertions(+), 2 deletions(-) create mode 100644 src/compare/libs/looks-same/looks-same.service.boundaries.spec.ts create mode 100644 src/compare/libs/pixelmatch/pixelmatch.core.boundaries.spec.ts create mode 100644 src/compare/libs/pixelmatch/pixelmatch.service.boundaries.spec.ts diff --git a/src/auth/guards/role.guard.spec.ts b/src/auth/guards/role.guard.spec.ts index 586ebaa5..c00fb5e8 100644 --- a/src/auth/guards/role.guard.spec.ts +++ b/src/auth/guards/role.guard.spec.ts @@ -2,6 +2,7 @@ import { ExecutionContext } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { Role, User } from '@prisma/client'; import { PrismaService } from '../../prisma/prisma.service'; +import { Roles } from '../../shared/roles.decorator'; import { RoleGuard } from './role.guard'; const userWithRole = (role: Role): User => ({ id: '1', role }) as User; @@ -80,4 +81,38 @@ describe('RoleGuard', () => { await expect(guard.canActivate(context(undefined, 'unknown'))).resolves.toBe(false); }); + describe('with the @Roles metadata', () => { + @Roles(Role.admin) + class AdminController { + inherited(): void {} + + @Roles(Role.editor) + overridden(): void {} + } + + const decoratedContext = (handler: () => void, user: User): ExecutionContext => + ({ + switchToHttp: () => ({ getRequest: () => ({ user, header: jest.fn() }) }), + getHandler: () => handler, + getClass: () => AdminController, + }) as unknown as ExecutionContext; + + beforeEach(() => { + guard = new RoleGuard(prismaService as unknown as PrismaService, new Reflector()); + }); + + it('applies the roles declared on the controller class', async () => { + const handler = AdminController.prototype.inherited; + + await expect(guard.canActivate(decoratedContext(handler, userWithRole(Role.editor)))).resolves.toBe(false); + await expect(guard.canActivate(decoratedContext(handler, userWithRole(Role.admin)))).resolves.toBe(true); + }); + + it('lets the roles on the handler override the class', async () => { + const handler = AdminController.prototype.overridden; + + await expect(guard.canActivate(decoratedContext(handler, userWithRole(Role.editor)))).resolves.toBe(true); + await expect(guard.canActivate(decoratedContext(handler, userWithRole(Role.admin)))).resolves.toBe(false); + }); + }); }); diff --git a/src/compare/diff-worker-pool.workers.spec.ts b/src/compare/diff-worker-pool.workers.spec.ts index ffc6ca54..f2526c39 100644 --- a/src/compare/diff-worker-pool.workers.spec.ts +++ b/src/compare/diff-worker-pool.workers.spec.ts @@ -381,6 +381,38 @@ describe('DiffWorkerPool with worker threads', () => { expect(mockWorkers).toHaveLength(3); }); + it('does not run a job again when the worker that already answered it crashes later', async () => { + createPool(); + + const result = pool.run(job(1)); + mockWorkers[0].emit('message', output(1)); + await expect(result).resolves.toEqual(output(1)); + + mockWorkers[0].emit('error', new Error('boom')); + + expect(mockWorkers).toHaveLength(2); + expect(mockWorkers[1].postMessage).not.toHaveBeenCalled(); + }); + + it('takes only the crashed worker out of the idle ones', async () => { + createPool({ workers: '3' }); + + settle(pool.run(job(1))); + const [crashed, idle, busy] = mockWorkers; + expect(busy.postMessage).toHaveBeenCalledWith(job(1)); + + crashed.emit('error', new Error('boom')); + const replacement = mockWorkers[3]; + settle(pool.run(job(2))); + settle(pool.run(job(3))); + settle(pool.run(job(4))); + + expect(replacement.postMessage).toHaveBeenCalledWith(job(2)); + expect(idle.postMessage).toHaveBeenCalledWith(job(3)); + // job 4 waits: the crashed worker is never handed anything + expect(crashed.postMessage).not.toHaveBeenCalled(); + }); + it('ignores a late answer from a worker that was already dropped', async () => { createPool(); diff --git a/src/compare/libs/looks-same/looks-same.service.boundaries.spec.ts b/src/compare/libs/looks-same/looks-same.service.boundaries.spec.ts new file mode 100644 index 00000000..e7889ac1 --- /dev/null +++ b/src/compare/libs/looks-same/looks-same.service.boundaries.spec.ts @@ -0,0 +1,58 @@ +import { Test } from '@nestjs/testing'; +import { TestStatus } from '@prisma/client'; +import { PNG } from 'pngjs'; +import { StaticService } from '../../../static/static.service'; +import { DIFF_DIMENSION_RESULT } from '../consts'; +import { DEFAULT_CONFIG, LookSameService } from './looks-same.service'; + +const filled = (width: number, height: number, value: number): PNG => { + const image = new PNG({ width, height }); + image.data.fill(value); + for (let i = 3; i < image.data.length; i += 4) { + image.data[i] = 255; + } + return image; +}; + +const initService = async (baseline: PNG, image: PNG) => { + const saveImage = jest.fn().mockResolvedValue('diff.png'); + const module = await Test.createTestingModule({ + providers: [ + LookSameService, + { + provide: StaticService, + useValue: { getImage: jest.fn().mockResolvedValueOnce(baseline).mockResolvedValueOnce(image), saveImage }, + }, + ], + }).compile(); + return { service: module.get(LookSameService), saveImage }; +}; + +const input = { baseline: 'baseline', image: 'image', diffTollerancePercent: 0, ignoreAreas: [], saveDiffAsFile: true }; + +describe('LookSameService.getDiff dimensions', () => { + it('reports a different height as a dimension change', async () => { + const { service } = await initService(filled(10, 10, 0), filled(10, 5, 0)); + + await expect(service.getDiff(input, DEFAULT_CONFIG)).resolves.toBe(DIFF_DIMENSION_RESULT); + }); + + it('reports a different width as a dimension change', async () => { + const { service } = await initService(filled(10, 10, 0), filled(5, 10, 0)); + + await expect(service.getDiff(input, DEFAULT_CONFIG)).resolves.toBe(DIFF_DIMENSION_RESULT); + }); +}); + +describe('LookSameService.getDiff saving the diff', () => { + it('stores the diff as a "diff" image', async () => { + const { service, saveImage } = await initService(filled(10, 10, 0), filled(10, 10, 255)); + + const result = await service.getDiff(input, DEFAULT_CONFIG); + + expect(result.status).toBe(TestStatus.unresolved); + expect(result.diffName).toBe('diff.png'); + expect(saveImage).toHaveBeenCalledWith('diff', expect.any(Buffer)); + expect(PNG.sync.read(saveImage.mock.calls[0][1]).width).toBe(10); + }); +}); diff --git a/src/compare/libs/pixelmatch/pixelmatch.core.boundaries.spec.ts b/src/compare/libs/pixelmatch/pixelmatch.core.boundaries.spec.ts new file mode 100644 index 00000000..be814f64 --- /dev/null +++ b/src/compare/libs/pixelmatch/pixelmatch.core.boundaries.spec.ts @@ -0,0 +1,91 @@ +import { PNG } from 'pngjs'; +import { computePixelmatchDiff, PixelmatchJobInput } from './pixelmatch.core'; + +// white image with the first `redPixels` pixels (row by row) painted red +const png = (width: number, height: number, redPixels = 0): Buffer => { + const image = new PNG({ width, height }); + for (let i = 0; i < width * height; i++) { + image.data[i * 4] = 255; + image.data[i * 4 + 1] = i < redPixels ? 0 : 255; + image.data[i * 4 + 2] = i < redPixels ? 0 : 255; + image.data[i * 4 + 3] = 255; + } + return PNG.sync.write(image); +}; + +const job = (baseline: Buffer, image: Buffer, overrides: Partial = {}) => + computePixelmatchDiff({ + kind: 'diff', + baseline, + image, + ignoreAreas: [], + threshold: 0.1, + // count every differing pixel, so the percentages below are exact + includeAA: true, + allowDiffDimensions: false, + diffTolerancePercent: 0, + saveDiff: true, + ...overrides, + }); + +describe('computePixelmatchDiff tolerance boundary', () => { + // 36 of 3600 pixels differ: exactly 1% + const baseline = png(60, 60); + const image = png(60, 60, 36); + + it('reports the mismatch count and percent', () => { + const result = job(baseline, image, { diffTolerancePercent: 1 }); + + expect(result.equal).toBe(false); + expect(result.isSameDimension).toBe(true); + expect(result.pixelMisMatchCount).toBe(36); + expect(result.diffPercent).toBe(1); + }); + + it('does not keep the diff when the percent equals the tolerance', () => { + const result = job(baseline, image, { diffTolerancePercent: 1, withThumbnails: true }); + + expect(result.diffBuffer).toBeUndefined(); + expect(result.imageThumbnail).toBeUndefined(); + expect(result.diffThumbnail).toBeUndefined(); + }); + + it('keeps the diff and thumbnails when the percent is just over the tolerance', () => { + const result = job(baseline, image, { diffTolerancePercent: 0.99, withThumbnails: true }); + + expect(PNG.sync.read(Buffer.from(result.diffBuffer!)).width).toBe(60); + expect(result.imageThumbnail).toBeDefined(); + expect(result.diffThumbnail).toBeDefined(); + }); + + it('does not keep the diff over the tolerance when saving is off', () => { + const result = job(baseline, image, { diffTolerancePercent: 0, saveDiff: false, withThumbnails: true }); + + expect(result.diffBuffer).toBeUndefined(); + expect(result.imageThumbnail).toBeUndefined(); + }); +}); + +describe('computePixelmatchDiff dimensions', () => { + it('stops at a different height when different dimensions are not allowed', () => { + expect(job(png(60, 60), png(60, 30, 1))).toStrictEqual({ equal: false, isSameDimension: false }); + }); + + it('stops at a different width when different dimensions are not allowed', () => { + expect(job(png(60, 60), png(30, 60, 1))).toStrictEqual({ equal: false, isSameDimension: false }); + }); + + it('compares on the larger canvas when different dimensions are allowed', () => { + // the checkpoint is padded with transparent pixels, which pixelmatch blends to white: + // only the baseline's red first row differs + const result = job(png(10, 10, 10), png(10, 5), { allowDiffDimensions: true, withSignature: true }); + + expect(result.equal).toBe(false); + expect(result.isSameDimension).toBe(false); + expect(result.pixelMisMatchCount).toBe(10); + expect(result.diffPercent).toBe(10); + expect(PNG.sync.read(Buffer.from(result.diffBuffer!)).height).toBe(10); + // no signature for screenshots of different sizes + expect(result.signature).toBeUndefined(); + }); +}); diff --git a/src/compare/libs/pixelmatch/pixelmatch.service.boundaries.spec.ts b/src/compare/libs/pixelmatch/pixelmatch.service.boundaries.spec.ts new file mode 100644 index 00000000..f2cc0a2b --- /dev/null +++ b/src/compare/libs/pixelmatch/pixelmatch.service.boundaries.spec.ts @@ -0,0 +1,76 @@ +import { Test } from '@nestjs/testing'; +import { TestStatus } from '@prisma/client'; +import { StaticService } from '../../../static/static.service'; +import { DiffWorkerPool } from '../../diff-worker-pool'; +import { Dinov2Service } from '../dinov2/dinov2.service'; +import { DEFAULT_CONFIG, PixelmatchService } from './pixelmatch.service'; + +const diffBuffer = Buffer.from('diff'); +const imageThumbnail = Buffer.from('image-thumb'); +const diffThumbnail = Buffer.from('diff-thumb'); + +const initService = async (output: Record) => { + const saveImage = jest + .fn() + .mockResolvedValueOnce('diff.png') + .mockResolvedValueOnce('image-thumb.png') + .mockResolvedValueOnce('diff-thumb.png'); + const module = await Test.createTestingModule({ + providers: [ + PixelmatchService, + { provide: DiffWorkerPool, useValue: { run: jest.fn().mockResolvedValue(output) } }, + { provide: Dinov2Service, useValue: { confirmsNoise: jest.fn() } }, + { + provide: StaticService, + useValue: { getImageBuffer: jest.fn().mockResolvedValue(Buffer.from('png')), saveImage }, + }, + ], + }).compile(); + return { service: module.get(PixelmatchService), saveImage }; +}; + +const input = (diffTollerancePercent: number) => ({ + baseline: 'baseline', + image: 'image', + diffTollerancePercent, + ignoreAreas: [], + saveDiffAsFile: true, +}); + +describe('PixelmatchService.getDiff tolerance boundary', () => { + const output = { + equal: false, + isSameDimension: true, + pixelMisMatchCount: 10, + diffPercent: 1, + diffBuffer, + imageThumbnail, + diffThumbnail, + }; + + it('is ok and saves nothing when the percent equals the tolerance', async () => { + const { service, saveImage } = await initService(output); + + const result = await service.getDiff(input(1), DEFAULT_CONFIG); + + expect(result.status).toBe(TestStatus.ok); + expect(result.diffName).toBeNull(); + expect(saveImage).not.toHaveBeenCalled(); + }); + + it('is unresolved and saves the diff and both thumbnails just over the tolerance', async () => { + const { service, saveImage } = await initService(output); + + const result = await service.getDiff(input(0.99), DEFAULT_CONFIG); + + expect(result.status).toBe(TestStatus.unresolved); + expect(result.diffName).toBe('diff.png'); + expect(result.imageThumbnailName).toBe('image-thumb.png'); + expect(result.diffThumbnailName).toBe('diff-thumb.png'); + expect(saveImage.mock.calls).toEqual([ + ['diff', diffBuffer], + ['screenshot', imageThumbnail], + ['diff', diffThumbnail], + ]); + }); +}); diff --git a/src/compare/libs/pixelmatch/signature.core.spec.ts b/src/compare/libs/pixelmatch/signature.core.spec.ts index 73f7abc1..3c48e99c 100644 --- a/src/compare/libs/pixelmatch/signature.core.spec.ts +++ b/src/compare/libs/pixelmatch/signature.core.spec.ts @@ -55,6 +55,20 @@ describe('computeChangeSignature', () => { expect(signatureOf(blank, PNG.sync.write(taller))).toBeNull(); }); + it('has no signature for a checkpoint with the same pixel count but another shape', () => { + const wide = new PNG({ width: WIDTH * 2, height: HEIGHT / 2 }); + wide.data.fill(0); + + expect(signatureOf(blank, PNG.sync.write(wide))).toBeNull(); + }); + + it('has no signature for a checkpoint of another width only', () => { + const wider = new PNG({ width: WIDTH * 2, height: HEIGHT }); + wider.data.fill(0); + + expect(signatureOf(blank, PNG.sync.write(wider))).toBeNull(); + }); + it('has no signature when nothing changed', () => { expect(signatureOf(blank, blank)).toBeNull(); }); diff --git a/src/compare/libs/vlm/vlm.service.spec.ts b/src/compare/libs/vlm/vlm.service.spec.ts index f3c0eb8f..5629797c 100644 --- a/src/compare/libs/vlm/vlm.service.spec.ts +++ b/src/compare/libs/vlm/vlm.service.spec.ts @@ -421,6 +421,20 @@ describe('VlmService', () => { expect(result.vlmDescription).toBe('From thinking'); }); + it('marks the run unresolved when the model sees a difference, whatever status pixelmatch gave', async () => { + const { getImageMock } = createImageMocks(); + const pixelmatchGetDiffMock = jest.fn().mockResolvedValue(createPixelmatchResult({ status: TestStatus.failed })); + const ollamaGenerateMock = jest.fn().mockResolvedValue({ + content: '{"identical": false, "description": "Button moved"}', + }); + const service = await initService({ getImageMock, ollamaGenerateMock, pixelmatchGetDiffMock }); + + const result = await service.getDiff(input, DEFAULT_CONFIG); + + expect(result.status).toBe(TestStatus.unresolved); + expect(result.vlmDescription).toBe('Button moved'); + }); + it('falls back to the content field when thinking is preferred but empty', async () => { const { getImageMock } = createImageMocks(); const pixelmatchGetDiffMock = jest.fn().mockResolvedValue(createPixelmatchResult({})); diff --git a/src/compare/utils/index.spec.ts b/src/compare/utils/index.spec.ts index 6a45147e..b514f537 100644 --- a/src/compare/utils/index.spec.ts +++ b/src/compare/utils/index.spec.ts @@ -2,7 +2,7 @@ import { PNG } from 'pngjs'; import { IgnoreAreaDto } from '../../test-runs/dto/ignore-area.dto'; import { generatePng } from '../../_data_'; import { Logger } from '@nestjs/common'; -import { applyIgnoreAreas, parseConfig } from '.'; +import { applyIgnoreAreas, downscale, parseConfig, scaleImageToSize } from '.'; describe('utils', () => { it.each<[PNG, IgnoreAreaDto[]]>([ @@ -36,3 +36,117 @@ describe('parseConfig', () => { expect(parseConfig(json, defaults, logger)).toStrictEqual(expected); }); }); + +// pixel i of a w x h image gets channels [i, 100 + i, 200 - i, 50 + i] so every channel is distinguishable +const distinctPixels = (width: number, height: number): Buffer => { + const data = Buffer.alloc(width * height * 4); + for (let i = 0; i < width * height; i++) { + data[i * 4] = i; + data[i * 4 + 1] = 100 + i; + data[i * 4 + 2] = 200 - i; + data[i * 4 + 3] = 50 + i; + } + return data; +}; + +const pixelOf = (data: Buffer, index: number): number[] => [...data.subarray(index * 4, index * 4 + 4)]; + +describe('applyIgnoreAreas edges', () => { + it('clears all four channels inside the area and leaves the first row and column after it untouched', () => { + const image = generatePng(3, 3); + + applyIgnoreAreas(image, [{ x: 0, y: 0, width: 2, height: 1 }]); + + expect(pixelOf(image.data, 0)).toEqual([0, 0, 0, 0]); + expect(pixelOf(image.data, 1)).toEqual([0, 0, 0, 0]); + // column right after the area + expect(pixelOf(image.data, 2)).toEqual([255, 255, 255, 255]); + // row right after the area + expect(pixelOf(image.data, 3)).toEqual([255, 255, 255, 255]); + expect(pixelOf(image.data, 4)).toEqual([255, 255, 255, 255]); + }); + + it('clears an area in the middle of the image without touching its surroundings', () => { + const image = generatePng(4, 4); + + applyIgnoreAreas(image, [{ x: 1, y: 1, width: 2, height: 2 }]); + + const cleared = [5, 6, 9, 10]; + for (let i = 0; i < 16; i++) { + expect(pixelOf(image.data, i)).toEqual(cleared.includes(i) ? [0, 0, 0, 0] : [255, 255, 255, 255]); + } + }); +}); + +describe('scaleImageToSize', () => { + it('returns the same image when the requested size equals the image size', () => { + const image = generatePng(3, 2); + + expect(scaleImageToSize(image, 3, 2)).toBe(image); + }); + + it('returns the same image when only the height is requested smaller', () => { + const image = generatePng(3, 3); + + expect(scaleImageToSize(image, 3, 2)).toBe(image); + }); + + it('pads a wider canvas with transparent black, keeping the original pixels top-left', () => { + const image = new PNG({ width: 2, height: 1 }); + distinctPixels(2, 1).copy(image.data); + + const result = scaleImageToSize(image, 3, 1); + + expect(result).not.toBe(image); + expect(result.width).toBe(3); + expect(result.height).toBe(1); + expect([...result.data]).toEqual([0, 100, 200, 50, 1, 101, 199, 51, 0, 0, 0, 0]); + }); + + it('pads a taller canvas with transparent black, keeping the original pixels top-left', () => { + const image = new PNG({ width: 1, height: 1 }); + distinctPixels(1, 1).copy(image.data); + + const result = scaleImageToSize(image, 1, 2); + + expect(result.width).toBe(1); + expect(result.height).toBe(2); + expect([...result.data]).toEqual([0, 100, 200, 50, 0, 0, 0, 0]); + }); +}); + +describe('downscale', () => { + it('returns the source itself when it already fits', () => { + const source = { data: distinctPixels(2, 2), width: 2, height: 2 }; + + expect(downscale(source, 2)).toBe(source); + }); + + it('picks the nearest-neighbour source pixel for every target pixel', () => { + const source = { data: distinctPixels(4, 4), width: 4, height: 4 }; + + const result = downscale(source, 2); + + expect(result.width).toBe(2); + expect(result.height).toBe(2); + expect(result.data.length).toBe(2 * 2 * 4); + // target (x, y) <- source (2x, 2y): source indexes 0, 2, 8, 10 + expect([...result.data]).toEqual([ + ...pixelOf(source.data, 0), + ...pixelOf(source.data, 2), + ...pixelOf(source.data, 8), + ...pixelOf(source.data, 10), + ]); + }); + + it('scales by the longest side and keeps the aspect ratio', () => { + const source = { data: distinctPixels(6, 3), width: 6, height: 3 }; + + const result = downscale(source, 2); + + expect(result.width).toBe(2); + expect(result.height).toBe(1); + // target x 0 <- source x 0, target x 1 <- source x 3 (scale 1/3) + expect([...result.data]).toEqual([...pixelOf(source.data, 0), ...pixelOf(source.data, 3)]); + }); +}); diff --git a/src/shared/events/events.gateway.spec.ts b/src/shared/events/events.gateway.spec.ts index 7139b9ce..32ddae0b 100644 --- a/src/shared/events/events.gateway.spec.ts +++ b/src/shared/events/events.gateway.spec.ts @@ -154,6 +154,19 @@ describe('EventsGateway', () => { expect(emitsOf(emitMock, 'testRun_deleted')).toEqual([[testRunB]]); }); + it('keeps the other queued updates when one updated test run is deleted', async () => { + const { gateway, emitMock } = await initGateway({}); + + gateway.testRunUpdated(testRunA); + gateway.testRunUpdated(testRunB); + gateway.testRunUpdated(testRunC); + gateway.testRunDeleted(testRunB); + + jest.advanceTimersByTime(1500); + + expect(emitsOf(emitMock, 'testRun_updated')).toEqual([[testRunA], [testRunC]]); + }); + it('still emits an empty batch once every queued entry was deleted', async () => { const { gateway, emitMock } = await initGateway({}); diff --git a/src/test-runs/test-runs.service.spec.ts b/src/test-runs/test-runs.service.spec.ts index 420d4eb1..0a869fd7 100644 --- a/src/test-runs/test-runs.service.spec.ts +++ b/src/test-runs/test-runs.service.spec.ts @@ -1579,6 +1579,7 @@ describe('TestRunsService', () => { new Prisma.PrismaClientKnownRequestError('fk', { code: 'P2003', clientVersion: 'test' }), ], ['any other error', new Error('connection lost')], + ['a non-prisma error carrying the P2025 code', Object.assign(new Error('gone'), { code: 'P2025' })], ])('swallows %s and reports the run as deleted', async (_case, error) => { const { built, testRun, eventTestRunDeletedMock } = await initDeleteService(error); @@ -2016,6 +2017,58 @@ describe('TestRunsService', () => { expect(compareGetChangeSignatureMock).toHaveBeenCalledTimes(1); }); + // the default project config is threshold 0.1 with antialiasing ignored + const stamped = (signature: unknown[], stamp = { threshold: 0.1, includeAA: true }) => + JSON.stringify({ ...stamp, signature }); + + it('reuses a stored signature stamped with the current config', async () => { + const testRun = sibling('target', { changeSignature: stamped(SAME_PALETTE) }); + const { built, compareGetChangeSignatureMock } = await initEdgeService({ testRun }); + + await built.getMatchingVariations(testRun.id); + + expect(compareGetChangeSignatureMock).not.toHaveBeenCalled(); + }); + + it.each([ + ['only the threshold changed', { threshold: 0.2, includeAA: true }], + ['only the antialiasing setting changed', { threshold: 0.1, includeAA: false }], + ])('recomputes a stored signature when %s', async (_case, stamp) => { + const testRun = sibling('target', { changeSignature: stamped(SAME_PALETTE, stamp) }); + const { built, compareGetChangeSignatureMock } = await initEdgeService({ testRun }); + + await built.getMatchingVariations(testRun.id); + + expect(compareGetChangeSignatureMock).toHaveBeenCalledTimes(1); + }); + + it.each([ + ['a single value that is not a number', stamped([1, ...new Array(SIGNATURE_LENGTH - 1).fill('nope')])], + ['a value that is not finite', stamped(SAME_PALETTE).replace('[1,', '[1e999,')], + ])('recomputes a stored signature with %s', async (_case, changeSignature) => { + const testRun = sibling('target', { changeSignature }); + const { built, compareGetChangeSignatureMock } = await initEdgeService({ testRun }); + + await built.getMatchingVariations(testRun.id); + + expect(compareGetChangeSignatureMock).toHaveBeenCalledTimes(1); + }); + + it.each([ + ['no stored signature', null], + ['an empty stored signature', ''], + ['a stored json null', 'null'], + ])('computes quietly for %s', async (_case, changeSignature) => { + const testRun = sibling('target', { changeSignature }); + const { built, compareGetChangeSignatureMock } = await initEdgeService({ testRun }); + const warn = jest.spyOn(built['logger'], 'warn').mockImplementation(() => undefined); + + await built.getMatchingVariations(testRun.id); + + expect(warn).not.toHaveBeenCalled(); + expect(compareGetChangeSignatureMock).toHaveBeenCalledTimes(1); + }); + it('does not remember a failed signature', async () => { const testRun = sibling('target'); let calls = 0; @@ -2054,6 +2107,19 @@ describe('TestRunsService', () => { expect(cache.get('key 2000')).toBeDefined(); }); + it('does not take up room for a key that was looked up but never set', () => { + const cache = service['signatureCache']; + for (let index = 0; index < 2000; index++) { + cache.set(`key ${index}`, Promise.resolve([index])); + } + + expect(cache.get('absent')).toBeUndefined(); + // replacing an existing key keeps the size, so nothing is evicted + cache.set('key 0', Promise.resolve([0])); + + expect(cache.get('key 1')).toBeDefined(); + }); + it('keeps an entry that was read recently', () => { const cache = service['signatureCache']; for (let index = 0; index < 2000; index++) { diff --git a/src/users/oidc/oidcusers.service.spec.ts b/src/users/oidc/oidcusers.service.spec.ts index f5873d6e..649fa609 100644 --- a/src/users/oidc/oidcusers.service.spec.ts +++ b/src/users/oidc/oidcusers.service.spec.ts @@ -412,6 +412,163 @@ describe('OidcUsersService', () => { }); }); + describe('configuration', () => { + const configWith = (overrides: Record): ConfigService => + ({ + get: jest.fn((key: string, defaultValue?: string) => + key in overrides ? overrides[key] : configService.get(key, defaultValue) + ), + getOrThrow: configService.getOrThrow, + }) as unknown as ConfigService; + const checks = { state: 'mock-state', codeVerifier: 'mock-verifier' }; + + it.each([ + ['a single admin value', { groups: 'admin' }, Role.admin], + ['values separated by spaces', { groups: 'viewer vrt-admin' }, Role.admin], + ['values separated by commas', { groups: 'viewer,admin' }, Role.admin], + ['admin values in another case', { groups: ['VRT-Admin'] }, Role.admin], + ['values that are only separators', { groups: ' , ' }, Role.editor], + ['object values, which are ignored', { groups: [{ name: 'admin' }] }, Role.editor], + ])('derives the role from %s', async (_case, roleClaims, expected) => { + mockCallback.mockResolvedValue({ + claims: () => ({ email: 'new@example.com', email_verified: true, ...roleClaims }), + }); + findUnique.mockResolvedValue(null); + create.mockImplementation(({ data }) => ({ ...user, ...data })); + + await service.handleCallback({ code: 'code' }, checks); + + expect(create).toHaveBeenCalledWith( + expect.objectContaining({ data: expect.objectContaining({ role: expected }) }) + ); + }); + + it('trims and lower-cases the configured admin values', async () => { + const custom = new OidcUsersService( + configWith({ OIDC_ADMIN_VALUES: ' Ops-Team , ,42' }), + prismaService, + authService + ); + mockCallback.mockResolvedValue({ + claims: () => ({ email: 'new@example.com', email_verified: true, groups: ['ops-team'] }), + }); + findUnique.mockResolvedValue(null); + create.mockImplementation(({ data }) => ({ ...user, ...data })); + + await custom.handleCallback({ code: 'code' }, checks); + + expect(create).toHaveBeenCalledWith( + expect.objectContaining({ data: expect.objectContaining({ role: Role.admin }) }) + ); + }); + + it.each([ + ['a number', 42], + ['a boolean', true], + ])('compares a role claim holding %s as a string', async (_case, value) => { + const custom = new OidcUsersService(configWith({ OIDC_ADMIN_VALUES: String(value) }), prismaService, authService); + mockCallback.mockResolvedValue({ + claims: () => ({ email: 'new@example.com', email_verified: true, groups: [value] }), + }); + findUnique.mockResolvedValue(null); + create.mockImplementation(({ data }) => ({ ...user, ...data })); + + await custom.handleCallback({ code: 'code' }, checks); + + expect(create).toHaveBeenCalledWith( + expect.objectContaining({ data: expect.objectContaining({ role: Role.admin }) }) + ); + }); + + it('gives new users without an admin claim the configured default role', async () => { + const custom = new OidcUsersService(configWith({ OIDC_DEFAULT_ROLE: 'Admin' }), prismaService, authService); + mockCallback.mockResolvedValue({ claims: () => ({ email: 'new@example.com', email_verified: true }) }); + findUnique.mockResolvedValue(null); + create.mockImplementation(({ data }) => ({ ...user, ...data })); + + await custom.handleCallback({ code: 'code' }, checks); + + expect(create).toHaveBeenCalledWith( + expect.objectContaining({ data: expect.objectContaining({ role: Role.admin }) }) + ); + }); + }); + + describe('handleCallback claim sources', () => { + const checks = { state: 'mock-state', codeVerifier: 'mock-verifier' }; + const relaxed = () => + new OidcUsersService( + { + get: jest.fn((key: string, defaultValue?: string) => + key === 'OIDC_REQUIRE_VERIFIED_EMAIL' ? 'false' : configService.get(key, defaultValue) + ), + getOrThrow: configService.getOrThrow, + } as unknown as ConfigService, + prismaService, + authService + ); + + it('rejects a missing email even when unverified emails are allowed', async () => { + mockCallback.mockResolvedValue({ claims: () => ({ sub: 'user-1' }) }); + + await expect(relaxed().handleCallback({ code: 'code' }, checks)).rejects.toThrow('OIDC login failed.'); + + expect(findUnique).not.toHaveBeenCalled(); + }); + + it('warns and keeps the role of an existing user when the role claim is missing', async () => { + mockCallback.mockResolvedValue({ claims: () => ({ email: 'test@example.com', email_verified: true }) }); + findUnique.mockResolvedValue({ ...user, role: Role.admin }); + const warn = jest.spyOn(service['logger'], 'warn').mockImplementation(() => undefined); + + await service.handleCallback({ code: 'code' }, checks); + + expect(warn).toHaveBeenCalledWith("Claim 'groups' not found for 'test@example.com'. Keeping role 'admin'."); + expect(update).not.toHaveBeenCalled(); + }); + + it.each([ + ['a missing parent claim', {}], + ['a null parent claim', { realm_access: null }], + ['a parent claim that is not an object', { realm_access: 'admin' }], + ])('treats a dotted role claim with %s as missing', async (_case, parent) => { + const nested = new OidcUsersService( + { + get: jest.fn((key: string, defaultValue?: string) => + key === 'OIDC_ROLE_CLAIM' ? 'realm_access.length' : configService.get(key, defaultValue) + ), + getOrThrow: configService.getOrThrow, + } as unknown as ConfigService, + prismaService, + authService + ); + mockCallback.mockResolvedValue({ + claims: () => ({ email: 'test@example.com', email_verified: true, ...parent }), + }); + findUnique.mockResolvedValue({ ...user, role: Role.admin }); + + await nested.handleCallback({ code: 'code' }, checks); + + expect(update).not.toHaveBeenCalled(); + }); + + it.each([ + ['another prisma error', new PrismaClientKnownRequestError('fk', { code: 'P2003', clientVersion: '5' })], + ['a non-prisma error', new Error('db down')], + ])('rethrows %s from creating the user without looking it up again', async (_case, error) => { + mockCallback.mockResolvedValue({ claims: () => ({ email: 'race@example.com', email_verified: true }) }); + // a lookup after the failed insert would find the user + findUnique + .mockReset() + .mockResolvedValueOnce(null) + .mockResolvedValue({ ...user, email: 'race@example.com' }); + create.mockRejectedValue(error); + + await expect(service.handleCallback({ code: 'code' }, checks)).rejects.toBe(error); + expect(findUnique).toHaveBeenCalledTimes(1); + }); + }); + describe('non-SSO operations', () => { it('updates the name but never the email of an OIDC user', async () => { update.mockResolvedValue(user); diff --git a/src/users/users.service.spec.ts b/src/users/users.service.spec.ts index 71c5a81a..3ebc4228 100644 --- a/src/users/users.service.spec.ts +++ b/src/users/users.service.spec.ts @@ -162,7 +162,7 @@ describe('UsersService with DbUserService implementation', () => { await service.delete(user.id); // Assert - expect(prismaUserDeleteMock).toHaveBeenCalled(); + expect(prismaUserDeleteMock).toHaveBeenCalledWith({ where: { id: user.id } }); }); describe('deleteMany', () => { @@ -196,6 +196,14 @@ describe('UsersService with DbUserService implementation', () => { // Assert expect(result).toBeInstanceOf(UserDto); + expect(prismaService.user.findUnique).toHaveBeenCalledWith({ where: { id: user.id } }); + }); + + it('looks a user up by id', async () => { + const found = await service.findOne(user.id); + + expect(found).toBe(user); + expect(prismaService.user.findUnique).toHaveBeenCalledWith({ where: { id: user.id } }); }); it('should assign role to user', async () => { From aa75cd7cdf29f38786024593edd745331f6c2b5c Mon Sep 17 00:00:00 2001 From: Pavel Strunkin Date: Sat, 24 Oct 2026 17:20:12 +0300 Subject: [PATCH 2/5] =?UTF-8?q?fix(deps):=20drop=20uuid-apikey,=20bump=20l?= =?UTF-8?q?dapts=20to=209=20=E2=80=94=20no=20vulnerable=20uuid=20left?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit uuid-apikey is unmaintained and pulled in uuid <11.1.1 (GHSA-w5hq-g745-h8pq, no fixed release); ldapts 7 pulled in the same. The app only used uuid-apikey to make random keys and to tell a project id from a name: - API keys and image names come from generateRandomKey(): the same format as before (28 Crockford base32 digits, upper case, no dashes), so keys already issued keep working, from 140 random bits (a v4 UUID had 122) - project lookups keep the same id-or-name check (isUuidShaped); an empty reference is a name now instead of throwing - tests use crypto.randomUUID() ldapts 9 keeps the Client/bind/search/unbind API the LDAP login uses and has no uuid dependency. npm audit --omit=dev no longer lists uuid or ldapts; the remaining @nestjs/core and multer advisories have no fixed NestJS 12 release yet. Co-Authored-By: Claude Opus 5.5 --- package-lock.json | 153 ++------------------------- package.json | 3 +- src/auth/auth.service.spec.ts | 7 +- src/auth/auth.service.ts | 4 +- src/projects/projects.service.ts | 4 +- src/static/utils.spec.ts | 4 +- src/static/utils.ts | 4 +- src/utils/random-key.spec.ts | 39 +++++++ src/utils/random-key.ts | 38 +++++++ test/builds-api.e2e-spec.ts | 18 ++-- test/builds.e2e-spec.ts | 4 +- test/comparators.e2e-spec.ts | 4 +- test/preconditions.ts | 4 +- test/project-favorites.e2e-spec.ts | 12 +-- test/projects.e2e-spec.ts | 4 +- test/static.e2e-spec.ts | 4 +- test/tasks.e2e-spec.ts | 4 +- test/test-runs-review.e2e-spec.ts | 18 ++-- test/test-runs.e2e-spec.ts | 6 +- test/test-variations-api.e2e-spec.ts | 10 +- test/test-variations.e2e-spec.ts | 4 +- test/users-admin.e2e-spec.ts | 6 +- test_acceptance/acceptance.spec.ts | 33 +++--- 23 files changed, 158 insertions(+), 229 deletions(-) create mode 100644 src/utils/random-key.spec.ts create mode 100644 src/utils/random-key.ts diff --git a/package-lock.json b/package-lock.json index 1a0fbb61..e391740f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.15.1", "compression": "^1.8.2", - "ldapts": "^7.1.0", + "ldapts": "^9.2.0", "lodash": "^4.17.21", "looks-same": "^10.0.1", "odiff-bin": "^4.5.0", @@ -47,7 +47,6 @@ "redis": "^4.7.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.2", - "uuid-apikey": "^1.5.3", "zod": "^4.6.7", "zod-to-json-schema": "^3.25.0" }, @@ -4235,15 +4234,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@types/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-V91DSJ2l0h0gRhVP4oBfBzRBN9lAbPUkGDMCnwedqPKX2d84aAMc9CulOvxdw1f7DfEYx99afab+Rsm3e52jhA==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -5824,15 +5814,6 @@ "dev": true, "license": "MIT" }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -6579,15 +6560,6 @@ "dev": true, "license": "MIT" }, - "node_modules/colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -7264,15 +7236,6 @@ "node": ">=14" } }, - "node_modules/encode32": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/encode32/-/encode32-1.1.0.tgz", - "integrity": "sha512-BCmijZ4lWec5+fuGHclf7HSZf+mos2ncQkBjtvomvRWVEGAMI/tw56fuN2x4e+FTgQuTPbZjODPwX80lFy958w==", - "license": "MIT", - "engines": { - "node": ">= 0.4.7" - } - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -9895,37 +9858,15 @@ } }, "node_modules/ldapts": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/ldapts/-/ldapts-7.4.0.tgz", - "integrity": "sha512-QLgx2pLvxMXY1nCc85Fx+cwVJDvC0sQ3l4CJZSl1FJ/iV8Ypfl6m+5xz4lm1lhoXcUlvhPqxEoyIj/8LR6ut+A==", - "license": "MIT", - "dependencies": { - "@types/asn1": ">=0.2.4", - "asn1": "0.2.6", - "debug": "4.4.0", - "strict-event-emitter-types": "2.0.0", - "uuid": "11.1.0", - "whatwg-url": "14.2.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/ldapts/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/ldapts/-/ldapts-9.2.0.tgz", + "integrity": "sha512-5mDg3OT5fDP9DUXga9dCLW8cMW24OTOKT4pv/Qj+f2zCV3qxb+gkZLCzMAqzVTOANoycidKkYpPvkclUQU2aXw==", "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "strict-event-emitter-types": "2.0.0" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=22" } }, "node_modules/leven": { @@ -11562,6 +11503,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -12648,18 +12590,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/ts-essentials": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-10.2.1.tgz", @@ -13044,53 +12974,6 @@ "node": ">= 0.4.0" } }, - "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/uuid-apikey": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/uuid-apikey/-/uuid-apikey-1.5.3.tgz", - "integrity": "sha512-v28vGJ1hRDzqLm6ufZ7b098Kmk159PInIHYWXfB47r3xOACZ5nRIAWe9VxFjvSW0MwckQYAnS1ucWUAXGKo95w==", - "license": "MIT", - "dependencies": { - "colors": "^1.4.0", - "commander": "^8.0.0", - "encode32": "^1.1.0", - "uuid": "^8.3.1" - }, - "bin": { - "apiKeyTool": "bin/apiKeyTool.js" - } - }, - "node_modules/uuid-apikey/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/uuid-apikey/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -13155,34 +13038,12 @@ "node": ">= 8" } }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, "node_modules/whatwg-fetch": { "version": "3.6.20", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", "license": "MIT" }, - "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==", - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 739d4d58..a7135ed8 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.15.1", "compression": "^1.8.2", - "ldapts": "^7.1.0", + "ldapts": "^9.2.0", "lodash": "^4.17.21", "looks-same": "^10.0.1", "odiff-bin": "^4.5.0", @@ -66,7 +66,6 @@ "redis": "^4.7.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.2", - "uuid-apikey": "^1.5.3", "zod": "^4.6.7", "zod-to-json-schema": "^3.25.0" }, diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts index ffebb106..8a5c729c 100644 --- a/src/auth/auth.service.spec.ts +++ b/src/auth/auth.service.spec.ts @@ -2,7 +2,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { JwtService } from '@nestjs/jwt'; import { User } from '@prisma/client'; import { compare } from 'bcryptjs'; -import uuidAPIKey from 'uuid-apikey'; import { AuthService } from './auth.service'; import { UsersService } from '../users/users.service'; @@ -53,11 +52,11 @@ describe('AuthService', () => { }); describe('generateApiKey', () => { - it('returns a valid uuid-apikey without dashes', () => { + it('returns a key in the format keys were always issued in', () => { const apiKey = service.generateApiKey(); - expect(apiKey).toMatch(/^[0-9A-Z]{28}$/); - expect(uuidAPIKey.isAPIKey(apiKey)).toBe(true); + // 28 Crockford base32 digits, upper case, no dashes + expect(apiKey).toMatch(/^[0-9A-HJKMNP-TV-Z]{28}$/); }); it('returns a new key on every call', () => { diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 446b7b05..352d3e75 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; -import uuidAPIKey from 'uuid-apikey'; +import { generateRandomKey } from '../utils/random-key'; import { compare, genSalt, hash } from 'bcryptjs'; import { JwtPayload } from './jwt-payload.model'; import { User } from '@prisma/client'; @@ -15,7 +15,7 @@ export class AuthService { } generateApiKey(): string { - return uuidAPIKey.create({ noDashes: true }).apiKey; + return generateRandomKey(); } async compare(password: string, hashedPassword: string): Promise { diff --git a/src/projects/projects.service.ts b/src/projects/projects.service.ts index fe4bff36..c638e922 100644 --- a/src/projects/projects.service.ts +++ b/src/projects/projects.service.ts @@ -4,7 +4,7 @@ import { BuildsService } from '../builds/builds.service'; import { TestVariationsService } from '../test-variations/test-variations.service'; import { PrismaService } from '../prisma/prisma.service'; import { Project } from '@prisma/client'; -import uuidAPIKey from 'uuid-apikey'; +import { isUuidShaped } from '../utils/random-key'; import { UpdateProjectDto } from './dto/update-project.dto'; @Injectable() @@ -20,7 +20,7 @@ export class ProjectsService { ) {} async findOne(idOrName: string): Promise { - const isUUID = uuidAPIKey.isUUID(idOrName); + const isUUID = isUuidShaped(idOrName); const project: Project | null = await this.prismaService.project.findUnique({ where: { id: isUUID ? idOrName : undefined, diff --git a/src/static/utils.spec.ts b/src/static/utils.spec.ts index a2b4d788..cbdeae63 100644 --- a/src/static/utils.spec.ts +++ b/src/static/utils.spec.ts @@ -1,4 +1,3 @@ -import uuidAPIKey from 'uuid-apikey'; import { generateNewImageName, isHddStaticServiceConfigured, isPngBuffer, isS3ServiceConfigured } from './utils'; describe('static utils', () => { @@ -40,8 +39,7 @@ describe('static utils', () => { const name = generateNewImageName(type); const [key, suffix, extension] = name.split('.'); - expect(uuidAPIKey.isAPIKey(key)).toBe(true); - expect(key).toMatch(/^[0-9A-Z]{28}$/); + expect(key).toMatch(/^[0-9A-HJKMNP-TV-Z]{28}$/); expect(suffix).toBe(type); expect(extension).toBe('png'); }); diff --git a/src/static/utils.ts b/src/static/utils.ts index 20e65b3e..259f7954 100644 --- a/src/static/utils.ts +++ b/src/static/utils.ts @@ -1,4 +1,4 @@ -import uuidAPIKey from 'uuid-apikey'; +import { generateRandomKey } from '../utils/random-key'; export function isHddStaticServiceConfigured() { return !process.env.STATIC_SERVICE || process.env.STATIC_SERVICE === 'hdd'; @@ -9,7 +9,7 @@ export function isS3ServiceConfigured() { } export function generateNewImageName(type: 'screenshot' | 'diff' | 'baseline'): string { - return `${uuidAPIKey.create({ noDashes: true }).apiKey}.${type}.png`; + return `${generateRandomKey()}.${type}.png`; } const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); diff --git a/src/utils/random-key.spec.ts b/src/utils/random-key.spec.ts new file mode 100644 index 00000000..4abfd561 --- /dev/null +++ b/src/utils/random-key.spec.ts @@ -0,0 +1,39 @@ +import { randomUUID } from 'crypto'; +import { generateRandomKey, isUuidShaped } from './random-key'; + +describe('generateRandomKey', () => { + it('has the format keys were always issued in: 28 Crockford base32 digits, upper case', () => { + expect(generateRandomKey()).toMatch(/^[0-9A-HJKMNP-TV-Z]{28}$/); + }); + + it('never repeats', () => { + const keys = new Set(Array.from({ length: 1000 }, generateRandomKey)); + + expect(keys.size).toBe(1000); + }); + + it('uses every digit', () => { + const digits = new Set(Array.from({ length: 200 }, generateRandomKey).join('')); + + expect(digits.size).toBe(32); + }); +}); + +describe('isUuidShaped', () => { + it.each([randomUUID(), randomUUID().toUpperCase(), '00000000-0000-0000-0000-000000000000'])( + 'takes %s for an id', + (id) => { + expect(isUuidShaped(id)).toBe(true); + } + ); + + it.each([ + ['a project name', 'Default project'], + ['an empty string', ''], + ['a key without dashes', generateRandomKey()], + ['dashes in other places', '0000-00000000-0000-0000-000000000000'], + ['a UUID one character short', '00000000-0000-0000-0000-00000000000'], + ])('takes %s for a name', (_, value) => { + expect(isUuidShaped(value)).toBe(false); + }); +}); diff --git a/src/utils/random-key.ts b/src/utils/random-key.ts new file mode 100644 index 00000000..d4f9c91d --- /dev/null +++ b/src/utils/random-key.ts @@ -0,0 +1,38 @@ +import { randomBytes } from 'crypto'; + +// Crockford's base32 digits: no I, L, O or U, so a key read aloud or retyped +// is not mistaken. The alphabet of the keys issued before (uuid-apikey). +const CROCKFORD_DIGITS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; +const KEY_LENGTH = 28; + +/** + * A random 28-character key in the format API keys and image names have + * always had — Crockford base32, upper case, no dashes — so keys issued + * before keep working and clients see no change. Each character carries 5 + * random bits (140 in all, more than the 122 a v4 UUID gave). + */ +export function generateRandomKey(): string { + const bytes = randomBytes(KEY_LENGTH); + let key = ''; + for (const byte of bytes) { + // 256 is a multiple of 32, so taking the low 5 bits keeps every digit equally likely + key += CROCKFORD_DIGITS[byte & 31]; + } + return key; +} + +/** + * Whether a project reference is an id rather than a name: dashes where a + * UUID has its first three, and 32 characters without them. The check + * uuid-apikey made, so references resolve as they did; an empty string is + * simply not a UUID instead of an exception. + */ +export function isUuidShaped(value: string): boolean { + return ( + !!value && + value.charAt(8) === '-' && + value.charAt(13) === '-' && + value.charAt(18) === '-' && + value.replace(/-/g, '').length === 32 + ); +} diff --git a/test/builds-api.e2e-spec.ts b/test/builds-api.e2e-spec.ts index 27904d8c..9c66e54a 100644 --- a/test/builds-api.e2e-spec.ts +++ b/test/builds-api.e2e-spec.ts @@ -1,6 +1,6 @@ import { INestApplication } from '@nestjs/common'; import { Build, Project, TestRun, TestStatus } from '@prisma/client'; -import uuidAPIKey from 'uuid-apikey'; +import { randomUUID } from 'crypto'; import { TEST_PROJECT } from '../src/_data_'; import { BuildDto } from '../src/builds/dto/build.dto'; import { BuildsService } from '../src/builds/builds.service'; @@ -40,7 +40,7 @@ describe('Builds API (e2e)', () => { ...TEST_PROJECT, autoApproveFeature: false, maxBuildAllowed: 100, - name: `Builds API E2E ${uuidAPIKey.create().uuid}`, + name: `Builds API E2E ${randomUUID()}`, }); }); @@ -106,11 +106,11 @@ describe('Builds API (e2e)', () => { }); it('aroundBuildId that holds no page falls back to skip', async () => { - const otherProject = await projectsService.create({ ...TEST_PROJECT, name: `Other ${uuidAPIKey.create().uuid}` }); + const otherProject = await projectsService.create({ ...TEST_PROJECT, name: `Other ${randomUUID()}` }); try { const foreign = await newBuild(undefined, otherProject.id); - for (const aroundBuildId of [uuidAPIKey.create().uuid, foreign.id]) { + for (const aroundBuildId of [randomUUID(), foreign.id]) { const res = await list(`take=2&skip=2&aroundBuildId=${aroundBuildId}`).expect(200); expect(res.body.skip).toBe(2); expect(res.body.data.map((build: BuildDto) => build.id)).toEqual(ordered.slice(2, 4)); @@ -189,7 +189,7 @@ describe('Builds API (e2e)', () => { }); it('404 for an unknown id', async () => { - const id = uuidAPIKey.create().uuid; + const id = randomUUID(); const res = await requestWithAuth(app, 'get', `/builds/${id}`, user.token).expect(404); @@ -234,7 +234,7 @@ describe('Builds API (e2e)', () => { }); it('404 for an unknown id', async () => { - await requestWithAuth(app, 'delete', `/builds/${uuidAPIKey.create().uuid}`, user.token).expect(404); + await requestWithAuth(app, 'delete', `/builds/${randomUUID()}`, user.token).expect(404); }); it('403 for a guest', async () => { @@ -282,9 +282,7 @@ describe('Builds API (e2e)', () => { }); it('404 for an unknown id', async () => { - await requestWithAuth(app, 'patch', `/builds/${uuidAPIKey.create().uuid}/approve?merge=false`, user.token).expect( - 404 - ); + await requestWithAuth(app, 'patch', `/builds/${randomUUID()}/approve?merge=false`, user.token).expect(404); }); it('403 for a guest', async () => { @@ -342,7 +340,7 @@ describe('Builds API (e2e)', () => { }); it('404 for an unknown id', async () => { - await requestWithApiKey(app, 'patch', `/builds/${uuidAPIKey.create().uuid}`, user.apiKey) + await requestWithApiKey(app, 'patch', `/builds/${randomUUID()}`, user.apiKey) .send({ isRunning: false }) .expect(404); }); diff --git a/test/builds.e2e-spec.ts b/test/builds.e2e-spec.ts index f9455613..d4883764 100644 --- a/test/builds.e2e-spec.ts +++ b/test/builds.e2e-spec.ts @@ -10,7 +10,7 @@ import { ProjectsService } from '../src/projects/projects.service'; import { TestRunsService } from '../src/test-runs/test-runs.service'; import { BuildsController } from '../src/builds/builds.controller'; import { TEST_PROJECT } from '../src/_data_'; -import uuidAPIKey from 'uuid-apikey'; +import { randomUUID } from 'crypto'; describe('Builds (e2e)', () => { let app: INestApplication; @@ -42,7 +42,7 @@ describe('Builds (e2e)', () => { user = await haveUserLogged(usersService); project = await projecstService.create({ ...TEST_PROJECT, - name: `Builds E2E test${uuidAPIKey.create().uuid}`, + name: `Builds E2E test${randomUUID()}`, }); }); diff --git a/test/comparators.e2e-spec.ts b/test/comparators.e2e-spec.ts index f7445e9e..05df6494 100644 --- a/test/comparators.e2e-spec.ts +++ b/test/comparators.e2e-spec.ts @@ -4,7 +4,7 @@ import { ImageComparison, Project, TestStatus } from '@prisma/client'; import { existsSync, readFileSync, promises as fs } from 'fs'; import { createServer } from 'net'; import { PNG } from 'pngjs'; -import uuidAPIKey from 'uuid-apikey'; +import { randomUUID } from 'crypto'; import { AppModule } from '../src/app.module'; import { UsersService } from '../src/users/users.service'; import { ProjectsService } from '../src/projects/projects.service'; @@ -105,7 +105,7 @@ describe('Comparators (e2e)', () => { }); const projectBody = (imageComparison: ImageComparison, imageComparisonConfig: string) => ({ - name: `Comparators E2E ${imageComparison} ${uuidAPIKey.create().uuid}`, + name: `Comparators E2E ${imageComparison} ${randomUUID()}`, mainBranchName: 'master', autoApproveFeature: false, imageComparison, diff --git a/test/preconditions.ts b/test/preconditions.ts index c3886f23..26babde6 100644 --- a/test/preconditions.ts +++ b/test/preconditions.ts @@ -1,6 +1,6 @@ import { INestApplication } from '@nestjs/common'; import { UsersService } from 'src/users/users.service'; -import uuidAPIKey from 'uuid-apikey'; +import { generateRandomKey } from '../src/utils/random-key'; import request, { Test } from 'supertest'; import { BuildsService } from 'src/builds/builds.service'; import { TestRunsService } from 'src/test-runs/test-runs.service'; @@ -14,7 +14,7 @@ import { UserLoginResponseDto } from 'src/users/dto/user-login-response.dto'; export type LoggedUser = UserLoginResponseDto & { token: string }; export const generateUser = (password: string): CreateUserDto => ({ - email: `email+${uuidAPIKey.create().apiKey.toLocaleLowerCase()}@example.com`, + email: `email+${generateRandomKey().toLocaleLowerCase()}@example.com`, password, firstName: 'fName', lastName: 'lName', diff --git a/test/project-favorites.e2e-spec.ts b/test/project-favorites.e2e-spec.ts index 53eb8919..2e748249 100644 --- a/test/project-favorites.e2e-spec.ts +++ b/test/project-favorites.e2e-spec.ts @@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, ValidationPipe } from '@nestjs/common'; import { Project, Role } from '@prisma/client'; import request from 'supertest'; -import uuidAPIKey from 'uuid-apikey'; +import { randomUUID } from 'crypto'; import { AppModule } from '../src/app.module'; import { UsersService } from '../src/users/users.service'; import { PrismaService } from '../src/prisma/prisma.service'; @@ -25,7 +25,7 @@ describe('Project favourites (e2e)', () => { }; const haveProject = async (): Promise => { - const project = await projectsService.create({ ...TEST_PROJECT, name: `Favourite ${uuidAPIKey.create().uuid}` }); + const project = await projectsService.create({ ...TEST_PROJECT, name: `Favourite ${randomUUID()}` }); projectIds.push(project.id); return project; }; @@ -58,8 +58,8 @@ describe('Project favourites (e2e)', () => { it.each([ ['get', '/projects/favorites'], - ['put', `/projects/${uuidAPIKey.create().uuid}/favorite`], - ['delete', `/projects/${uuidAPIKey.create().uuid}/favorite`], + ['put', `/projects/${randomUUID()}/favorite`], + ['delete', `/projects/${randomUUID()}/favorite`], ] as const)('401 on %s %s without a token', (method, url) => request(app.getHttpServer())[method](url).expect(401)); it('starts with no favourites', async () => { @@ -101,7 +101,7 @@ describe('Project favourites (e2e)', () => { it('answers 404 for adding an unknown project', async () => { const user = await haveUser(); - await requestWithAuth(app, 'put', `/projects/${uuidAPIKey.create().uuid}/favorite`, user.token).expect(404); + await requestWithAuth(app, 'put', `/projects/${randomUUID()}/favorite`, user.token).expect(404); await expect(favoritesOf(user)).resolves.toEqual([]); }); @@ -109,7 +109,7 @@ describe('Project favourites (e2e)', () => { it('answers 204 for removing an unknown project', async () => { const user = await haveUser(); - await requestWithAuth(app, 'delete', `/projects/${uuidAPIKey.create().uuid}/favorite`, user.token).expect(204); + await requestWithAuth(app, 'delete', `/projects/${randomUUID()}/favorite`, user.token).expect(204); }); it('answers 400 for an id that is not a UUID', async () => { diff --git a/test/projects.e2e-spec.ts b/test/projects.e2e-spec.ts index 7e288246..24d9762e 100644 --- a/test/projects.e2e-spec.ts +++ b/test/projects.e2e-spec.ts @@ -4,11 +4,11 @@ import { AppModule } from '../src/app.module'; import { UsersService } from '../src/users/users.service'; import { requestWithAuth, haveUserLogged, LoggedUser } from './preconditions'; import { ProjectsService } from '../src/projects/projects.service'; -import uuidAPIKey from 'uuid-apikey'; +import { randomUUID } from 'crypto'; import { ProjectDto } from 'src/projects/dto/project.dto'; const project: ProjectDto = { - id: uuidAPIKey.create().uuid, + id: randomUUID(), name: 'Test project', buildsCounter: 0, mainBranchName: 'master', diff --git a/test/static.e2e-spec.ts b/test/static.e2e-spec.ts index c5bb6bbc..28b064ef 100644 --- a/test/static.e2e-spec.ts +++ b/test/static.e2e-spec.ts @@ -3,7 +3,7 @@ import { Project, TestStatus } from '@prisma/client'; import { readFileSync } from 'fs'; import { PNG } from 'pngjs'; import request from 'supertest'; -import uuidAPIKey from 'uuid-apikey'; +import { randomUUID } from 'crypto'; import { TEST_PROJECT } from '../src/_data_'; import { BuildsService } from '../src/builds/builds.service'; import { PrismaService } from '../src/prisma/prisma.service'; @@ -47,7 +47,7 @@ describe('Images (e2e)', () => { project = await projectsService.create({ ...TEST_PROJECT, autoApproveFeature: false, - name: `Images E2E ${uuidAPIKey.create().uuid}`, + name: `Images E2E ${randomUUID()}`, }); }); diff --git a/test/tasks.e2e-spec.ts b/test/tasks.e2e-spec.ts index 238633dc..7d2778f8 100644 --- a/test/tasks.e2e-spec.ts +++ b/test/tasks.e2e-spec.ts @@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import { Project, TestStatus } from '@prisma/client'; import { existsSync } from 'fs'; -import uuidAPIKey from 'uuid-apikey'; +import { randomUUID } from 'crypto'; import { AppModule } from '../src/app.module'; import { CLEANUP_LOCK_KEY, TasksService } from '../src/shared/tasks/tasks.service'; import { PrismaService } from '../src/prisma/prisma.service'; @@ -64,7 +64,7 @@ describe('Tasks: cleanOldTestVariations (e2e)', () => { const haveProject = async (maxBranchLifetime: number): Promise => { const project = await projectsService.create({ ...TEST_PROJECT, - name: `Tasks E2E ${uuidAPIKey.create().uuid}`, + name: `Tasks E2E ${randomUUID()}`, maxBuildAllowed: 100, maxBranchLifetime, autoApproveFeature: false, diff --git a/test/test-runs-review.e2e-spec.ts b/test/test-runs-review.e2e-spec.ts index dce578d0..81cbc18b 100644 --- a/test/test-runs-review.e2e-spec.ts +++ b/test/test-runs-review.e2e-spec.ts @@ -1,6 +1,6 @@ import { INestApplication } from '@nestjs/common'; import { Build, Project, TestStatus } from '@prisma/client'; -import uuidAPIKey from 'uuid-apikey'; +import { randomUUID } from 'crypto'; import { TEST_PROJECT } from '../src/_data_'; import { BuildsService } from '../src/builds/builds.service'; import { PrismaService } from '../src/prisma/prisma.service'; @@ -37,7 +37,7 @@ describe('TestRuns review API (e2e)', () => { ...TEST_PROJECT, autoApproveFeature: false, maxBuildAllowed: 100, - name: `TestRuns review E2E ${uuidAPIKey.create().uuid}`, + name: `TestRuns review E2E ${randomUUID()}`, }); }); @@ -170,7 +170,7 @@ describe('TestRuns review API (e2e)', () => { }); it('404 for an unknown id', async () => { - const id = uuidAPIKey.create().uuid; + const id = randomUUID(); const res = await requestWithAuth(app, 'get', `/test-runs/${id}`, user.token).expect(404); @@ -297,9 +297,7 @@ describe('TestRuns review API (e2e)', () => { }); it('404 for an unknown id', async () => { - await requestWithAuth(app, 'post', `/test-runs/approve?merge=false`, user.token) - .send([uuidAPIKey.create().uuid]) - .expect(404); + await requestWithAuth(app, 'post', `/test-runs/approve?merge=false`, user.token).send([randomUUID()]).expect(404); }); }); @@ -346,7 +344,7 @@ describe('TestRuns review API (e2e)', () => { }); it('ignores ids that do not exist', async () => { - await requestWithAuth(app, 'post', `/test-runs/delete`, user.token).send([uuidAPIKey.create().uuid]).expect(201); + await requestWithAuth(app, 'post', `/test-runs/delete`, user.token).send([randomUUID()]).expect(201); }); it('403 for a guest', async () => { @@ -439,7 +437,7 @@ describe('TestRuns review API (e2e)', () => { it('404 for an unknown id', async () => { await requestWithAuth(app, 'post', `/test-runs/ignoreAreas/update`, user.token) - .send({ ids: [uuidAPIKey.create().uuid], ignoreAreas: [area] }) + .send({ ids: [randomUUID()], ignoreAreas: [area] }) .expect(404); }); }); @@ -655,7 +653,7 @@ describe('TestRuns review API (e2e)', () => { }, 60000); it('404 for an unknown id', async () => { - await matching(uuidAPIKey.create().uuid).expect(404); + await matching(randomUUID()).expect(404); }); it('400 for an id that is not a uuid', async () => { @@ -663,7 +661,7 @@ describe('TestRuns review API (e2e)', () => { }); it('401 without a token', async () => { - await requestWithAuth(app, 'get', `/test-runs/matchingSiblings/${uuidAPIKey.create().uuid}`, '').expect(401); + await requestWithAuth(app, 'get', `/test-runs/matchingSiblings/${randomUUID()}`, '').expect(401); }); }); }); diff --git a/test/test-runs.e2e-spec.ts b/test/test-runs.e2e-spec.ts index 62313fe8..f2125f8e 100644 --- a/test/test-runs.e2e-spec.ts +++ b/test/test-runs.e2e-spec.ts @@ -9,7 +9,7 @@ import { Project, TestStatus } from '@prisma/client'; import { BuildsService } from '../src/builds/builds.service'; import { TestVariationsService } from '../src/test-variations/test-variations.service'; import { TEST_PROJECT } from '../src/_data_'; -import uuidAPIKey from 'uuid-apikey'; +import { randomUUID } from 'crypto'; import { UpdateIgnoreAreasDto } from 'src/test-runs/dto/update-ignore-area.dto'; describe('TestRuns (e2e)', () => { @@ -46,7 +46,7 @@ describe('TestRuns (e2e)', () => { user = await haveUserLogged(usersService); project = await projecstService.create({ ...TEST_PROJECT, - name: `TestRun E2E test${uuidAPIKey.create().uuid}`, + name: `TestRun E2E test${randomUUID()}`, }); }); @@ -237,7 +237,7 @@ describe('TestRuns (e2e)', () => { it('404 for a project that does not exist, storing nothing', async () => { const build = await buildsService.findOrCreate({ projectId: project.id, branchName: project.mainBranchName }); - const projectId = uuidAPIKey.create().uuid; + const projectId = randomUUID(); const res = await requestWithApiKey(app, 'post', url, user.apiKey) .set('Content-type', 'multipart/form-data') diff --git a/test/test-variations-api.e2e-spec.ts b/test/test-variations-api.e2e-spec.ts index 11976750..a680b312 100644 --- a/test/test-variations-api.e2e-spec.ts +++ b/test/test-variations-api.e2e-spec.ts @@ -1,6 +1,6 @@ import { INestApplication } from '@nestjs/common'; import { Baseline, Build, Project, TestStatus } from '@prisma/client'; -import uuidAPIKey from 'uuid-apikey'; +import { randomUUID } from 'crypto'; import { TEST_PROJECT } from '../src/_data_'; import { BuildsService } from '../src/builds/builds.service'; import { PrismaService } from '../src/prisma/prisma.service'; @@ -40,7 +40,7 @@ describe('TestVariations API (e2e)', () => { ...TEST_PROJECT, autoApproveFeature: false, maxBuildAllowed: 100, - name: `TestVariations API E2E ${uuidAPIKey.create().uuid}`, + name: `TestVariations API E2E ${randomUUID()}`, }); }); @@ -75,7 +75,7 @@ describe('TestVariations API (e2e)', () => { it('lists the variations of every branch of the project', async () => { const master = (await haveBaseline(project.mainBranchName, WHITE))!; const feature = (await haveBaseline('feature', CHANGED))!; - const other = await projectsService.create({ ...TEST_PROJECT, name: `Other ${uuidAPIKey.create().uuid}` }); + const other = await projectsService.create({ ...TEST_PROJECT, name: `Other ${randomUUID()}` }); try { await testRunsService.postTestRun({ createTestRequestDto: { @@ -142,7 +142,7 @@ describe('TestVariations API (e2e)', () => { }); it('404 for an unknown id', async () => { - const id = uuidAPIKey.create().uuid; + const id = randomUUID(); const res = await requestWithAuth(app, 'get', `/test-variations/details/${id}`, user.token).expect(404); @@ -278,7 +278,7 @@ describe('TestVariations API (e2e)', () => { }); it('404 for an unknown id', async () => { - await requestWithAuth(app, 'delete', `/test-variations/${uuidAPIKey.create().uuid}`, user.token).expect(404); + await requestWithAuth(app, 'delete', `/test-variations/${randomUUID()}`, user.token).expect(404); }); it('403 for a guest', async () => { diff --git a/test/test-variations.e2e-spec.ts b/test/test-variations.e2e-spec.ts index 957bf57b..9f147aa9 100644 --- a/test/test-variations.e2e-spec.ts +++ b/test/test-variations.e2e-spec.ts @@ -9,7 +9,7 @@ import { Project } from '@prisma/client'; import { BuildsService } from '../src/builds/builds.service'; import { TestVariationsService } from '../src/test-variations/test-variations.service'; import { TEST_PROJECT } from '../src/_data_'; -import uuidAPIKey from 'uuid-apikey'; +import { randomUUID } from 'crypto'; describe('TestVariations (e2e)', () => { let app: INestApplication; @@ -41,7 +41,7 @@ describe('TestVariations (e2e)', () => { user = await haveUserLogged(usersService); project = await projecstService.create({ ...TEST_PROJECT, - name: `TestVariations E2E test${uuidAPIKey.create().uuid}`, + name: `TestVariations E2E test${randomUUID()}`, }); }); diff --git a/test/users-admin.e2e-spec.ts b/test/users-admin.e2e-spec.ts index 96b00322..51d314bc 100644 --- a/test/users-admin.e2e-spec.ts +++ b/test/users-admin.e2e-spec.ts @@ -4,7 +4,7 @@ import { Role, TestStatus } from '@prisma/client'; import request from 'supertest'; import { createHash, generateKeyPairSync, KeyObject, randomUUID, sign } from 'crypto'; import { createServer, IncomingMessage, Server } from 'http'; -import uuidAPIKey from 'uuid-apikey'; +import { generateRandomKey } from '../src/utils/random-key'; import { AppModule } from '../src/app.module'; import { UsersService } from '../src/users/users.service'; import { PrismaService } from '../src/prisma/prisma.service'; @@ -239,7 +239,7 @@ describe('Users admin and RBAC (e2e)', () => { const projectsService = app.get(ProjectsService); const project = await projectsService.create({ ...TEST_PROJECT, - name: `Users admin E2E ${uuidAPIKey.create().uuid}`, + name: `Users admin E2E ${randomUUID()}`, }); try { const { testRun } = await haveTestRunCreated( @@ -422,7 +422,7 @@ describe('Users OIDC (e2e)', () => { }); const newEmail = () => { - const email = `oidc+${uuidAPIKey.create().apiKey.toLowerCase()}@example.com`; + const email = `oidc+${generateRandomKey().toLowerCase()}@example.com`; emails.push(email); return email; }; diff --git a/test_acceptance/acceptance.spec.ts b/test_acceptance/acceptance.spec.ts index ccda8ce4..43b2ab0e 100644 --- a/test_acceptance/acceptance.spec.ts +++ b/test_acceptance/acceptance.spec.ts @@ -1,30 +1,29 @@ import { User } from '@prisma/client'; -import axios from 'axios' +import axios from 'axios'; import { UserLoginRequestDto } from 'src/users/dto/user-login-request.dto'; import { UserLoginResponseDto } from 'src/users/dto/user-login-response.dto'; -import uuidAPIKey from 'uuid-apikey'; +import { randomUUID } from 'crypto'; axios.defaults.baseURL = 'http://localhost:4200'; let user: Partial = { - email: `${uuidAPIKey.create().uuid}@example.com`, - password: '123456', - firstName: 'fName', - lastName: 'lName', + email: `${randomUUID()}@example.com`, + password: '123456', + firstName: 'fName', + lastName: 'lName', }; const loginData: UserLoginRequestDto = { - email: user.email!, - password: user.password!, -} + email: user.email!, + password: user.password!, +}; describe('Acceptance', () => { + test('Register and login', async () => { + await axios.post('/users/register', user); - test('Register and login', async () => { - await axios.post('/users/register', user); - - const response = await axios.post('/users/login', loginData) + const response = await axios.post('/users/login', loginData); - expect(response.status).toBe(201) - expect(response.data.token).toBeDefined() - }) -}) \ No newline at end of file + expect(response.status).toBe(201); + expect(response.data.token).toBeDefined(); + }); +}); From 503aafb3a6f6b7f687bb62f629e7fc940258c7fe Mon Sep 17 00:00:00 2001 From: Pavel Strunkin Date: Sat, 24 Oct 2026 17:46:06 +0300 Subject: [PATCH 3/5] =?UTF-8?q?build:=20compile=20with=20SWC=20=E2=80=94?= =?UTF-8?q?=205x=20faster=20builds=20and=20watch=20rebuilds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nest build (and start:dev on every change) goes from ~2.5s to ~0.5s (115 files in ~50ms). Unit tests run through @swc/jest as well, so they exercise the same output that ships, app.module.spec's DI wiring included; e2e stays on ts-jest, which measured faster there (1:02 vs 1:31). The type check stays a separate step (`npm run typecheck`). What SWC needed: - WrapperType on the eight forwardRef-injected constructor parameters: with circular imports the class is not initialised when the decorator metadata is emitted; tsc's output read it as undefined, SWC's live bindings throw. forwardRef still hands Nest the class. - VlmComparisonResultSchema in its own module: the providers imported it from vlm.service, which imports the providers — a cycle that only worked when vlm.service happened to load first. - compare.service.spec mocks static/utils instead of spying on it (SWC's exports are getters jest.spyOn cannot replace). - coverage by v8 on the TypeScript source (istanbul on SWC output counts decorator helpers as branches), type-only files excluded from it. Checked: 1043 unit and all 247 e2e tests pass under SWC. Co-Authored-By: Claude Opus 5.5 --- .swcrc | 13 + Dockerfile | 2 +- nest-cli.json | 7 +- package-lock.json | 3355 +++++++++++++---- package.json | 3 + src/builds/builds.controller.ts | 3 +- src/builds/builds.service.ts | 3 +- src/compare/compare.service.spec.ts | 15 +- .../vlm/providers/gemini/gemini.service.ts | 2 +- .../vlm/providers/ollama/ollama.service.ts | 2 +- src/compare/libs/vlm/vlm-result.schema.ts | 9 + src/compare/libs/vlm/vlm.service.ts | 7 +- src/jest.config.ts | 24 +- src/projects/projects.service.ts | 5 +- src/shared/tasks/tasks.service.ts | 3 +- src/shared/wrapper-type.ts | 8 + src/test-runs/test-runs.service.ts | 3 +- .../test-variations.service.ts | 5 +- 18 files changed, 2790 insertions(+), 679 deletions(-) create mode 100644 .swcrc create mode 100644 src/compare/libs/vlm/vlm-result.schema.ts create mode 100644 src/shared/wrapper-type.ts diff --git a/.swcrc b/.swcrc new file mode 100644 index 00000000..742ad40e --- /dev/null +++ b/.swcrc @@ -0,0 +1,13 @@ +{ + "$schema": "https://swc.rs/schema.json", + "sourceMaps": true, + "module": { "type": "commonjs" }, + "jsc": { + "target": "es2023", + "parser": { "syntax": "typescript", "decorators": true, "dynamicImport": true }, + "transform": { "legacyDecorator": true, "decoratorMetadata": true, "useDefineForClassFields": false }, + "keepClassNames": true, + "baseUrl": "./", + "paths": { "src/*": ["./src/*"] } + } +} diff --git a/Dockerfile b/Dockerfile index bfac584b..98cd02cd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ COPY package*.json ./ ENV NODE_TLS_REJECT_UNAUTHORIZED='0' RUN npm ci -COPY tsconfig*.json nest-cli.json ./ +COPY tsconfig*.json nest-cli.json .swcrc ./ COPY src ./src # Dev dependencies are only needed to build; the generated Prisma client diff --git a/nest-cli.json b/nest-cli.json index f9aa683b..bc46791e 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -3,6 +3,11 @@ "collection": "@nestjs/schematics", "sourceRoot": "src", "compilerOptions": { - "deleteOutDir": true + "deleteOutDir": true, + "builder": { + "type": "swc", + "options": { "swcrcPath": ".swcrc" } + }, + "typeCheck": false } } diff --git a/package-lock.json b/package-lock.json index e391740f..049e75d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,9 @@ "@nestjs/cli": "^12.0.0", "@nestjs/schematics": "^12.0.0", "@nestjs/testing": "^12.0.0", + "@swc/cli": "^0.8.1", + "@swc/core": "^1.16.20", + "@swc/jest": "^0.2.39", "@types/bcryptjs": "^2.4.4", "@types/compression": "^1.8.1", "@types/express": "^5.0.6", @@ -2158,6 +2161,19 @@ "node": ">=8" } }, + "node_modules/@jest/create-cache-key-function": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-30.5.1.tgz", + "integrity": "sha512-5qkif//qhlbt54255O/M2JtUZNlnefOzE12bi7FlApOIYO4pqrZcAwhNCfP4jOGFubez8uoLgY9czWFjYY57ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.5.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/diff-sequences": { "version": "30.5.0", "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.5.0.tgz", @@ -2506,6 +2522,13 @@ "integrity": "sha512-C10pc+0H6j0h8fENOfnGOvkXCmvpSQTDGlfGd0sHphZhPSGTyLjIrHba0FaZZdsKqA/wlmhYicUHb92vfZphaw==", "license": "Apache-2.0" }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, "node_modules/@lukeed/csprng": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", @@ -2521,222 +2544,566 @@ "integrity": "sha512-VHyYyyoH63jFoO3shpw9Zfijk5v/9p/VrClW35z0/q6ooK7sSmxpzOkS4iZPxs3vO4XtBW4H38HRAKkAMIK7DA==", "license": "MIT" }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.5.tgz", - "integrity": "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==", + "node_modules/@napi-rs/nice": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", + "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + "node": ">= 10" }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" }, - "peerDependencies": { - "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", - "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" - } - }, - "node_modules/@nestjs/cli": { - "version": "12.0.10", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-12.0.10.tgz", - "integrity": "sha512-sFtiyJaiZe2cPqFCLhgzje5lnU1ANaGuDeaOClWMkECRC20W2m6NkqG6gnUgv+nspejIVyTG9yIyn4mXGJ9Y9g==", + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.1.1", + "@napi-rs/nice-android-arm64": "1.1.1", + "@napi-rs/nice-darwin-arm64": "1.1.1", + "@napi-rs/nice-darwin-x64": "1.1.1", + "@napi-rs/nice-freebsd-x64": "1.1.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/nice-linux-arm64-gnu": "1.1.1", + "@napi-rs/nice-linux-arm64-musl": "1.1.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", + "@napi-rs/nice-linux-s390x-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-musl": "1.1.1", + "@napi-rs/nice-openharmony-arm64": "1.1.1", + "@napi-rs/nice-win32-arm64-msvc": "1.1.1", + "@napi-rs/nice-win32-ia32-msvc": "1.1.1", + "@napi-rs/nice-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@angular-devkit/core": "22.2.3", - "@angular-devkit/schematics": "22.2.3", - "@angular-devkit/schematics-cli": "22.2.3", - "@inquirer/prompts": "8.7.3", - "@nestjs/schematics": "^12.0.0", - "ansis": "4.4.0", - "chokidar": "5.0.0", - "cli-table3": "0.6.5", - "commander": "15.0.0", - "jsonc-parser": "3.3.1", - "minimatch": "10.2.6", - "node-emoji": "2.2.0", - "ora": "9.4.1", - "tsconfig-paths": "4.2.0", - "typescript": "~6.0.2" - }, - "bin": { - "nest": "bin/nest.js" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">= 20.11" - }, - "peerDependencies": { - "@rspack/core": "^1.7.7 || ^2.1.10", - "@swc/cli": "^0.8.0", - "@swc/core": "^1.15.18", - "fork-ts-checker-webpack-plugin": "^9.1.0", - "ts-loader": "^9.5.4", - "tsconfig-paths-webpack-plugin": "^4.2.0", - "webpack": "^5.105.4", - "webpack-node-externals": "^3.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "@swc/cli": { - "optional": true - }, - "@swc/core": { - "optional": true - }, - "fork-ts-checker-webpack-plugin": { - "optional": true - }, - "ts-loader": { - "optional": true - }, - "tsconfig-paths-webpack-plugin": { - "optional": true - }, - "webpack": { - "optional": true - }, - "webpack-node-externals": { - "optional": true - } + "node": ">= 10" } }, - "node_modules/@nestjs/cli/node_modules/@angular-devkit/core": { - "version": "22.2.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-22.2.3.tgz", - "integrity": "sha512-bi1oBjJeg1XCgRrBYSuJikaJV4sO/mcnyFE/0a1H646yHhQpVYoU/M2Vh69Qv8h+YoMfC7RuSxT4LZHaubhhLw==", + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", + "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "ajv": "8.20.0", - "ajv-formats": "3.0.1", - "jsonc-parser": "3.3.1", - "picomatch": "4.0.7", - "rxjs": "7.8.2", - "source-map": "0.8.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^22.22.3 || ^24.15.0 || >=26.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^5.0.0" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } + "node": ">= 10" } }, - "node_modules/@nestjs/common": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-12.2.0.tgz", - "integrity": "sha512-xC4tUL31tQ2eoZcHheHIjnCrhnqyAEDRMGGHMUrYsOdh0Kg1DvDlLn7zelisErMcpaNanSIApJH6m9AcnK3fQQ==", + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@standard-schema/spec": "1.1.0", - "file-type": "22.1.1", - "iterare": "1.2.1", - "load-esm": "1.0.3", - "tslib": "2.8.1", - "uid": "2.0.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nest" - }, - "peerDependencies": { - "class-transformer": ">=0.4.1", - "class-validator": ">=0.13.2", - "reflect-metadata": "^0.1.12 || ^0.2.0", - "rxjs": "^7.1.0" - }, - "peerDependenciesMeta": { - "class-transformer": { - "optional": true - }, - "class-validator": { - "optional": true - } + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@nestjs/config": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-12.0.1.tgz", - "integrity": "sha512-pxqEtULFi4qXda0TqyR2YFq7o4UoRa7h4K5H0lgFuBMrUGr6x6VVldaJJrowFyh4uvp8dNq9Mie3T+l+uEOTfw==", + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", + "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@standard-schema/spec": "1.1.0", - "dotenv": "18.0.3", - "dotenv-expand": "13.0.0", - "es-toolkit": "1.52.0" - }, - "peerDependencies": { - "@nestjs/common": "^11.0.0 || ^12.0.0", - "rxjs": "^7.1.0" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@nestjs/config/node_modules/dotenv": { - "version": "18.0.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.3.tgz", - "integrity": "sha512-/8CJTyL817qPnfbQrJgOEcrU3fDrJkRIoXBE566wHuvIXKOCHUZhPqGPbCPi9SogjdGnreF0TlvnTgK6J/8zpA==", - "license": "BSD-2-Clause", - "bin": { - "dotenv": "dist/index.cjs" - }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" + "node": ">= 10" } }, - "node_modules/@nestjs/core": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-12.2.0.tgz", - "integrity": "sha512-OTNlRALw/xlSJ/cXeIo7HzY/TqE9RSyv6uFS+OY41dDelDwMqnAjpRFiN5RVGOI/R4wAhaD0TJVjsfOrpZJCjA==", + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "fast-safe-stringify": "2.1.1", - "iterare": "1.2.1", - "path-to-regexp": "8.4.2", - "tslib": "2.8.1", - "uid": "2.0.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 20" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nest" - }, - "peerDependencies": { - "@nestjs/common": "^12.0.0", - "@nestjs/microservices": "^12.0.0", - "@nestjs/platform-express": "^12.0.0", - "@nestjs/websockets": "^12.0.0", - "reflect-metadata": "^0.1.12 || ^0.2.0", - "rxjs": "^7.1.0" - }, - "peerDependenciesMeta": { - "@nestjs/microservices": { - "optional": true - }, - "@nestjs/platform-express": { - "optional": true - }, - "@nestjs/websockets": { - "optional": true - } + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", + "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-openharmony-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", + "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.5.tgz", + "integrity": "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, + "node_modules/@nestjs/cli": { + "version": "12.0.10", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-12.0.10.tgz", + "integrity": "sha512-sFtiyJaiZe2cPqFCLhgzje5lnU1ANaGuDeaOClWMkECRC20W2m6NkqG6gnUgv+nspejIVyTG9yIyn4mXGJ9Y9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "22.2.3", + "@angular-devkit/schematics": "22.2.3", + "@angular-devkit/schematics-cli": "22.2.3", + "@inquirer/prompts": "8.7.3", + "@nestjs/schematics": "^12.0.0", + "ansis": "4.4.0", + "chokidar": "5.0.0", + "cli-table3": "0.6.5", + "commander": "15.0.0", + "jsonc-parser": "3.3.1", + "minimatch": "10.2.6", + "node-emoji": "2.2.0", + "ora": "9.4.1", + "tsconfig-paths": "4.2.0", + "typescript": "~6.0.2" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 20.11" + }, + "peerDependencies": { + "@rspack/core": "^1.7.7 || ^2.1.10", + "@swc/cli": "^0.8.0", + "@swc/core": "^1.15.18", + "fork-ts-checker-webpack-plugin": "^9.1.0", + "ts-loader": "^9.5.4", + "tsconfig-paths-webpack-plugin": "^4.2.0", + "webpack": "^5.105.4", + "webpack-node-externals": "^3.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "fork-ts-checker-webpack-plugin": { + "optional": true + }, + "ts-loader": { + "optional": true + }, + "tsconfig-paths-webpack-plugin": { + "optional": true + }, + "webpack": { + "optional": true + }, + "webpack-node-externals": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@angular-devkit/core": { + "version": "22.2.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-22.2.3.tgz", + "integrity": "sha512-bi1oBjJeg1XCgRrBYSuJikaJV4sO/mcnyFE/0a1H646yHhQpVYoU/M2Vh69Qv8h+YoMfC7RuSxT4LZHaubhhLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.20.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.7", + "rxjs": "7.8.2", + "source-map": "0.8.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@nestjs/common": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-12.2.0.tgz", + "integrity": "sha512-xC4tUL31tQ2eoZcHheHIjnCrhnqyAEDRMGGHMUrYsOdh0Kg1DvDlLn7zelisErMcpaNanSIApJH6m9AcnK3fQQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "1.1.0", + "file-type": "22.1.1", + "iterare": "1.2.1", + "load-esm": "1.0.3", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": ">=0.4.1", + "class-validator": ">=0.13.2", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/config": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-12.0.1.tgz", + "integrity": "sha512-pxqEtULFi4qXda0TqyR2YFq7o4UoRa7h4K5H0lgFuBMrUGr6x6VVldaJJrowFyh4uvp8dNq9Mie3T+l+uEOTfw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "1.1.0", + "dotenv": "18.0.3", + "dotenv-expand": "13.0.0", + "es-toolkit": "1.52.0" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0 || ^12.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/config/node_modules/dotenv": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.3.tgz", + "integrity": "sha512-/8CJTyL817qPnfbQrJgOEcrU3fDrJkRIoXBE566wHuvIXKOCHUZhPqGPbCPi9SogjdGnreF0TlvnTgK6J/8zpA==", + "license": "BSD-2-Clause", + "bin": { + "dotenv": "dist/index.cjs" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@nestjs/core": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-12.2.0.tgz", + "integrity": "sha512-OTNlRALw/xlSJ/cXeIo7HzY/TqE9RSyv6uFS+OY41dDelDwMqnAjpRFiN5RVGOI/R4wAhaD0TJVjsfOrpZJCjA==", + "license": "MIT", + "dependencies": { + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "8.4.2", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^12.0.0", + "@nestjs/microservices": "^12.0.0", + "@nestjs/platform-express": "^12.0.0", + "@nestjs/websockets": "^12.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } } }, "node_modules/@nestjs/jwt": { @@ -3942,218 +4309,611 @@ "@redis/client": "^1.0.0" } }, - "node_modules/@redis/client": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", - "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", + "node_modules/@redis/client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", + "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.52", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", + "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@smithy/core": { + "version": "3.36.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.36.2.tgz", + "integrity": "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz", + "integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz", + "integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz", + "integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.4.tgz", + "integrity": "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.19.0.tgz", + "integrity": "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@socket.io/redis-adapter": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@socket.io/redis-adapter/-/redis-adapter-8.3.0.tgz", + "integrity": "sha512-ly0cra+48hDmChxmIpnESKrc94LjRL80TEmZVscuQ/WWkRP81nNj8W8cCGMqbI4L6NCuAaPRSzZF1a9GlAxxnA==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.1", + "notepack.io": "~3.0.1", + "uid2": "1.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "socket.io-adapter": "^2.5.4" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@swc/cli": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@swc/cli/-/cli-0.8.1.tgz", + "integrity": "sha512-L+ACCGHCiS0VqHVep/INLVnvRvJ2XooQFLZq4L8snhxw1jsqz+XRcY313UsyPVturPPE1shW3jic7rt3qEQTSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@swc/counter": "^0.1.3", + "@xhmikosr/bin-wrapper": "^14.0.0", + "commander": "^8.3.0", + "minimatch": "^9.0.3", + "piscina": "^4.3.1", + "semver": "^7.3.8", + "slash": "3.0.0", + "source-map": "^0.7.3", + "tinyglobby": "^0.2.13" + }, + "bin": { + "spack": "bin/spack.js", + "swc": "bin/swc.js", + "swcx": "bin/swcx.js" + }, + "engines": { + "node": ">= 20.19.0" + }, + "peerDependencies": { + "@swc/core": "^1.2.66", + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@swc/cli/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/cli/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@swc/cli/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@swc/cli/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", "dependencies": { - "cluster-key-slot": "1.1.2", - "generic-pool": "3.9.0", - "yallist": "4.0.0" + "brace-expansion": "^2.0.2" }, "engines": { - "node": ">=14" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@redis/client/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/@redis/graph": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", - "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" + "node_modules/@swc/cli/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" } }, - "node_modules/@redis/json": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", - "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", - "license": "MIT", + "node_modules/@swc/core": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.16.20.tgz", + "integrity": "sha512-zcdfS7k28OJIMuLhqRRyGja9PxrC2GXhFXkbQ1X15y6uZEMsTrqyej55V1DiJtlXaDOmwSLn5222YPT9ZoFxPA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.28" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.16.20", + "@swc/core-darwin-x64": "1.16.20", + "@swc/core-linux-arm-gnueabihf": "1.16.20", + "@swc/core-linux-arm64-gnu": "1.16.20", + "@swc/core-linux-arm64-musl": "1.16.20", + "@swc/core-linux-ppc64-gnu": "1.16.20", + "@swc/core-linux-s390x-gnu": "1.16.20", + "@swc/core-linux-x64-gnu": "1.16.20", + "@swc/core-linux-x64-musl": "1.16.20", + "@swc/core-win32-arm64-msvc": "1.16.20", + "@swc/core-win32-ia32-msvc": "1.16.20", + "@swc/core-win32-x64-msvc": "1.16.20" + }, "peerDependencies": { - "@redis/client": "^1.0.0" + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } } }, - "node_modules/@redis/search": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", - "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" + "node_modules/@swc/core-darwin-arm64": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.16.20.tgz", + "integrity": "sha512-i7NFSDnuE6tdBvBgmoVKKmburgcwZt/FZK2IusJaVGrjFrMUC1hrgE3RI+Kt9RhYM7vHvQj0W9j53zSxKEMbWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" } }, - "node_modules/@redis/time-series": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", - "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" + "node_modules/@swc/core-darwin-x64": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.16.20.tgz", + "integrity": "sha512-ho/LkT1Q3beSBeZGNZnm1mVbh5bKZHC0UGHtK3MswqOylbHtZoMXJBL40N8JpDj5SBxwECmdNEyXnU9HJ4F6RA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" } }, - "node_modules/@scarf/scarf": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", - "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", - "hasInstallScript": true, - "license": "Apache-2.0" - }, - "node_modules/@sinclair/typebox": { - "version": "0.34.52", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", - "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.16.20.tgz", + "integrity": "sha512-xm3/s3zp9WyNgB1Sl78tpH76MHgobI3TSenTY15bkSHyrdQa9W2MUSESHRPd7CHGFLfpWl/0jwwNp5+hwG0e9A==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.16.20.tgz", + "integrity": "sha512-e6Um0U+VcTQyurOXhbratRQlrPITSRwugG00fhICbi2MGTi1vRiE4TMqGCjWpR1zPmZKXDGMw+yNFvh7AAgT2Q==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.16.20.tgz", + "integrity": "sha512-ekCO2obfK9NFwXGHfAQjjuU5mLDDC7qMaUJH/V/4iyzD0BqNhilr6McZrXLytOQ/xKtTNxLQ71wYrD3M2LW8Kg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" } }, - "node_modules/@sinonjs/fake-timers": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", - "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.16.20.tgz", + "integrity": "sha512-6/x2K3l4GNUWyaSmkywnth6ipZISzhVzOLsI66bj12/hSQYaKAyg9MKTwnuFf2B/iYA6GrjDghk36RYghRameQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" } }, - "node_modules/@smithy/core": { - "version": "3.36.2", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.36.2.tgz", - "integrity": "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.19.0", - "tslib": "^2.6.2" - }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.16.20.tgz", + "integrity": "sha512-1GTd8sqS/NpcF+Y3YDSE0bT31V61+hD7ZZWEsVyiyxk/HQt2zpk4zlJJHOASBLc+/CyUuBZ949n6QMszbIXkmQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.0.0" + "node": ">=10" } }, - "node_modules/@smithy/credential-provider-imds": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz", - "integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "@smithy/types": "^4.17.2", - "tslib": "^2.6.2" - }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.16.20.tgz", + "integrity": "sha512-XtMeYAgQa5UZhAX84TXn0sXPmhf1xHwcv0cl5hHiBXxTVbplY7WiTwIQU9uOtVm3VUqL/quyCm78/umUNZ7FmA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.0.0" + "node": ">=10" } }, - "node_modules/@smithy/fetch-http-handler": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz", - "integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.3", - "@smithy/types": "^4.18.0", - "tslib": "^2.6.2" - }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.16.20.tgz", + "integrity": "sha512-qeF3UILa+MA4zJ29CdQ8KE7YbGFPMQBxWo3Lh0Ed0ETFEJ8pCKISWo7wQG4/SmfM9HX4KT1Odoj6ixyBn6Ji2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.0.0" + "node": ">=10" } }, - "node_modules/@smithy/node-http-handler": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz", - "integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.3", - "@smithy/types": "^4.18.0", - "tslib": "^2.6.2" - }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.16.20.tgz", + "integrity": "sha512-ciMtHXAmE+jnrWWO14rErr6tfK34QZm8jZRSdRLPodqJlzrYDw7TqN60HC8Yim5Ua9a1r/poyfkpNft6GRXvxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.0.0" + "node": ">=10" } }, - "node_modules/@smithy/signature-v4": { - "version": "5.7.4", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.4.tgz", - "integrity": "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.35.0", - "@smithy/types": "^4.19.0", - "tslib": "^2.6.2" - }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.16.20.tgz", + "integrity": "sha512-MTbkgGooWebmjmATx1+QRECVZ+q4ICKNXCutwOrFVD5b5hdxla/yV4e6cRFrfNMowejaNpmMQg31ENWt0Yfzpg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.0.0" + "node": ">=10" } }, - "node_modules/@smithy/types": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.19.0.tgz", - "integrity": "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.16.20.tgz", + "integrity": "sha512-Lf5oMaSdJBImtGhDecasxcIa7Z5LeJ4aGmBOY7TScroO+A/MxxB9PslIpgV0rtG/FcxtipN7hveBg9ZTeboaaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.0.0" + "node": ">=10" } }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "license": "MIT" + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" }, - "node_modules/@socket.io/redis-adapter": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@socket.io/redis-adapter/-/redis-adapter-8.3.0.tgz", - "integrity": "sha512-ly0cra+48hDmChxmIpnESKrc94LjRL80TEmZVscuQ/WWkRP81nNj8W8cCGMqbI4L6NCuAaPRSzZF1a9GlAxxnA==", + "node_modules/@swc/jest": { + "version": "0.2.39", + "resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.39.tgz", + "integrity": "sha512-eyokjOwYd0Q8RnMHri+8/FS1HIrIUKK/sRrFp8c1dThUOfNeCWbLmBP1P5VsKdvmkd25JaH+OKYwEYiAYg9YAA==", + "dev": true, "license": "MIT", "dependencies": { - "debug": "~4.3.1", - "notepack.io": "~3.0.1", - "uid2": "1.0.0" + "@jest/create-cache-key-function": "^30.0.0", + "@swc/counter": "^0.1.3", + "jsonc-parser": "^3.2.0" }, "engines": { - "node": ">=10.0.0" + "npm": ">= 7.0.0" }, "peerDependencies": { - "socket.io-adapter": "^2.5.4" + "@swc/core": "*" } }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" + "node_modules/@swc/types": { + "version": "0.1.28", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.28.tgz", + "integrity": "sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } }, "node_modules/@tokenizer/inflate": { "version": "0.4.1", @@ -4462,6 +5222,13 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -5163,445 +5930,793 @@ "license": "MIT", "optional": true, "os": [ - "darwin" + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" ] }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", - "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ - "freebsd" + "linux" ] }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "node_modules/@unrs/resolver-binding-linux-x64-musl": { "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", - "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", "cpu": [ - "arm" + "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" ] }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "node_modules/@unrs/resolver-binding-openharmony-arm64": { "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", - "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "openharmony" ] }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "node_modules/@unrs/resolver-binding-wasm32-wasi": { "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", - "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", "cpu": [ - "arm64" + "wasm32" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" + } }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", - "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ] }, - "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", - "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", "cpu": [ - "loong64" + "ia32" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ] }, - "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", - "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", "cpu": [ - "loong64" + "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ] }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", - "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", - "cpu": [ - "ppc64" - ], + "node_modules/@visx/curve": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-4.0.1-alpha.0.tgz", + "integrity": "sha512-jRu61Uz274pV1zyioXmboyrLutYbnKsgjj4njSGCnhdXj5GkZvZbg+ThDb6oOzoAnJOBRLz4rzPlWvNJOzuVMg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@visx/vendor": "4.0.0-alpha.0" + } + }, + "node_modules/@visx/event": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/event/-/event-4.0.1-alpha.0.tgz", + "integrity": "sha512-EQqCMSv/s8NbFjo+hz3FKsvvYfP+2QslsFJ/24/O5l/W+7UC6J6aAvO0ujVwrTwdYbuQ+vhxKi1xdPdKR/qj1g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@visx/point": "4.0.1-alpha.0" + } + }, + "node_modules/@visx/grid": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/grid/-/grid-4.0.1-alpha.0.tgz", + "integrity": "sha512-rycutGmTHO+znNdPumheWMglm7YfpffvRwUkVy5zy4WoORIuKTMkDxwnOzHG2xMxU3EE/YCd37xFV5AxA30yeg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@visx/curve": "4.0.1-alpha.0", + "@visx/group": "4.0.1-alpha.0", + "@visx/point": "4.0.1-alpha.0", + "@visx/scale": "4.0.1-alpha.0", + "@visx/shape": "4.0.1-alpha.0", + "classnames": "^2.3.1" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/group": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/group/-/group-4.0.1-alpha.0.tgz", + "integrity": "sha512-V19l7iQ7jccBv8kao/EByuI6o4xtxzzLV9nqVI1hRvmdzTVsuLpqlwzYCZUXJaTVvUWf8s4D2SQFjGkj/Nw+0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "classnames": "^2.3.1" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/point": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/point/-/point-4.0.1-alpha.0.tgz", + "integrity": "sha512-ijTfr/Nx09f03vIj9nyTr3z4Xth4Y75427UaogJh6dnIRLMEFHQOwNu791sbfiNj0a+ZXuaE32h0vKrFe4/8Qg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@visx/responsive": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-4.0.1-alpha.0.tgz", + "integrity": "sha512-o+1zGywQZY0+yOx3Iw87wc4bbPJRr/HnIukTwfOz4UVyj9pB1OQNVHB7OORO1+LBHJceWpB31co/ZV9KHncKrA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "^4.17.13", + "@types/react": "*", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/scale": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-4.0.1-alpha.0.tgz", + "integrity": "sha512-nzjeE87vFSAXGWFiiNfBpNLAf0Q8Qmf6syvKLjqNi4kGZkdhbUll3E/59YsgWXmjM8+llPLWzGsP+JPvo5eq1A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@visx/vendor": "4.0.0-alpha.0" + } + }, + "node_modules/@visx/shape": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-4.0.1-alpha.0.tgz", + "integrity": "sha512-62QeiVNmPlterQGwhkEDcbq7M0MqY0lBsK5QKXtM9ZoPZWkuGV3aykA3+Xu20B2FAvyJq4LqJzBc7Sxr+EAdbA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "^4.17.13", + "@types/react": "*", + "@visx/curve": "4.0.1-alpha.0", + "@visx/group": "4.0.1-alpha.0", + "@visx/scale": "4.0.1-alpha.0", + "@visx/vendor": "4.0.0-alpha.0", + "classnames": "^2.3.1", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/vendor": { + "version": "4.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/vendor/-/vendor-4.0.0-alpha.0.tgz", + "integrity": "sha512-6I+MuqXBcv9jnlcVowHoHKSdk9gXTWkHLKyqBwRWg7LY6A3Ei8SHfubpqGV5rBUSppxMq2RszPJUS6w+H0YgmQ==", + "devOptional": true, + "license": "MIT and ISC", + "dependencies": { + "@types/d3-array": "3.0.3", + "@types/d3-color": "3.1.0", + "@types/d3-delaunay": "6.0.1", + "@types/d3-format": "3.0.1", + "@types/d3-geo": "3.1.0", + "@types/d3-interpolate": "3.0.1", + "@types/d3-path": "3.1.1", + "@types/d3-scale": "4.0.2", + "@types/d3-shape": "3.1.7", + "@types/d3-time": "3.0.0", + "@types/d3-time-format": "2.1.0", + "d3-array": "3.2.1", + "d3-color": "3.1.0", + "d3-delaunay": "6.0.2", + "d3-format": "3.1.0", + "d3-geo": "3.1.0", + "d3-interpolate": "3.0.1", + "d3-path": "3.1.0", + "d3-scale": "4.0.2", + "d3-shape": "3.2.0", + "d3-time": "3.1.0", + "d3-time-format": "4.1.0", + "internmap": "2.0.3" + } + }, + "node_modules/@visx/vendor/node_modules/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@xhmikosr/archive-type": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/archive-type/-/archive-type-8.1.0.tgz", + "integrity": "sha512-EXOjEbnZFE5c/nFMf4FOrEURVanzHpnkPYmnmr78u02/8hAhE0FMq8p9TK1IM0/bFr5VcyBUY0gfLm8f7dKy+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^21.3.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@xhmikosr/archive-type/node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", - "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", - "cpu": [ - "riscv64" - ], + "node_modules/@xhmikosr/bin-check": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/bin-check/-/bin-check-8.3.0.tgz", + "integrity": "sha512-ALFOzArQWaPhSb6zr5yQslFr/vyc/ZJENLzFeFzqeTWI2exm3V84zMieU2FEI29KBuotkX1zf5RTKXIc0UaChQ==", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "isexe": "^4.0.0", + "tinyexec": "^1.3.1" + }, + "engines": { + "node": ">=20" + } }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", - "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", - "cpu": [ - "riscv64" - ], + "node_modules/@xhmikosr/bin-check/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@xhmikosr/bin-wrapper": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@xhmikosr/bin-wrapper/-/bin-wrapper-14.5.2.tgz", + "integrity": "sha512-I0hz3Y7V8jbHPXLRgAYOuHodl3/4eX+dDePRb8l+nwXY07Gog7Lt3cZ9tIz3IJIbtJZyocTTb8whsatPmr5Zjg==", "dev": true, - "libc": [ - "musl" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@xhmikosr/bin-check": "^8.3.0", + "@xhmikosr/downloader": "^16.3.1", + "@xhmikosr/os-filter-obj": "^4.1.0", + "binary-version-check": "^6.1.0" + }, + "engines": { + "node": ">=20" + } }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", - "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", - "cpu": [ - "s390x" - ], + "node_modules/@xhmikosr/decompress": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress/-/decompress-11.1.4.tgz", + "integrity": "sha512-ZbYL7SAfY37/TMpopqBR3mQiuQ76kI/RNpN4q82YHSw/UxktZNy8P4wgiKPSrTImMAWRaUG8UK1pgEa56YdLaw==", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@xhmikosr/decompress-tar": "^9.0.2", + "@xhmikosr/decompress-tarbz2": "^9.0.2", + "@xhmikosr/decompress-targz": "^9.0.1", + "@xhmikosr/decompress-unzip": "^8.2.1", + "graceful-fs": "^4.2.11", + "strip-dirs": "^3.0.0" + }, + "engines": { + "node": ">=20" + } }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", - "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", - "cpu": [ - "x64" - ], + "node_modules/@xhmikosr/decompress-tar": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-tar/-/decompress-tar-9.0.2.tgz", + "integrity": "sha512-8nPZ6lZ3ExhsSxi/X/PMB3K+Vtsuxk43HowxYpxw4AsCHTYqFBXwC8B3Y+M/meaUOGOVm+2tFNUAfWGRjBt+Ww==", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "file-type": "^21.3.4", + "is-stream": "^4.0.1", + "tar-stream": "3.1.7" + }, + "engines": { + "node": ">=20" + } }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", - "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", - "cpu": [ - "x64" - ], + "node_modules/@xhmikosr/decompress-tar/node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", "dev": true, - "libc": [ - "musl" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } }, - "node_modules/@unrs/resolver-binding-openharmony-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", - "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", - "cpu": [ - "arm64" - ], + "node_modules/@xhmikosr/decompress-tar/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", - "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", - "cpu": [ - "wasm32" - ], + "node_modules/@xhmikosr/decompress-tarbz2": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-tarbz2/-/decompress-tarbz2-9.0.2.tgz", + "integrity": "sha512-m0DvZhE7remCxtS8xY2iHSjivT4v+iyYDdfNoeuu8Nm+7g8xEXdLKSyDEicu4u1ImJLLGEfjMuTLera/F6UGWw==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" + "@xhmikosr/decompress-tar": "^9.0.1", + "file-type": "^21.3.4", + "is-stream": "^4.0.1", + "seek-bzip": "^2.0.0", + "unbzip2-stream": "^1.4.3" }, "engines": { - "node": ">=14.0.0" + "node": ">=20" } }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", - "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", - "cpu": [ - "arm64" - ], + "node_modules/@xhmikosr/decompress-tarbz2/node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", - "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", - "cpu": [ - "ia32" - ], + "node_modules/@xhmikosr/decompress-tarbz2/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", - "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", - "cpu": [ - "x64" - ], + "node_modules/@xhmikosr/decompress-targz": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-targz/-/decompress-targz-9.0.1.tgz", + "integrity": "sha512-1JXu2b6yrpm5EuBoOzMU57B4qrHXJKWQQ7LlMynNEiz85mEjDciO3ayf//GXaTLLCEKiHjWlU3q3THjgf7uODA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@xhmikosr/decompress-tar": "^9.0.0", + "file-type": "^21.3.0", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=20" + } }, - "node_modules/@visx/curve": { - "version": "4.0.1-alpha.0", - "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-4.0.1-alpha.0.tgz", - "integrity": "sha512-jRu61Uz274pV1zyioXmboyrLutYbnKsgjj4njSGCnhdXj5GkZvZbg+ThDb6oOzoAnJOBRLz4rzPlWvNJOzuVMg==", - "devOptional": true, + "node_modules/@xhmikosr/decompress-targz/node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "dev": true, "license": "MIT", "dependencies": { - "@visx/vendor": "4.0.0-alpha.0" + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, - "node_modules/@visx/event": { - "version": "4.0.1-alpha.0", - "resolved": "https://registry.npmjs.org/@visx/event/-/event-4.0.1-alpha.0.tgz", - "integrity": "sha512-EQqCMSv/s8NbFjo+hz3FKsvvYfP+2QslsFJ/24/O5l/W+7UC6J6aAvO0ujVwrTwdYbuQ+vhxKi1xdPdKR/qj1g==", - "devOptional": true, + "node_modules/@xhmikosr/decompress-targz/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, "license": "MIT", - "dependencies": { - "@types/react": "*", - "@visx/point": "4.0.1-alpha.0" + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@visx/grid": { - "version": "4.0.1-alpha.0", - "resolved": "https://registry.npmjs.org/@visx/grid/-/grid-4.0.1-alpha.0.tgz", - "integrity": "sha512-rycutGmTHO+znNdPumheWMglm7YfpffvRwUkVy5zy4WoORIuKTMkDxwnOzHG2xMxU3EE/YCd37xFV5AxA30yeg==", - "devOptional": true, + "node_modules/@xhmikosr/decompress-unzip": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-unzip/-/decompress-unzip-8.2.1.tgz", + "integrity": "sha512-2MS94QnmXQwjkKN8WyFiu1sU7J3rcWJcMze4kRYsX7tN+CXpUGECgkh4YSOhujpkWPuVlFudIziJHO/TxOqkQQ==", + "dev": true, "license": "MIT", - "dependencies": { - "@types/react": "*", - "@visx/curve": "4.0.1-alpha.0", - "@visx/group": "4.0.1-alpha.0", - "@visx/point": "4.0.1-alpha.0", - "@visx/scale": "4.0.1-alpha.0", - "@visx/shape": "4.0.1-alpha.0", - "classnames": "^2.3.1" + "dependencies": { + "file-type": "^21.3.4", + "get-stream": "^9.0.1", + "yauzl": "^3.4.0" }, - "peerDependencies": { - "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + "engines": { + "node": ">=20" } }, - "node_modules/@visx/group": { - "version": "4.0.1-alpha.0", - "resolved": "https://registry.npmjs.org/@visx/group/-/group-4.0.1-alpha.0.tgz", - "integrity": "sha512-V19l7iQ7jccBv8kao/EByuI6o4xtxzzLV9nqVI1hRvmdzTVsuLpqlwzYCZUXJaTVvUWf8s4D2SQFjGkj/Nw+0w==", - "devOptional": true, + "node_modules/@xhmikosr/decompress-unzip/node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "dev": true, "license": "MIT", "dependencies": { - "@types/react": "*", - "classnames": "^2.3.1" + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" }, - "peerDependencies": { - "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, - "node_modules/@visx/point": { - "version": "4.0.1-alpha.0", - "resolved": "https://registry.npmjs.org/@visx/point/-/point-4.0.1-alpha.0.tgz", - "integrity": "sha512-ijTfr/Nx09f03vIj9nyTr3z4Xth4Y75427UaogJh6dnIRLMEFHQOwNu791sbfiNj0a+ZXuaE32h0vKrFe4/8Qg==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@visx/responsive": { - "version": "4.0.1-alpha.0", - "resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-4.0.1-alpha.0.tgz", - "integrity": "sha512-o+1zGywQZY0+yOx3Iw87wc4bbPJRr/HnIukTwfOz4UVyj9pB1OQNVHB7OORO1+LBHJceWpB31co/ZV9KHncKrA==", - "devOptional": true, + "node_modules/@xhmikosr/decompress-unzip/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, "license": "MIT", "dependencies": { - "@types/lodash": "^4.17.13", - "@types/react": "*", - "lodash": "^4.17.21" + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" }, - "peerDependencies": { - "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@visx/scale": { - "version": "4.0.1-alpha.0", - "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-4.0.1-alpha.0.tgz", - "integrity": "sha512-nzjeE87vFSAXGWFiiNfBpNLAf0Q8Qmf6syvKLjqNi4kGZkdhbUll3E/59YsgWXmjM8+llPLWzGsP+JPvo5eq1A==", - "devOptional": true, + "node_modules/@xhmikosr/decompress-unzip/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, "license": "MIT", - "dependencies": { - "@visx/vendor": "4.0.0-alpha.0" + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@visx/shape": { - "version": "4.0.1-alpha.0", - "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-4.0.1-alpha.0.tgz", - "integrity": "sha512-62QeiVNmPlterQGwhkEDcbq7M0MqY0lBsK5QKXtM9ZoPZWkuGV3aykA3+Xu20B2FAvyJq4LqJzBc7Sxr+EAdbA==", - "devOptional": true, + "node_modules/@xhmikosr/downloader": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@xhmikosr/downloader/-/downloader-16.3.1.tgz", + "integrity": "sha512-M67dvznaFbsvoqhGT4FsHWysaXXQ8386OViGZm0WOyQS3apW9p16WgvHp9nWj2vfKQAR2ZdqIBPNCHSM5rKSCg==", + "dev": true, "license": "MIT", "dependencies": { - "@types/lodash": "^4.17.13", - "@types/react": "*", - "@visx/curve": "4.0.1-alpha.0", - "@visx/group": "4.0.1-alpha.0", - "@visx/scale": "4.0.1-alpha.0", - "@visx/vendor": "4.0.0-alpha.0", - "classnames": "^2.3.1", - "lodash": "^4.17.21" + "@xhmikosr/archive-type": "^8.1.0", + "@xhmikosr/decompress": "^11.1.4", + "content-disposition": "^2.0.1", + "ext-name": "^5.0.0", + "file-type": "^21.3.4", + "filenamify": "^7.0.2", + "got": "^14.6.6" }, - "peerDependencies": { - "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + "engines": { + "node": ">=20" } }, - "node_modules/@visx/vendor": { - "version": "4.0.0-alpha.0", - "resolved": "https://registry.npmjs.org/@visx/vendor/-/vendor-4.0.0-alpha.0.tgz", - "integrity": "sha512-6I+MuqXBcv9jnlcVowHoHKSdk9gXTWkHLKyqBwRWg7LY6A3Ei8SHfubpqGV5rBUSppxMq2RszPJUS6w+H0YgmQ==", - "devOptional": true, - "license": "MIT and ISC", + "node_modules/@xhmikosr/downloader/node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "dev": true, + "license": "MIT", "dependencies": { - "@types/d3-array": "3.0.3", - "@types/d3-color": "3.1.0", - "@types/d3-delaunay": "6.0.1", - "@types/d3-format": "3.0.1", - "@types/d3-geo": "3.1.0", - "@types/d3-interpolate": "3.0.1", - "@types/d3-path": "3.1.1", - "@types/d3-scale": "4.0.2", - "@types/d3-shape": "3.1.7", - "@types/d3-time": "3.0.0", - "@types/d3-time-format": "2.1.0", - "d3-array": "3.2.1", - "d3-color": "3.1.0", - "d3-delaunay": "6.0.2", - "d3-format": "3.1.0", - "d3-geo": "3.1.0", - "d3-interpolate": "3.0.1", - "d3-path": "3.1.0", - "d3-scale": "4.0.2", - "d3-shape": "3.2.0", - "d3-time": "3.1.0", - "d3-time-format": "4.1.0", - "internmap": "2.0.3" + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, - "node_modules/@visx/vendor/node_modules/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==", - "devOptional": true, - "license": "ISC", + "node_modules/@xhmikosr/os-filter-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/os-filter-obj/-/os-filter-obj-4.1.0.tgz", + "integrity": "sha512-y5ArHvQ7BVule/+L9yE2nYMhceiJhgsqo58lOfnisQ7bg+Kjfmkgr7JBuVFiTkl+ErdShpp829QstZQyLugl8g==", + "dev": true, + "license": "MIT", "dependencies": { - "internmap": "1 - 2" + "system-architecture": "^1.0.0" }, "engines": { - "node": ">=12" + "node": ">=20" } }, "node_modules/accepts": { @@ -5844,6 +6959,21 @@ "proxy-from-env": "^2.1.0" } }, + "node_modules/b4a": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", + "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/babel-jest": { "version": "30.5.2", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.5.2.tgz", @@ -6047,6 +7177,21 @@ "node": "18 || 20 || >=22" } }, + "node_modules/bare-events": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", + "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -6086,20 +7231,186 @@ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", "license": "MIT" }, - "node_modules/better-result": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.10.0.tgz", - "integrity": "sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==", - "devOptional": true, - "license": "MIT" + "node_modules/better-result": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.10.0.tgz", + "integrity": "sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-version": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/binary-version/-/binary-version-7.1.0.tgz", + "integrity": "sha512-Iy//vPc3ANPNlIWd242Npqc8MK0a/i4kVcHDlDA6HNMv5zMxz4ulIFhOSYJVKw/8AbHdHy0CnGYEt1QqSXxPsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^8.0.1", + "find-versions": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/binary-version-check": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/binary-version-check/-/binary-version-check-6.1.0.tgz", + "integrity": "sha512-REKdLKmuViV2WrtWXvNSiPX04KbIjfUV3Cy8batUeOg+FtmowavzJorfFhWq95cVJzINnL/44ixP26TrdJZACA==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-version": "^7.1.0", + "semver": "^7.6.0", + "semver-truncate": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/binary-version/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/binary-version/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/binary-version/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/binary-version/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/binary-version/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/binary-version/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/binary-version/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/binary-version/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "node_modules/binary-version/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, "license": "MIT", "engines": { - "node": "*" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/body-parser": { @@ -6219,6 +7530,31 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-crc32": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", @@ -6245,6 +7581,19 @@ "node": ">=10.16.0" } }, + "node_modules/byte-counter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/byte-counter/-/byte-counter-0.1.0.tgz", + "integrity": "sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -6283,6 +7632,65 @@ } } }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "13.0.19", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-13.0.19.tgz", + "integrity": "sha512-SVXGH037+Mo1aIMO5B2UcleR43FGjFdN+M8JObSyEoQ2Mn4CODRWx28gN5jiTF0n5ItsgtIZfyargMNs8GX4kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.2.0", + "get-stream": "^9.0.1", + "http-cache-semantics": "^4.2.0", + "keyv": "^5.6.0", + "mimic-response": "^4.0.0", + "normalize-url": "^8.1.1", + "responselike": "^4.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-request/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -6706,6 +8114,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/convert-hrtime": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz", + "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -6965,6 +8386,22 @@ } } }, + "node_modules/decompress-response": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-10.0.0.tgz", + "integrity": "sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^4.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dedent": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", @@ -7767,6 +9204,16 @@ "node": ">= 0.6" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -7893,6 +9340,33 @@ "devOptional": true, "license": "MIT" }, + "node_modules/ext-list": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", + "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.28.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ext-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", + "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -7936,6 +9410,13 @@ "devOptional": true, "license": "MIT" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -8092,6 +9573,35 @@ "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, + "node_modules/filename-reserved-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-4.1.0.tgz", + "integrity": "sha512-pdgqcVlu789LoYmu0kz4pxSphL0pX/UVFFDJ/kTHk+1ZRc43UiokPZFsRV8OGxW49khh11rj54AhqczGIwLa+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/filenamify": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-7.0.3.tgz", + "integrity": "sha512-Pf0dwHrWs1GcIK15ps304fmxp2AOcfNR6vOIMvCPEy6ZztKYxzRQHlOuPVWo2HRjbCLHaRTY+kD8n3RbQ0A/fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^4.0.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -8162,6 +9672,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-versions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-6.0.0.tgz", + "integrity": "sha512-2kCCtc+JvcZ86IGAz3Z2Y0A1baIz9fL31pH/0S1IqZr9Iwnjq8izfPtrCyQKO6TLMPELLsQMre7VDqeIKCsHkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver-regex": "^4.0.5", + "super-regex": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -8248,6 +9775,16 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz", + "integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, "node_modules/form-data/node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -8328,6 +9865,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function-timeout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-1.0.2.tgz", + "integrity": "sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gaxios": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.0.tgz", @@ -8589,6 +10139,59 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "14.6.6", + "resolved": "https://registry.npmjs.org/got/-/got-14.6.6.tgz", + "integrity": "sha512-QLV1qeYSo5l13mQzWgP/y0LbMr5Plr5fJilgAIwgnwseproEbtNym8xpLsDzeZ6MWXgNE6kdWGBjdh3zT/Qerg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^7.0.1", + "byte-counter": "^0.1.0", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^13.0.12", + "decompress-response": "^10.0.0", + "form-data-encoder": "^4.0.2", + "http2-wrapper": "^2.2.1", + "keyv": "^5.5.3", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^4.0.1", + "responselike": "^4.0.2", + "type-fest": "^4.26.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got/node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/got/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -8699,6 +10302,13 @@ "dev": true, "license": "MIT" }, + "node_modules/http-cache-semantics": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz", + "integrity": "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -8719,6 +10329,20 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -8842,6 +10466,16 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/inspect-with-kind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/inspect-with-kind/-/inspect-with-kind-1.0.5.tgz", + "integrity": "sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "kind-of": "^6.0.2" + } + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -8924,6 +10558,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -9857,6 +11501,26 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ldapts": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/ldapts/-/ldapts-9.2.0.tgz", @@ -10042,6 +11706,19 @@ "node": ">= 18.0.0" } }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -10087,6 +11764,37 @@ "@jridgewell/sourcemap-codec": "^1.6.0" } }, + "node_modules/make-asynchronous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/make-asynchronous/-/make-asynchronous-1.1.0.tgz", + "integrity": "sha512-ayF7iT+44LXdxJLTrTd3TLQpFDDvPCBxXxbv+pMUSuHA5Q8zyAfwkRP6aHHwNVFBUFWtxAHqwNJxF8vMZLAbVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-event": "^6.0.0", + "type-fest": "^4.6.0", + "web-worker": "^1.5.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-asynchronous/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -10222,6 +11930,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", @@ -10511,6 +12232,19 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/notepack.io": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-3.0.1.tgz", @@ -10791,6 +12525,32 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/p-cancelable": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", + "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/p-event": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-6.0.1.tgz", + "integrity": "sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -10836,6 +12596,19 @@ "node": ">=8" } }, + "node_modules/p-timeout": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", + "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -11024,6 +12797,13 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, "node_modules/perfect-debounce": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", @@ -11150,6 +12930,16 @@ "node": ">= 6" } }, + "node_modules/piscina": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.9.4.tgz", + "integrity": "sha512-RyBDr2VheQ8ZfH3N8SzQZHztqVyOtadTwLnUYof6gdj5161/eu2wNhCbGl5GHnNKDpnkicJYpKtL2J+y/gk6XA==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "@napi-rs/nice": "^1.0.1" + } + }, "node_modules/pixelmatch": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz", @@ -11542,6 +13332,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/range-parser": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", @@ -11673,6 +13476,13 @@ "node": ">=0.10.0" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -11706,6 +13516,22 @@ "node": ">=4" } }, + "node_modules/responselike": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz", + "integrity": "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -11864,6 +13690,30 @@ "license": "MIT", "peer": true }, + "node_modules/seek-bzip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-2.0.0.tgz", + "integrity": "sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^6.0.0" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } + }, + "node_modules/seek-bzip/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -11876,6 +13726,35 @@ "node": ">=10" } }, + "node_modules/semver-regex": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-4.0.5.tgz", + "integrity": "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver-truncate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-3.0.0.tgz", + "integrity": "sha512-LJWA9kSvMolR51oDE6PN3kALBNaUdkxzAGcexw8gjMA8xr5zUqK0JiR3CgARSqanYF3Z1YHvsErb1KDgh+v7Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", @@ -12243,6 +14122,32 @@ "node": ">= 0.6" } }, + "node_modules/sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0.tgz", @@ -12345,6 +14250,18 @@ "node": ">=10.0.0" } }, + "node_modules/streamx": { + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", + "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/strict-event-emitter-types": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz", @@ -12433,6 +14350,17 @@ "node": ">=8" } }, + "node_modules/strip-dirs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-3.0.0.tgz", + "integrity": "sha512-I0sdgcFTfKQlUPZyAqPJmSG3HLO9rWDFnxonnIbskYNM3DwFOeTNB5KzVq3dA1GdRAc/25b5Y7UO2TQfKWw4aQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "inspect-with-kind": "^1.0.5", + "is-plain-obj": "^1.1.0" + } + }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -12472,6 +14400,24 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/super-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-1.1.0.tgz", + "integrity": "sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-timeout": "^1.0.1", + "make-asynchronous": "^1.0.1", + "time-span": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/superagent": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.4.1.tgz", @@ -12546,6 +14492,74 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/system-architecture": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-1.0.0.tgz", + "integrity": "sha512-0OJWD12D7XX3KUg1DYkMaTTjSTo2k/mhIYI3HlBlceXSMcJhW/1qO735fPKS5prcyjvn57Ub151vvASYXpQrEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/time-span": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", + "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "convert-hrtime": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -12861,6 +14875,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, "node_modules/undici-types": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", @@ -13038,6 +15063,13 @@ "node": ">= 8" } }, + "node_modules/web-worker": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", + "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/whatwg-fetch": { "version": "3.6.20", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", @@ -13219,6 +15251,19 @@ "node": ">=12" } }, + "node_modules/yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index a7135ed8..4b3dcc20 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,9 @@ "@nestjs/cli": "^12.0.0", "@nestjs/schematics": "^12.0.0", "@nestjs/testing": "^12.0.0", + "@swc/cli": "^0.8.1", + "@swc/core": "^1.16.20", + "@swc/jest": "^0.2.39", "@types/bcryptjs": "^2.4.4", "@types/compression": "^1.8.1", "@types/express": "^5.0.6", diff --git a/src/builds/builds.controller.ts b/src/builds/builds.controller.ts index c7f5d00b..f455b953 100644 --- a/src/builds/builds.controller.ts +++ b/src/builds/builds.controller.ts @@ -30,6 +30,7 @@ import { ProjectsService } from '../projects/projects.service'; import { RoleGuard } from '../auth/guards/role.guard'; import { Roles } from '../shared/roles.decorator'; import { CurrentUser } from '../shared/current-user.decorator'; +import type { WrapperType } from '../shared/wrapper-type'; @Controller('builds') @ApiTags('builds') @@ -37,7 +38,7 @@ export class BuildsController { constructor( private buildsService: BuildsService, @Inject(forwardRef(() => ProjectsService)) - private projectService: ProjectsService + private projectService: WrapperType ) {} @Get() diff --git a/src/builds/builds.service.ts b/src/builds/builds.service.ts index 30bd1d1d..b8273e70 100644 --- a/src/builds/builds.service.ts +++ b/src/builds/builds.service.ts @@ -7,6 +7,7 @@ import { BuildDto } from './dto/build.dto'; import { getBuildsStats } from './build-stats'; import { PaginatedBuildDto } from './dto/build-paginated.dto'; import { ModifyBuildDto } from './dto/build-modify.dto'; +import type { WrapperType } from '../shared/wrapper-type'; // Newest first, with the id to break a tie: without a total order two builds // made in the same instant have no fixed place, so an offset page could show @@ -23,7 +24,7 @@ export class BuildsService { private prismaService: PrismaService, private eventsGateway: EventsGateway, @Inject(forwardRef(() => TestRunsService)) - private testRunsService: TestRunsService + private testRunsService: WrapperType ) {} async findOne(id: string): Promise { diff --git a/src/compare/compare.service.spec.ts b/src/compare/compare.service.spec.ts index e0e981af..aab21bb6 100644 --- a/src/compare/compare.service.spec.ts +++ b/src/compare/compare.service.spec.ts @@ -13,10 +13,17 @@ import { StaticModule } from '../static/static.module'; import { StaticService } from '../static/static.service'; import { Dinov2Service } from './libs/dinov2/dinov2.service'; import { ImageComparison, TestStatus } from '@prisma/client'; -import * as utils from '../static/utils'; +import { isHddStaticServiceConfigured } from '../static/utils'; import { ImageCompareInput } from './libs/ImageCompareInput'; import { BlurredDifference, DEFAULT_BLUR_SIGMA, MAX_SOLID_SHARE } from './libs/pixelmatch/blur-amplitude.core'; +// a mock function in place of the export rather than a spy on it: SWC's +// CommonJS exports are getters, which jest.spyOn cannot replace +jest.mock('../static/utils', () => { + const actual = jest.requireActual('../static/utils'); + return { ...actual, isHddStaticServiceConfigured: jest.fn(actual.isHddStaticServiceConfigured) }; +}); + jest.mock('zod/v3', () => { const actualZod = jest.requireActual('zod'); return actualZod; @@ -75,13 +82,13 @@ describe('CompareService', () => { }); it('should return odiffService', () => { - jest.spyOn(utils, 'isHddStaticServiceConfigured').mockReturnValue(true); + jest.mocked(isHddStaticServiceConfigured).mockReturnValue(true); expect(service.getComparator(ImageComparison.odiff)).toBe(odiffService); }); it('should throw if not HDD for Odiff', () => { - jest.spyOn(utils, 'isHddStaticServiceConfigured').mockReturnValue(false); + jest.mocked(isHddStaticServiceConfigured).mockReturnValue(false); expect(() => service.getComparator(ImageComparison.odiff)).toThrow( 'Odiff can only be used with HDD static service. Please use another image comparison lib in project settings or switch STATIC_SERVICE envitonmental variable to HDD.' @@ -269,7 +276,7 @@ describe('CompareService with mocked collaborators', () => { }); it('passes an odiff failure that is only rendering noise', async () => { - jest.spyOn(utils, 'isHddStaticServiceConfigured').mockReturnValue(true); + jest.mocked(isHddStaticServiceConfigured).mockReturnValue(true); const { service } = await setup({ imageComparison: ImageComparison.odiff }); const result = await service.getDiff({ projectId: 'projectId', data }); diff --git a/src/compare/libs/vlm/providers/gemini/gemini.service.ts b/src/compare/libs/vlm/providers/gemini/gemini.service.ts index c949561f..e3c0ff55 100644 --- a/src/compare/libs/vlm/providers/gemini/gemini.service.ts +++ b/src/compare/libs/vlm/providers/gemini/gemini.service.ts @@ -3,7 +3,7 @@ import { GoogleGenAI } from '@google/genai'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { VlmProvider, VlmProviderResponse } from '../../vlm-provider.interface'; import { VlmConfig, GeminiVlmConfig } from '../../vlm.types'; -import { VlmComparisonResultSchema } from '../../vlm.service'; +import { VlmComparisonResultSchema } from '../../vlm-result.schema'; @Injectable() export class GeminiService implements VlmProvider { diff --git a/src/compare/libs/vlm/providers/ollama/ollama.service.ts b/src/compare/libs/vlm/providers/ollama/ollama.service.ts index c3e9cd58..e4d4ae78 100644 --- a/src/compare/libs/vlm/providers/ollama/ollama.service.ts +++ b/src/compare/libs/vlm/providers/ollama/ollama.service.ts @@ -4,7 +4,7 @@ import { Ollama, ChatRequest, ChatResponse, ListResponse, ModelResponse } from ' import { zodToJsonSchema } from 'zod-to-json-schema'; import { VlmProvider, VlmProviderResponse } from '../../vlm-provider.interface'; import { VlmConfig, OllamaVlmConfig } from '../../vlm.types'; -import { VlmComparisonResultSchema } from '../../vlm.service'; +import { VlmComparisonResultSchema } from '../../vlm-result.schema'; @Injectable() export class OllamaService implements VlmProvider { diff --git a/src/compare/libs/vlm/vlm-result.schema.ts b/src/compare/libs/vlm/vlm-result.schema.ts new file mode 100644 index 00000000..a6ec8555 --- /dev/null +++ b/src/compare/libs/vlm/vlm-result.schema.ts @@ -0,0 +1,9 @@ +import z from 'zod/v3'; + +// The answer a VLM provider must give. Its own module because the providers +// validate with it and VlmService uses the providers: kept in vlm.service it +// made an import cycle, which only resolves when vlm.service loads first. +export const VlmComparisonResultSchema = z.object({ + identical: z.boolean(), + description: z.string(), +}); diff --git a/src/compare/libs/vlm/vlm.service.ts b/src/compare/libs/vlm/vlm.service.ts index d1f6a326..7ecadd50 100644 --- a/src/compare/libs/vlm/vlm.service.ts +++ b/src/compare/libs/vlm/vlm.service.ts @@ -7,11 +7,11 @@ import { ImageComparator } from '../image-comparator.interface'; import { ImageCompareInput } from '../ImageCompareInput'; import { VlmConfig, OllamaVlmConfig } from './vlm.types'; import { VlmProvider } from './vlm-provider.interface'; +import { VlmComparisonResultSchema } from './vlm-result.schema'; import { OllamaService } from './providers/ollama/ollama.service'; import { GeminiService } from './providers/gemini/gemini.service'; import { PixelmatchService, DEFAULT_CONFIG as PIXELMATCH_DEFAULT_CONFIG } from '../pixelmatch/pixelmatch.service'; import { PNG } from 'pngjs'; -import z from 'zod/v3'; export const DEFAULT_PROMPT = `You are provided with three images: 1. First image: baseline screenshot @@ -22,10 +22,7 @@ Spot any difference in text, color, shape and position of elements - treat as di Ignore minor rendering artifacts that are imperceptible to users like antialiasing. Describe the difference in about 100 words.`; -export const VlmComparisonResultSchema = z.object({ - identical: z.boolean(), - description: z.string(), -}); +export { VlmComparisonResultSchema } from './vlm-result.schema'; export const DEFAULT_CONFIG: OllamaVlmConfig = { provider: 'ollama', diff --git a/src/jest.config.ts b/src/jest.config.ts index 495170c4..a544aa21 100644 --- a/src/jest.config.ts +++ b/src/jest.config.ts @@ -5,11 +5,29 @@ module.exports = async () => { roots: ['./'], moduleFileExtensions: ['js', 'json', 'ts'], transform: { - // transpile-only: type errors are caught once by `npm run typecheck` - '^.+\\.(t|j)s$': ['ts-jest', { tsconfig: '/../tsconfig.jest.json' }], + // SWC only transpiles; type errors are caught once by `npm run typecheck` + '^.+\\.(t|j)s$': '@swc/jest', }, // count every source file, not just the ones some test happens to import - collectCoverageFrom: ['**/*.ts', '!**/*.spec.ts', '!jest.config.ts', '!main.ts'], + // measured on the TypeScript source through source maps: istanbul would + // instrument SWC's output and count its decorator helpers as branches + coverageProvider: 'v8', + collectCoverageFrom: [ + '**/*.ts', + '!**/*.spec.ts', + '!jest.config.ts', + // startup only + '!main.ts', + '!swagger.ts', + // types only: no code runs, so v8 would report them as never executed + '!**/*.interface.ts', + '!**/*.types.ts', + '!**/*.model.ts', + '!compare/libs/ImageCompareInput.ts', + '!test-runs/diffResult.ts', + '!shared/wrapper-type.ts', + '!test-variations/dto/test-variation-update.dto.ts', + ], coverageThreshold: { global: { statements: 98, branches: 85, functions: 98, lines: 98 }, }, diff --git a/src/projects/projects.service.ts b/src/projects/projects.service.ts index c638e922..251b08d9 100644 --- a/src/projects/projects.service.ts +++ b/src/projects/projects.service.ts @@ -6,6 +6,7 @@ import { PrismaService } from '../prisma/prisma.service'; import { Project } from '@prisma/client'; import { isUuidShaped } from '../utils/random-key'; import { UpdateProjectDto } from './dto/update-project.dto'; +import type { WrapperType } from '../shared/wrapper-type'; @Injectable() export class ProjectsService { @@ -14,9 +15,9 @@ export class ProjectsService { constructor( private prismaService: PrismaService, @Inject(forwardRef(() => BuildsService)) - private buildsService: BuildsService, + private buildsService: WrapperType, @Inject(forwardRef(() => TestVariationsService)) - private testVariationsService: TestVariationsService + private testVariationsService: WrapperType ) {} async findOne(idOrName: string): Promise { diff --git a/src/shared/tasks/tasks.service.ts b/src/shared/tasks/tasks.service.ts index 4aebadf7..a6b67176 100644 --- a/src/shared/tasks/tasks.service.ts +++ b/src/shared/tasks/tasks.service.ts @@ -2,6 +2,7 @@ import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; import { PrismaService } from '../../prisma/prisma.service'; import { TestVariationsService } from '../../test-variations/test-variations.service'; +import type { WrapperType } from '../wrapper-type'; // Advisory lock key of the cleanup, shared by every instance against the same // database: "VRT" and 1, a value nothing else in the database locks on @@ -14,7 +15,7 @@ export class TasksService { constructor( private prismaService: PrismaService, @Inject(forwardRef(() => TestVariationsService)) - private testVariationService: TestVariationsService + private testVariationService: WrapperType ) {} /** diff --git a/src/shared/wrapper-type.ts b/src/shared/wrapper-type.ts new file mode 100644 index 00000000..50b5520a --- /dev/null +++ b/src/shared/wrapper-type.ts @@ -0,0 +1,8 @@ +/** + * The type of a dependency injected with forwardRef. A plain class type would + * be emitted into the constructor's design:paramtypes metadata, and with + * circular imports that class is not initialised yet when the decorator runs: + * tsc's output reads it as undefined, SWC's live bindings throw. Behind an + * alias the metadata says Object, and forwardRef hands Nest the real class. + */ +export type WrapperType = T; diff --git a/src/test-runs/test-runs.service.ts b/src/test-runs/test-runs.service.ts index 9ed59037..03e042f8 100644 --- a/src/test-runs/test-runs.service.ts +++ b/src/test-runs/test-runs.service.ts @@ -16,6 +16,7 @@ import { parseConfig } from '../compare/utils'; import { DEFAULT_CONFIG } from '../compare/libs/pixelmatch/pixelmatch.service'; import { PixelmatchConfig } from '../compare/libs/pixelmatch/pixelmatch.types'; import { signaturesMatch, SIGNATURE_LENGTH } from '../compare/libs/pixelmatch/signature.core'; +import type { WrapperType } from '../shared/wrapper-type'; @Injectable() export class TestRunsService { @@ -24,7 +25,7 @@ export class TestRunsService { constructor( @Inject(forwardRef(() => TestVariationsService)) - private testVariationService: TestVariationsService, + private testVariationService: WrapperType, private prismaService: PrismaService, private staticService: StaticService, private compareService: CompareService, diff --git a/src/test-variations/test-variations.service.ts b/src/test-variations/test-variations.service.ts index d6ed71cd..69224317 100644 --- a/src/test-variations/test-variations.service.ts +++ b/src/test-variations/test-variations.service.ts @@ -9,6 +9,7 @@ import { BuildDto } from '../builds/dto/build.dto'; import { getTestVariationUniqueData } from '../utils'; import { TestVariationUpdateDto } from './dto/test-variation-update.dto'; import { BaselineDataDto } from '../shared/dto/baseline-data.dto'; +import type { WrapperType } from '../shared/wrapper-type'; @Injectable() export class TestVariationsService { @@ -18,9 +19,9 @@ export class TestVariationsService { private prismaService: PrismaService, private staticService: StaticService, @Inject(forwardRef(() => TestRunsService)) - private testRunsService: TestRunsService, + private testRunsService: WrapperType, @Inject(forwardRef(() => BuildsService)) - private buildsService: BuildsService + private buildsService: WrapperType ) {} async getDetails(id: string): Promise< From 692b1bc7a64ebb1af68e0aaaea66103ed7fbd46d Mon Sep 17 00:00:00 2001 From: Pavel Strunkin Date: Sat, 24 Oct 2026 18:55:41 +0300 Subject: [PATCH 4/5] fix(pixelmatch): never call screenshots of another size equal The byte-equality shortcut ran before the size check: a 60x30 and a 30x60 screenshot of the same pixels decode to the very same buffer and came out equal, both when different dimensions were refused and when they were allowed and should have been compared on the larger canvas. The shortcut now applies to same-size screenshots only, after the dimension guard. Co-Authored-By: Claude Opus 5.5 --- .../pixelmatch.core.boundaries.spec.ts | 17 +++++++++++++++++ src/compare/libs/pixelmatch/pixelmatch.core.ts | 13 +++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/compare/libs/pixelmatch/pixelmatch.core.boundaries.spec.ts b/src/compare/libs/pixelmatch/pixelmatch.core.boundaries.spec.ts index be814f64..2abae522 100644 --- a/src/compare/libs/pixelmatch/pixelmatch.core.boundaries.spec.ts +++ b/src/compare/libs/pixelmatch/pixelmatch.core.boundaries.spec.ts @@ -75,6 +75,23 @@ describe('computePixelmatchDiff dimensions', () => { expect(job(png(60, 60), png(30, 60, 1))).toStrictEqual({ equal: false, isSameDimension: false }); }); + // 60x30 and 30x60 with the first 60 pixels red hold the very same bytes: + // one red row against two half-width red rows + const wide = png(60, 30, 60); + const tall = png(30, 60, 60); + + it('stops at another shape with the same pixel bytes when different dimensions are not allowed', () => { + expect(job(wide, tall)).toStrictEqual({ equal: false, isSameDimension: false }); + }); + + it('compares another shape with the same pixel bytes instead of calling it equal when dimensions may differ', () => { + const result = job(wide, tall, { allowDiffDimensions: true }); + + expect(result.equal).toBe(false); + expect(result.isSameDimension).toBe(false); + expect(result.pixelMisMatchCount).toBeGreaterThan(0); + }); + it('compares on the larger canvas when different dimensions are allowed', () => { // the checkpoint is padded with transparent pixels, which pixelmatch blends to white: // only the baseline's red first row differs diff --git a/src/compare/libs/pixelmatch/pixelmatch.core.ts b/src/compare/libs/pixelmatch/pixelmatch.core.ts index 5241b001..5cf732e2 100644 --- a/src/compare/libs/pixelmatch/pixelmatch.core.ts +++ b/src/compare/libs/pixelmatch/pixelmatch.core.ts @@ -78,15 +78,20 @@ export function computePixelmatchDiff(input: PixelmatchJobInput): PixelmatchJobO const baseline = PNG.sync.read(toBuffer(input.baseline)); const image = PNG.sync.read(toBuffer(input.image)); - if (baseline.data.equals(new Uint8Array(image.data.buffer, image.data.byteOffset, image.data.byteLength))) { - return { equal: true }; - } - const isSameDimension = baseline.width === image.width && baseline.height === image.height; if (!isSameDimension && !input.allowDiffDimensions) { return { equal: false, isSameDimension }; } + // Identical bytes only mean identical screenshots at the same size: a 60x30 + // and a 30x60 image of the same pixels decode to the very same buffer. + if ( + isSameDimension && + baseline.data.equals(new Uint8Array(image.data.buffer, image.data.byteOffset, image.data.byteLength)) + ) { + return { equal: true }; + } + // scale image to max size const maxWidth = Math.max(baseline.width, image.width); const maxHeight = Math.max(baseline.height, image.height); From 77371c0ab795a1f8badc7404680697f28542b4f7 Mon Sep 17 00:00:00 2001 From: Pavel Strunkin Date: Sat, 24 Oct 2026 18:55:41 +0300 Subject: [PATCH 5/5] build(docker): gate the image on the type check SWC compiles without checking types, which tsc's build used to do; the images published from tags (docker.yml) do not go through the CI typecheck job. The builder stage runs `npm run typecheck` before building (~3s). Co-Authored-By: Claude Opus 5.5 --- Dockerfile | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 98cd02cd..093de53c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,9 +15,11 @@ RUN npm ci COPY tsconfig*.json nest-cli.json .swcrc ./ COPY src ./src -# Dev dependencies are only needed to build; the generated Prisma client -# under node_modules/.prisma survives the prune -RUN npm run build && npm prune --omit=dev +# SWC compiles without checking types, so the image is gated on the type +# check here, as tsc's build used to gate it. Dev dependencies are only needed +# to build; the generated Prisma client under node_modules/.prisma survives +# the prune +RUN npm run typecheck && npm run build && npm prune --omit=dev FROM node:24-bookworm-slim ENV NODE_ENV=production