From 931f3e6d350ff84cec16fc6968f57d0ef7173c7f Mon Sep 17 00:00:00 2001 From: mattrace-gloo Date: Wed, 21 Oct 2026 03:12:22 -0700 Subject: [PATCH 1/3] fix(resources): complete offline manifests beyond 100 search hits Stop re-slicing bounded Aquifer search results at 100 and report truncated only when provider pagination metadata cannot prove enumeration finished. --- .../translation-resources.license.test.ts | 3 +- .../translation-resources.range.test.ts | 13 +-- .../translation-resources.service.ts | 26 +++--- .../translation-resources.test-fixtures.ts | 5 ++ .../translation-resources.test.ts | 85 +++++++++++++++---- .../translation-resources.types.ts | 3 +- .../services/aquifer/aquifer.client.test.ts | 74 +++++++++++++++- src/lib/services/aquifer/aquifer.client.ts | 14 ++- src/lib/services/aquifer/aquifer.types.ts | 10 +++ 9 files changed, 191 insertions(+), 42 deletions(-) diff --git a/src/domains/translation-resources/translation-resources.license.test.ts b/src/domains/translation-resources/translation-resources.license.test.ts index dff6da95..736b08ef 100644 --- a/src/domains/translation-resources/translation-resources.license.test.ts +++ b/src/domains/translation-resources/translation-resources.license.test.ts @@ -9,6 +9,7 @@ import { getTranslationNotes, getTranslationQuestions, } from './translation-resources.service'; +import { searchEnumeration } from './translation-resources.test-fixtures'; import { prepareOfflineManifestResponseSchema, translationNotesResponseSchema, @@ -57,7 +58,7 @@ const detail = { describe('resource license attribution', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(searchAllResources).mockResolvedValue(ok([hit])); + vi.mocked(searchAllResources).mockResolvedValue(searchEnumeration([hit])); vi.mocked(getResource).mockResolvedValue(ok(aquiferResourceDetailsSchema.parse(detail))); }); diff --git a/src/domains/translation-resources/translation-resources.range.test.ts b/src/domains/translation-resources/translation-resources.range.test.ts index 5d62a3cf..a753ff4f 100644 --- a/src/domains/translation-resources/translation-resources.range.test.ts +++ b/src/domains/translation-resources/translation-resources.range.test.ts @@ -13,6 +13,7 @@ import { imageSearchHit, NOTES_RANGE_PATH, QUESTIONS_RANGE_PATH, + searchEnumeration, searchHit, textDetails, } from './translation-resources.test-fixtures'; @@ -70,7 +71,7 @@ describe('translation-resources verse-range routes', () => { it('returns empty items when Aquifer has no notes in the range', async () => { asAuthenticatedUser(); asProjectMember(); - vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(ok([])); + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration([])); const res = await server.request(NOTES_RANGE_PATH, { method: 'GET' }); @@ -98,7 +99,7 @@ describe('translation-resources verse-range routes', () => { { ...searchHit, id: 101, name: 'one-dup', localizedName: 'One Dup' }, { ...searchHit, id: 202, name: 'two', localizedName: 'Two' }, ]; - vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(ok(hits)); + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration(hits)); vi.mocked(aquiferClient.getResource).mockImplementation(async (id: number) => ok({ ...textDetails, id, name: String(id), localizedName: String(id) }) ); @@ -165,7 +166,7 @@ describe('translation-resources verse-range routes', () => { it('returns empty items when none exist', async () => { asAuthenticatedUser(); asProjectMember(); - vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(ok([])); + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration([])); const res = await server.request(QUESTIONS_RANGE_PATH, { method: 'GET' }); @@ -183,7 +184,7 @@ describe('translation-resources verse-range routes', () => { it('returns 502 when details hydration fails for every hit', async () => { asAuthenticatedUser(); asProjectMember(); - vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(ok([searchHit])); + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration([searchHit])); vi.mocked(aquiferClient.getResource).mockResolvedValue( err(ErrorCode.AQUIFER_SERVICE_UNAVAILABLE) ); @@ -200,7 +201,7 @@ describe('translation-resources verse-range routes', () => { asAuthenticatedUser(); asProjectMember(); const hits = [imageSearchHit(55), imageSearchHit(55), imageSearchHit(66)]; - vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(ok(hits)); + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration(hits)); vi.mocked(aquiferClient.getResource).mockImplementation(async (id: number) => ok({ id, @@ -246,7 +247,7 @@ describe('translation-resources verse-range routes', () => { it('returns empty items when Aquifer has no images in the range', async () => { asAuthenticatedUser(); asProjectMember(); - vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(ok([])); + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration([])); const res = await server.request(IMAGES_RANGE_PATH, { method: 'GET' }); diff --git a/src/domains/translation-resources/translation-resources.service.ts b/src/domains/translation-resources/translation-resources.service.ts index 593f6a4d..39f7957f 100644 --- a/src/domains/translation-resources/translation-resources.service.ts +++ b/src/domains/translation-resources/translation-resources.service.ts @@ -25,8 +25,6 @@ export const AQUIFER_RESOURCE_COLLECTIONS = { /** Parallel getResource calls when hydrating search hits. */ const HYDRATE_CONCURRENCY = 5; -/** Max hydrated items kept per MANIFEST_SEARCH_CONFIGS entry. */ -const MAX_MANIFEST_ITEMS_PER_CONFIG = 100; /** * Aquifer end-verse window for chapter-range searches. Chapters never exceed * this verse count; mirrors fluent-mobile Prepare Offline (1–200). @@ -280,7 +278,7 @@ async function searchVerseRangeCollection( resourceCollectionCode, }); if (!search.ok) return search; - return ok(dedupeSearchHitsById(search.data)); + return ok(dedupeSearchHitsById(search.data.items)); } /** @@ -336,7 +334,7 @@ export async function getTranslationImagesForRange( }); if (!search.ok) return search; - const uniqueHits = dedupeSearchHitsById(search.data); + const uniqueHits = dedupeSearchHitsById(search.data.items); const hydrated = await mapWithConcurrency(uniqueHits, HYDRATE_CONCURRENCY, async (hit) => { const details = await getResource(hit.id); if (!details.ok) return details; @@ -459,16 +457,22 @@ export async function getPrepareOfflineManifest(params: { }); if (!search.ok) return search; - if (search.data.length > MAX_MANIFEST_ITEMS_PER_CONFIG) { + // Bounded Aquifer enumeration already collected every page it could. Only mark + // truncated when provider metadata could not prove the set is complete (page + // ceiling). Do not re-slice a successful enumeration — mobile rejects truncated. + if (!search.data.complete) { truncated = true; } - const capped = search.data.slice(0, MAX_MANIFEST_ITEMS_PER_CONFIG); - const hydrated = await mapWithConcurrency(capped, HYDRATE_CONCURRENCY, async (searchItem) => { - const details = await getResource(searchItem.id); - if (!details.ok) return details; - return ok({ searchItem, details: details.data }); - }); + const hydrated = await mapWithConcurrency( + search.data.items, + HYDRATE_CONCURRENCY, + async (searchItem) => { + const details = await getResource(searchItem.id); + if (!details.ok) return details; + return ok({ searchItem, details: details.data }); + } + ); if (!hydrated.ok) return hydrated; for (const { searchItem, details } of hydrated.data) { diff --git a/src/domains/translation-resources/translation-resources.test-fixtures.ts b/src/domains/translation-resources/translation-resources.test-fixtures.ts index bd826c18..6055020c 100644 --- a/src/domains/translation-resources/translation-resources.test-fixtures.ts +++ b/src/domains/translation-resources/translation-resources.test-fixtures.ts @@ -100,3 +100,8 @@ export function imageSearchHit(id: number) { }, }; } + +/** Synthetic Aquifer search enumeration for route mocks (not a live payload). */ +export function searchEnumeration(items: T[], complete = true) { + return ok({ items, complete }); +} diff --git a/src/domains/translation-resources/translation-resources.test.ts b/src/domains/translation-resources/translation-resources.test.ts index 2da00c8c..57f1615a 100644 --- a/src/domains/translation-resources/translation-resources.test.ts +++ b/src/domains/translation-resources/translation-resources.test.ts @@ -17,6 +17,7 @@ import { MOCK_PROJECT, NOTES_PATH, QUESTIONS_PATH, + searchEnumeration, searchHit, textDetails, } from './translation-resources.test-fixtures'; @@ -114,7 +115,7 @@ describe('translation-resources routes', () => { it('returns empty items when Aquifer has no notes', async () => { asAuthenticatedUser(); asProjectMember(); - vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(ok([])); + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration([])); const res = await server.request(NOTES_PATH, { method: 'GET' }); @@ -125,7 +126,7 @@ describe('translation-resources routes', () => { it('returns hydrated notes on success', async () => { asAuthenticatedUser(); asProjectMember(); - vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(ok([searchHit])); + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration([searchHit])); vi.mocked(aquiferClient.getResource).mockResolvedValue(ok(textDetails)); const res = await server.request(NOTES_PATH, { method: 'GET' }); @@ -164,7 +165,7 @@ describe('translation-resources routes', () => { { ...searchHit, id: 2, name: 'two', localizedName: 'Two' }, { ...searchHit, id: 3, name: 'three', localizedName: 'Three' }, ]; - vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(ok(hits)); + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration(hits)); vi.mocked(aquiferClient.getResource).mockImplementation(async (id: number) => { if (id === 2) return err(ErrorCode.AQUIFER_SERVICE_UNAVAILABLE); return ok({ ...textDetails, id, name: String(id), localizedName: String(id) }); @@ -208,7 +209,7 @@ describe('translation-resources routes', () => { it('returns empty items when none exist', async () => { asAuthenticatedUser(); asProjectMember(); - vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(ok([])); + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration([])); const res = await server.request(QUESTIONS_PATH, { method: 'GET' }); @@ -219,7 +220,7 @@ describe('translation-resources routes', () => { it('returns 502 when details hydration fails', async () => { asAuthenticatedUser(); asProjectMember(); - vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(ok([searchHit])); + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration([searchHit])); vi.mocked(aquiferClient.getResource).mockResolvedValue( err(ErrorCode.AQUIFER_SERVICE_UNAVAILABLE) ); @@ -236,7 +237,7 @@ describe('translation-resources routes', () => { asAuthenticatedUser(); asProjectMember(); const imageHit = imageSearchHit(55); - vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(ok([imageHit])); + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration([imageHit])); vi.mocked(aquiferClient.getResource).mockResolvedValue( ok({ id: 55, @@ -267,7 +268,7 @@ describe('translation-resources routes', () => { asAuthenticatedUser(); asProjectMember(); const imageHit = imageSearchHit(58); - vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(ok([imageHit])); + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration([imageHit])); vi.mocked(aquiferClient.getResource).mockResolvedValue( ok({ id: 58, @@ -298,7 +299,7 @@ describe('translation-resources routes', () => { asAuthenticatedUser(); asProjectMember(); const imageHit = imageSearchHit(56); - vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(ok([imageHit])); + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration([imageHit])); vi.mocked(aquiferClient.getResource).mockResolvedValue( ok({ id: 56, @@ -329,7 +330,7 @@ describe('translation-resources routes', () => { asAuthenticatedUser(); asProjectMember(); const imageHit = imageSearchHit(59); - vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(ok([imageHit])); + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration([imageHit])); vi.mocked(aquiferClient.getResource).mockResolvedValue( ok({ id: 59, @@ -366,7 +367,7 @@ describe('translation-resources routes', () => { asAuthenticatedUser(); asProjectMember(); const imageHit = imageSearchHit(57); - vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(ok([imageHit])); + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration([imageHit])); vi.mocked(aquiferClient.getResource).mockResolvedValue( ok({ id: 57, @@ -401,7 +402,7 @@ describe('translation-resources routes', () => { it('returns empty items when Aquifer has no images', async () => { asAuthenticatedUser(); asProjectMember(); - vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(ok([])); + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration([])); const res = await server.request(IMAGES_PATH, { method: 'GET' }); @@ -411,10 +412,62 @@ describe('translation-resources routes', () => { }); describe('gET manifest', () => { + it('keeps every required resource when one chapter has more than 100 notes (#431)', async () => { + asAuthenticatedUser(); + asProjectMember(); + const hits = Array.from({ length: 101 }, (_, index) => ({ + ...searchHit, + id: index + 1, + })); + vi.mocked(aquiferClient.searchAllResources) + .mockResolvedValueOnce(searchEnumeration(hits)) + .mockResolvedValue(searchEnumeration([])); + vi.mocked(aquiferClient.getResource).mockImplementation(async (id) => + ok({ ...textDetails, id }) + ); + + const res = await server.request(MANIFEST_PATH, { method: 'GET' }); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.truncated).toBe(false); + expect(body.items).toHaveLength(101); + expect(body.items.map((item: { aquiferContentId: number }) => item.aquiferContentId)).toEqual( + hits.map((hit) => hit.id) + ); + expect(body.totalBytes).toBe( + body.items.reduce((sum: number, item: { bytesTotal: number }) => sum + item.bytesTotal, 0) + ); + expect(body.totalBytes).toBeGreaterThan(0); + expect(aquiferClient.getResource).toHaveBeenCalledTimes(101); + }); + + it('sets truncated when Aquifer search enumeration is incomplete (#431)', async () => { + asAuthenticatedUser(); + asProjectMember(); + const hits = Array.from({ length: 50 }, (_, index) => ({ + ...searchHit, + id: index + 1, + })); + vi.mocked(aquiferClient.searchAllResources) + .mockResolvedValueOnce(searchEnumeration(hits, false)) + .mockResolvedValue(searchEnumeration([])); + vi.mocked(aquiferClient.getResource).mockImplementation(async (id) => + ok({ ...textDetails, id }) + ); + + const res = await server.request(MANIFEST_PATH, { method: 'GET' }); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.truncated).toBe(true); + expect(body.items).toHaveLength(50); + }); + it('returns empty manifest when Aquifer has no resources', async () => { asAuthenticatedUser(); asProjectMember(); - vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(ok([])); + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration([])); const res = await server.request(MANIFEST_PATH, { method: 'GET' }); @@ -434,8 +487,8 @@ describe('translation-resources routes', () => { asAuthenticatedUser(); asProjectMember(); vi.mocked(aquiferClient.searchAllResources) - .mockResolvedValueOnce(ok([searchHit])) - .mockResolvedValue(ok([])); + .mockResolvedValueOnce(searchEnumeration([searchHit])) + .mockResolvedValue(searchEnumeration([])); vi.mocked(aquiferClient.getResource).mockResolvedValue(ok(textDetails)); const res = await server.request(MANIFEST_PATH, { method: 'GET' }); @@ -466,8 +519,8 @@ describe('translation-resources routes', () => { asAuthenticatedUser(); asProjectMember(); vi.mocked(aquiferClient.searchAllResources) - .mockResolvedValueOnce(ok([searchHit])) - .mockResolvedValue(ok([])); + .mockResolvedValueOnce(searchEnumeration([searchHit])) + .mockResolvedValue(searchEnumeration([])); vi.mocked(aquiferClient.getResource).mockResolvedValue(ok(textDetails)); const res = await server.request(`${MANIFEST_PATH}&includeContent=true`, { method: 'GET' }); diff --git a/src/domains/translation-resources/translation-resources.types.ts b/src/domains/translation-resources/translation-resources.types.ts index 86b3c626..05774c6b 100644 --- a/src/domains/translation-resources/translation-resources.types.ts +++ b/src/domains/translation-resources/translation-resources.types.ts @@ -313,7 +313,8 @@ export const prepareOfflineManifestResponseSchema = z items: z.array(prepareOfflineManifestItemSchema), totalBytes: z.number().int().nonnegative(), truncated: z.boolean().openapi({ - description: 'True when one or more resource configs exceeded the per-config hydration cap', + description: + 'True when one or more resource configs could not be fully enumerated within the server bounded Aquifer search page budget (provider total / short-page proof missing). False when every matching resource was collected and hydrated. Clients must not treat a truncated catalogue as complete.', }), }) .openapi('PrepareOfflineResourceManifest'); diff --git a/src/lib/services/aquifer/aquifer.client.test.ts b/src/lib/services/aquifer/aquifer.client.test.ts index 03eae8b4..3c5ca9d9 100644 --- a/src/lib/services/aquifer/aquifer.client.test.ts +++ b/src/lib/services/aquifer/aquifer.client.test.ts @@ -4,6 +4,7 @@ import env from '@/env'; import { ErrorCode } from '@/lib/types'; import { + AQUIFER_MAX_SEARCH_PAGES, getBibles, getBibleText, getResource, @@ -283,8 +284,9 @@ describe('aquifer.client', () => { expect(result.ok).toBe(true); if (result.ok) { - expect(result.data).toHaveLength(101); - expect(result.data.at(-1)?.id).toBe(101); + expect(result.data.items).toHaveLength(101); + expect(result.data.items.at(-1)?.id).toBe(101); + expect(result.data.complete).toBe(true); } expect(globalThis.fetch).toHaveBeenCalledTimes(2); }); @@ -307,10 +309,76 @@ describe('aquifer.client', () => { expect(result.ok).toBe(true); if (result.ok) { - expect(result.data).toHaveLength(1); + expect(result.data.items).toHaveLength(1); + expect(result.data.complete).toBe(true); } expect(globalThis.fetch).toHaveBeenCalledTimes(1); }); + + it('marks enumeration incomplete when the page ceiling is hit with a known higher total', async () => { + const fullPage = (offset: number) => ({ + totalItemCount: AQUIFER_MAX_SEARCH_PAGES * 100 + 1, + returnedItemCount: 100, + offset, + items: Array.from({ length: 100 }, (_, index) => ({ + ...searchItem, + id: offset + index + 1, + name: `item-${offset + index + 1}`, + localizedName: `Item ${offset + index + 1}`, + })), + }); + const fetchMock = vi.spyOn(globalThis, 'fetch'); + for (let page = 0; page < AQUIFER_MAX_SEARCH_PAGES; page += 1) { + fetchMock.mockResolvedValueOnce(jsonResponse(fullPage(page * 100))); + } + + const result = await searchAllResources({ + bookCode: 'MRK', + startChapter: 1, + endChapter: 1, + languageCode: 'eng', + }); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.items).toHaveLength(AQUIFER_MAX_SEARCH_PAGES * 100); + expect(result.data.complete).toBe(false); + } + expect(globalThis.fetch).toHaveBeenCalledTimes(AQUIFER_MAX_SEARCH_PAGES); + }); + + it('marks enumeration complete when totalItemCount is reached exactly at the page ceiling', async () => { + const total = AQUIFER_MAX_SEARCH_PAGES * 100; + const fullPage = (offset: number) => ({ + totalItemCount: total, + returnedItemCount: 100, + offset, + items: Array.from({ length: 100 }, (_, index) => ({ + ...searchItem, + id: offset + index + 1, + name: `item-${offset + index + 1}`, + localizedName: `Item ${offset + index + 1}`, + })), + }); + const fetchMock = vi.spyOn(globalThis, 'fetch'); + for (let page = 0; page < AQUIFER_MAX_SEARCH_PAGES; page += 1) { + fetchMock.mockResolvedValueOnce(jsonResponse(fullPage(page * 100))); + } + + const result = await searchAllResources({ + bookCode: 'MRK', + startChapter: 1, + endChapter: 1, + languageCode: 'eng', + }); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.items).toHaveLength(total); + expect(result.data.complete).toBe(true); + } + expect(globalThis.fetch).toHaveBeenCalledTimes(AQUIFER_MAX_SEARCH_PAGES); + }); }); describe('getBibles', () => { diff --git a/src/lib/services/aquifer/aquifer.client.ts b/src/lib/services/aquifer/aquifer.client.ts index 286986cc..7c039909 100644 --- a/src/lib/services/aquifer/aquifer.client.ts +++ b/src/lib/services/aquifer/aquifer.client.ts @@ -13,6 +13,7 @@ import type { AquiferLanguageResourceCount, AquiferResourceCollection, AquiferResourceDetails, + AquiferResourceSearchEnumeration, AquiferResourceSearchResponse, AquiferSearchResourcesParams, } from './aquifer.types'; @@ -30,7 +31,8 @@ import { const DEFAULT_TIMEOUT_MS = 30_000; /** Hard cap on search pagination to avoid unbounded Aquifer fan-out. */ -const MAX_SEARCH_PAGES = 20; +export const AQUIFER_MAX_SEARCH_PAGES = 20; +const MAX_SEARCH_PAGES = AQUIFER_MAX_SEARCH_PAGES; /** Cap on upstream body text echoed into logs. */ const MAX_LOGGED_BODY_CHARS = 300; /** Cap on zod issues echoed into logs. */ @@ -236,14 +238,17 @@ export async function getResource(contentId: number): Promise -): Promise> { +): Promise> { const allItems: AquiferResourceSearchResponse['items'] = []; let offset = 0; const limit = 100; + let complete = false; for (let pageIndex = 0; pageIndex < MAX_SEARCH_PAGES; pageIndex += 1) { const page = await searchResources({ ...params, limit, offset }); @@ -255,12 +260,13 @@ export async function searchAllResources( page.data.items.length === 0 || page.data.items.length < limit ) { + complete = true; break; } offset += limit; } - return { ok: true, data: allItems }; + return { ok: true, data: { items: allItems, complete } }; } /** diff --git a/src/lib/services/aquifer/aquifer.types.ts b/src/lib/services/aquifer/aquifer.types.ts index 91065fc2..a10ecd27 100644 --- a/src/lib/services/aquifer/aquifer.types.ts +++ b/src/lib/services/aquifer/aquifer.types.ts @@ -50,6 +50,16 @@ export const aquiferResourceSearchResponseSchema = z.object({ export type AquiferResourceSearchResponse = z.infer; +/** + * Bounded Aquifer search collection. `complete` is false when the page ceiling + * stopped collection without provider proof that every matching item was returned + * (short/empty page or `totalItemCount` reached). + */ +export interface AquiferResourceSearchEnumeration { + items: AquiferResourceSearchResponse['items']; + complete: boolean; +} + // One wire model for online resources and offline manifests. Known fields are optional to // tolerate partial upstream metadata; unknown keys survive, including nested attribution fields. const aquiferLicenseLinkSchema = z From be268dc410133df654705f5bca0b75dead0f7ac1 Mon Sep 17 00:00:00 2001 From: mattrace-gloo Date: Thu, 22 Oct 2026 09:53:59 -0700 Subject: [PATCH 2/3] Merge origin/main into mrace/fix/431-complete-resource-manifest-clean Resolve conflicts with #432 fail-closed manifest hydrate and #435 image metadata. Address #437 review: soften truncated OpenAPI copy, warn on incomplete Aquifer searches (manifest + range), and drop the local MAX_SEARCH_PAGES alias / redundant empty-page check. --- .github/dependabot.yml | 58 + .github/workflows/build.yml | 4 +- .github/workflows/cut-release.yml | 2 +- .github/workflows/deploy.yml | 4 +- .github/workflows/pre-merge.yml | 6 +- .github/workflows/test-version-injection.yml | 2 +- .../ai-tools-integration-status.md | 2 +- .../2026-08-21-pm-project-create-bypass.md | 6 +- eslint-suppressions.json | 46 +- package-lock.json | 3377 ++++++++--------- package.json | 22 +- src/db/seeds/audio-demo.db.test.ts | 2 +- ...ai-pericope.repository.integration.test.ts | 131 +- .../ai-suggestions/ai-pericope.service.ts | 142 +- .../ai-pericope.test-fixtures.ts | 167 + .../ai-suggestions/ai-suggestions.service.ts | 125 +- .../bible-provider-resources.db.test.ts | 2 +- ...talogue-sync.watermark.integration.test.ts | 1 + .../chapter-assignments.repository.test.ts | 215 +- .../chapter-assignments.repository.ts | 227 +- .../chapter-assignments.service.ts | 99 +- .../chapter-assignments.transitions.test.ts | 127 + .../chapter-assignments.transitions.ts | 74 + ...ts-progress.repository.integration.test.ts | 222 ++ ...er-assignments-progress.repository.test.ts | 63 + ...chapter-assignments-progress.repository.ts | 230 ++ .../chapter-assignments-progress.service.ts | 13 + .../chapter-assignments-progress.types.ts | 9 + .../source-audio.manifest.test.ts | 177 + .../source-audio/source-audio.service.test.ts | 136 +- .../translation-resources.license.test.ts | 111 +- ...on-resources.manifest-completeness.test.ts | 258 ++ .../translation-resources.range.test.ts | 25 + .../translation-resources.service.ts | 69 +- .../translation-resources.test.ts | 165 - .../translation-resources.types.ts | 18 +- .../verse-audio.concurrency.service.test.ts | 449 +++ .../verse-audio.reclaim.service.test.ts | 101 + .../verse-audio.reclaim.service.ts | 64 + .../verse-audio.repository.test.ts | 21 - .../verse-audio/verse-audio.repository.ts | 42 - .../verse-audio/verse-audio.responses.ts | 188 + .../verse-audio/verse-audio.service.test.ts | 838 +--- .../verse-audio/verse-audio.service.ts | 234 +- .../verse-audio/verse-audio.test-fixtures.ts | 51 + .../verse-audio.upload.service.test.ts | 344 ++ src/lib/services/aquifer/aquifer.client.ts | 19 +- src/workers/ingest-bible-text.worker.test.ts | 22 +- 48 files changed, 4666 insertions(+), 4044 deletions(-) create mode 100644 src/domains/ai-suggestions/ai-pericope.test-fixtures.ts create mode 100644 src/domains/chapter-assignments/chapter-assignments.transitions.test.ts create mode 100644 src/domains/chapter-assignments/chapter-assignments.transitions.ts create mode 100644 src/domains/chapter-assignments/progress/chapter-assignments-progress.repository.integration.test.ts create mode 100644 src/domains/chapter-assignments/progress/chapter-assignments-progress.repository.test.ts create mode 100644 src/domains/chapter-assignments/progress/chapter-assignments-progress.repository.ts create mode 100644 src/domains/chapter-assignments/progress/chapter-assignments-progress.service.ts create mode 100644 src/domains/chapter-assignments/progress/chapter-assignments-progress.types.ts create mode 100644 src/domains/source-audio/source-audio.manifest.test.ts create mode 100644 src/domains/translation-resources/translation-resources.manifest-completeness.test.ts create mode 100644 src/domains/verse-audio/verse-audio.concurrency.service.test.ts create mode 100644 src/domains/verse-audio/verse-audio.reclaim.service.test.ts create mode 100644 src/domains/verse-audio/verse-audio.reclaim.service.ts create mode 100644 src/domains/verse-audio/verse-audio.responses.ts create mode 100644 src/domains/verse-audio/verse-audio.test-fixtures.ts create mode 100644 src/domains/verse-audio/verse-audio.upload.service.test.ts diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6bc098dd..e173a733 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -30,3 +30,61 @@ updates: - dependencies commit-message: prefix: build(deps) + + # The npm ecosystem had no entry here until now, which did not stop npm + # bumps from arriving -- Dependabot security updates run off the alert feed + # and ignore this file entirely. Every npm pull request this repo has ever + # seen came in that way. The cost was invisible until it wasn't: + # + # * no routine version updates at all, so dependencies only ever moved + # when an advisory forced them; + # * one pull request per advisory, ungrouped and without the cooldown + # above, which is the same sprawl the github-actions groups exist to + # prevent; + # * `@dependabot rebase` fails on those pull requests. The rebase path + # looks for the ecosystem's entry in this file, does not find one, and + # reports that the entry "has been deleted" -- which sends you to close + # a perfectly good pull request. Closing it teaches Dependabot the + # release was declined and it stops offering that fix, which is how the + # axios and better-auth advisories ended up with no open pull request. + - package-ecosystem: npm + directory: / + schedule: + interval: daily + cooldown: + default-days: 7 + open-pull-requests-limit: 5 + # Bump the minimum of whatever specification already exists, rather than + # widening it into a range. An exact pin stays exact -- which is most of + # package.json -- and the handful of caret ranges (@aws-sdk/*, better-auth, + # bcryptjs) keep their caret at a higher floor. This does not convert a + # range into a pin: tightening those is a package.json edit, not something + # a Dependabot setting can do. + versioning-strategy: increase + groups: + # Minor and patch only. Majors are deliberately left ungrouped so each + # one arrives as its own pull request with its own changelog to read. + npm-production: + applies-to: version-updates + dependency-type: production + update-types: + - minor + - patch + npm-development: + applies-to: version-updates + dependency-type: development + update-types: + - minor + - patch + # Same reasoning as github-actions-security above: advisories arrive as + # a separate update type and default to a pull request each. + npm-security: + applies-to: security-updates + patterns: + - '*' + labels: + - dependencies + - javascript + commit-message: + prefix: build(deps) + prefix-development: build(deps-dev) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5bb5ed4a..3d47398e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,7 +40,7 @@ jobs: persist-credentials: false - name: Set up Node.js version - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.14.0 cache: npm @@ -94,7 +94,7 @@ jobs: node scripts/setup-webjob.js - name: Upload artifact for deployment job - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: ${{ inputs.artifact }} path: deployment/ diff --git a/.github/workflows/cut-release.yml b/.github/workflows/cut-release.yml index 5558510a..7d4570a4 100644 --- a/.github/workflows/cut-release.yml +++ b/.github/workflows/cut-release.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Mint App installation token id: app-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e98c69b3..dac4907a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -99,7 +99,7 @@ jobs: persist-credentials: false - name: Set up Node.js version - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.14.0 cache: npm @@ -121,7 +121,7 @@ jobs: npm run db:migrate - name: Download artifact from build job - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: ${{ inputs.artifact }} path: deployment/ diff --git a/.github/workflows/pre-merge.yml b/.github/workflows/pre-merge.yml index a7b65ee7..26fd5af1 100644 --- a/.github/workflows/pre-merge.yml +++ b/.github/workflows/pre-merge.yml @@ -21,7 +21,7 @@ jobs: persist-credentials: false - name: Set up Node.js version - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.14.0 cache: npm @@ -54,7 +54,7 @@ jobs: persist-credentials: false - name: Set up Node.js version - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.14.0 cache: npm @@ -98,7 +98,7 @@ jobs: persist-credentials: false - name: Set up Node.js version - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.14.0 cache: npm diff --git a/.github/workflows/test-version-injection.yml b/.github/workflows/test-version-injection.yml index 5f6f8ebc..cff67dd2 100644 --- a/.github/workflows/test-version-injection.yml +++ b/.github/workflows/test-version-injection.yml @@ -17,7 +17,7 @@ jobs: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.14.0 cache: npm diff --git a/docs/features/repeated-word-check/ai-tools-integration-status.md b/docs/features/repeated-word-check/ai-tools-integration-status.md index 87d7c35b..92114aed 100644 --- a/docs/features/repeated-word-check/ai-tools-integration-status.md +++ b/docs/features/repeated-word-check/ai-tools-integration-status.md @@ -42,7 +42,7 @@ | Route tests | ✅ Implemented | `ai-tools.route.test.ts`. | | Smoke script + npm alias | ✅ Implemented | `scripts/smoke-repeated-words.ts`, `npm run smoke:repeated-words` (auto sign-in + PASS/FAIL banner). | | Documentation | ✅ Implemented | This file + the split Part 1 / Part 2 proposal + summary. | -| **Live end-to-end run** (fluent-api ↔ fluent-ai) | ✅ **Done** | Verified 2026-06-04 against a from-scratch platform stack — 10/10 on the host **and** in-container. See §3.1. | +| **Live end-to-end run** (fluent-api ↔ fluent-ai) | ✅ **Done** | Verified 2026-06-04 against a from-scratch platform stack — 10/10 on the host **and** in-container. See §3.1. | | fluent-platform compose override | ✅ Applied | Applied locally (URL override + two clean-slate fixes). Still ships as a **separate paired PR** (D12); see §3.2. | | Polling endpoint / DB persistence / frontend / retries / caching | ⛔ Out of scope | Deferred by design — see §2 / §14 of the proposal. | diff --git a/docs/tasks/2026-08-21-pm-project-create-bypass.md b/docs/tasks/2026-08-21-pm-project-create-bypass.md index 5c0918c6..3348eb73 100644 --- a/docs/tasks/2026-08-21-pm-project-create-bypass.md +++ b/docs/tasks/2026-08-21-pm-project-create-bypass.md @@ -42,10 +42,10 @@ the file to find the exact block. When a user is assigned a role, a row is written to `user_roles`: -| userId | orgId | projectId | roleId | +| userId | orgId | projectId | roleId | | ------ | ----- | --------- | --------------- | ----------------------------------- | -| 5 | 2 | null | org_manager | ← org-wide, CAN create projects | -| 5 | 2 | 9 | project_manager | ← project-pinned, CANNOT (normally) | +| 5 | 2 | null | org_manager | ← org-wide, CAN create projects | +| 5 | 2 | 9 | project_manager | ← project-pinned, CANNOT (normally) | At login, `findGrantsByUserId()` loads all rows and groups them into `Grant` objects: `{ orgId, projectId, permissions: Set<...> }`. diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 16336aad..97bf6b31 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -29,11 +29,6 @@ "count": 1 } }, - "src/domains/ai-suggestions/ai-pericope.repository.integration.test.ts": { - "max-lines": { - "count": 1 - } - }, "src/domains/ai-suggestions/ai-pericope.repository.ts": { "fluent-standards/one-repository-per-directory": { "count": 1 @@ -86,9 +81,6 @@ "src/domains/ai-suggestions/ai-suggestions.service.ts": { "fluent-standards/repository-namespace-import": { "count": 1 - }, - "max-lines": { - "count": 1 } }, "src/domains/ai-tools/ai-tools-auth.middleware.ts": { @@ -204,15 +196,7 @@ "count": 1 }, "ts/no-explicit-any": { - "count": 3 - } - }, - "src/domains/chapter-assignments/chapter-assignments.repository.ts": { - "max-lines": { - "count": 1 - }, - "ts/no-explicit-any": { - "count": 1 + "count": 2 } }, "src/domains/chapter-assignments/chapter-assignments.route.test.ts": { @@ -232,14 +216,8 @@ } }, "src/domains/chapter-assignments/chapter-assignments.service.ts": { - "max-lines": { - "count": 1 - }, "no-restricted-syntax": { "count": 1 - }, - "ts/no-explicit-any": { - "count": 2 } }, "src/domains/languages/import/enrich-localized-names.test.ts": { @@ -415,11 +393,6 @@ "count": 6 } }, - "src/domains/source-audio/source-audio.service.test.ts": { - "max-lines": { - "count": 1 - } - }, "src/domains/source-audio/source-audio.service.ts": { "fluent-standards/no-cross-domain-repository": { "count": 1 @@ -440,6 +413,11 @@ "count": 1 } }, + "src/domains/translation-resources/translation-resources.manifest-completeness.test.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "src/domains/translation-resources/translation-resources.range.test.ts": { "no-restricted-syntax": { "count": 4 @@ -455,7 +433,7 @@ }, "src/domains/translation-resources/translation-resources.test.ts": { "no-restricted-syntax": { - "count": 5 + "count": 4 }, "ts/no-explicit-any": { "count": 2 @@ -590,16 +568,6 @@ "count": 2 } }, - "src/domains/verse-audio/verse-audio.service.test.ts": { - "max-lines": { - "count": 1 - } - }, - "src/domains/verse-audio/verse-audio.service.ts": { - "max-lines": { - "count": 1 - } - }, "src/domains/youversion/youversion.route.test.ts": { "no-restricted-syntax": { "count": 1 diff --git a/package-lock.json b/package-lock.json index 3aaada78..53b2eed3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,8 @@ "@aws-sdk/client-s3": "^3.1098.0", "@aws-sdk/lib-storage": "^3.1098.0", "@aws-sdk/s3-request-presigner": "^3.1098.0", - "@better-auth/drizzle-adapter": "^1.6.9", - "@better-auth/passkey": "^1.6.9", + "@better-auth/drizzle-adapter": "1.6.30", + "@better-auth/passkey": "1.6.30", "@hono/node-server": "1.19.15", "@hono/zod-openapi": "0.19.10", "@scalar/hono-api-reference": "0.9.48", @@ -21,14 +21,14 @@ "applicationinsights": "3.14.0", "archiver": "7.0.1", "bcryptjs": "^3.0.3", - "better-auth": "^1.6.9", + "better-auth": "1.6.30", "dotenv": "16.6.1", "dotenv-expand": "12.0.2", "drizzle-orm": "0.45.2", "drizzle-zod": "0.7.1", "hono": "4.13.5", "hono-pino": "0.8.0", - "mailgun.js": "12.0.3", + "mailgun.js": "14.1.0", "nanoid": "^6.0.2", "pg-boss": "12.1.1", "pino": "9.7.0", @@ -39,20 +39,20 @@ }, "devDependencies": { "@antfu/eslint-config": "4.18.0", - "@electric-sql/pglite": "0.5.8", + "@electric-sql/pglite": "0.5.10", "@types/archiver": "6.0.3", "@types/node": "24.1.0", "cross-env": "7.0.3", - "drizzle-kit": "0.31.10", - "eslint": "9.31.0", + "drizzle-kit": "0.31.11", + "eslint": "10.12.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-format": "1.0.1", "husky": "9.1.7", "knip": "5.88.1", - "pino-pretty": "13.0.0", - "prettier": "3.6.2", - "tsc-alias": "1.8.16", - "tsx": "4.21.0", + "pino-pretty": "13.2.0", + "prettier": "3.9.10", + "tsc-alias": "1.9.7", + "tsx": "4.23.15", "typescript": "5.8.3", "vitest": "3.2.4" }, @@ -172,6 +172,48 @@ } } }, + "node_modules/@antfu/eslint-config/node_modules/@es-joy/jsdoccomment": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.52.0.tgz", + "integrity": "sha512-BXuN7BII+8AyNtn57euU2Yxo9yA/KUDNzrpXyi3pfqKmBhhysR6ZWOebFh3vyPoqA3/j1SOvGgucElMGwlXing==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.8", + "@typescript-eslint/types": "^8.34.1", + "comment-parser": "1.4.1", + "esquery": "^1.6.0", + "jsdoc-type-pratt-parser": "~4.1.0" + }, + "engines": { + "node": ">=20.11.0" + } + }, + "node_modules/@antfu/eslint-config/node_modules/eslint-plugin-jsdoc": { + "version": "51.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-51.4.1.tgz", + "integrity": "sha512-y4CA9OkachG8v5nAtrwvcvjIbdcKgSyS6U//IfQr4FZFFyeBFwZFf/tfSsMr46mWDJgidZjBTqoCRlXywfFBMg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@es-joy/jsdoccomment": "~0.52.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.1", + "debug": "^4.4.1", + "escape-string-regexp": "^4.0.0", + "espree": "^10.4.0", + "esquery": "^1.6.0", + "parse-imports-exports": "^0.2.4", + "semver": "^7.7.2", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": ">=20.11.0" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, "node_modules/@antfu/install-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", @@ -614,6 +656,15 @@ "node": ">=20.0.0" } }, + "node_modules/@azure/core-process": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@azure/core-process/-/core-process-1.0.0.tgz", + "integrity": "sha512-/shnJ+ooO8WPxDhPEeI/2oRQuubn16gZ6CvlbpWbEswZfzwI9tI/sMAHmF3x1LuQ9yZYXfLW3TjzGMLEC5blKg==", + "license": "MIT", + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/@azure/core-rest-pipeline": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.24.0.tgz", @@ -728,9 +779,9 @@ } }, "node_modules/@azure/monitor-opentelemetry": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/@azure/monitor-opentelemetry/-/monitor-opentelemetry-1.19.0.tgz", - "integrity": "sha512-QJDMgsnhA1ysyOcr7E1WHHn5gYts3kf5FP6e4gy+Y8EpUmKg2g07MRLUNXUz6fvWRmevCFqEUcPDKw2rnXVyOw==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/@azure/monitor-opentelemetry/-/monitor-opentelemetry-1.20.1.tgz", + "integrity": "sha512-KisIq7afY46aF1jrEfp2VlfotZZ7bIP5aguZCL6dm8mYCICpPR0P6AV53PIKQR/NQ8Z7m3MKbe8TPaZxePZ1Aw==", "license": "MIT", "dependencies": { "@azure-rest/core-client": "^2.5.2", @@ -739,30 +790,30 @@ "@azure/core-tracing": "^1.2.0", "@azure/identity": "^4.13.1", "@azure/logger": "^1.3.0", - "@azure/monitor-opentelemetry-exporter": "1.0.0-beta.44", + "@azure/monitor-opentelemetry-exporter": "1.0.0-beta.46", "@azure/opentelemetry-instrumentation-azure-sdk": "^1.1.0-beta.1", "@microsoft/applicationinsights-web-snippet": "^1.2.3", "@opentelemetry/api": "^1.9.1", - "@opentelemetry/api-logs": "^0.220.0", - "@opentelemetry/core": "^2.9.0", - "@opentelemetry/instrumentation": "^0.220.0", - "@opentelemetry/instrumentation-bunyan": "^0.65.0", - "@opentelemetry/instrumentation-console": "^0.2.0", - "@opentelemetry/instrumentation-http": "^0.220.0", - "@opentelemetry/instrumentation-mongodb": "^0.73.0", - "@opentelemetry/instrumentation-mysql": "^0.66.0", - "@opentelemetry/instrumentation-pg": "^0.72.0", - "@opentelemetry/instrumentation-redis": "^0.68.0", - "@opentelemetry/instrumentation-winston": "^0.64.0", - "@opentelemetry/resource-detector-azure": "^0.28.0", - "@opentelemetry/resources": "^2.9.0", - "@opentelemetry/sdk-logs": "^0.220.0", - "@opentelemetry/sdk-metrics": "^2.9.0", - "@opentelemetry/sdk-node": "^0.220.0", - "@opentelemetry/sdk-trace-base": "^2.9.0", - "@opentelemetry/sdk-trace-node": "^2.9.0", - "@opentelemetry/semantic-conventions": "^1.40.0", - "@opentelemetry/winston-transport": "^0.30.0", + "@opentelemetry/api-logs": "^0.221.0", + "@opentelemetry/core": "^2.10.0", + "@opentelemetry/instrumentation": "^0.221.0", + "@opentelemetry/instrumentation-bunyan": "^0.66.0", + "@opentelemetry/instrumentation-console": "^0.3.0", + "@opentelemetry/instrumentation-http": "^0.221.0", + "@opentelemetry/instrumentation-mongodb": "^0.74.0", + "@opentelemetry/instrumentation-mysql": "^0.67.0", + "@opentelemetry/instrumentation-pg": "^0.73.0", + "@opentelemetry/instrumentation-redis": "^0.69.0", + "@opentelemetry/instrumentation-winston": "^0.65.0", + "@opentelemetry/resource-detector-azure": "^0.29.0", + "@opentelemetry/resources": "^2.10.0", + "@opentelemetry/sdk-logs": "^0.221.0", + "@opentelemetry/sdk-metrics": "^2.10.0", + "@opentelemetry/sdk-node": "^0.221.0", + "@opentelemetry/sdk-trace-base": "^2.10.0", + "@opentelemetry/sdk-trace-node": "^2.10.0", + "@opentelemetry/semantic-conventions": "^1.43.0", + "@opentelemetry/winston-transport": "^0.31.0", "tslib": "^2.8.1" }, "engines": { @@ -770,116 +821,38 @@ } }, "node_modules/@azure/monitor-opentelemetry-exporter": { - "version": "1.0.0-beta.44", - "resolved": "https://registry.npmjs.org/@azure/monitor-opentelemetry-exporter/-/monitor-opentelemetry-exporter-1.0.0-beta.44.tgz", - "integrity": "sha512-q266CqBoQDp4UcUvPXZ4NYSvl5aTDqiiUu7pDWqqtxL0nCvPkwZT8/vKOhhi8cB0pnL/ojc3basyV9lbS+jbPQ==", + "version": "1.0.0-beta.46", + "resolved": "https://registry.npmjs.org/@azure/monitor-opentelemetry-exporter/-/monitor-opentelemetry-exporter-1.0.0-beta.46.tgz", + "integrity": "sha512-DXrkmKi2+zZCZEWPZN2cwEv3++NSeoo93F6FlHivoT+t7Q/7IRxLrCVttn22jGvFs8636cRKcWA8tqS8jPFOsQ==", "license": "MIT", "dependencies": { "@azure-rest/core-client": "^2.5.2", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", + "@azure/core-process": "^1.0.0", "@azure/core-rest-pipeline": "^1.19.0", "@azure/core-util": "^1.11.0", "@opentelemetry/api": "^1.9.1", - "@opentelemetry/api-logs": "^0.220.0", - "@opentelemetry/core": "^2.9.0", - "@opentelemetry/resources": "^2.9.0", - "@opentelemetry/sdk-logs": "^0.220.0", - "@opentelemetry/sdk-metrics": "^2.9.0", - "@opentelemetry/sdk-trace-base": "^2.9.0", - "@opentelemetry/semantic-conventions": "^1.40.0", + "@opentelemetry/api-logs": "^0.221.0", + "@opentelemetry/core": "^2.10.0", + "@opentelemetry/resources": "^2.10.0", + "@opentelemetry/sdk-logs": "^0.221.0", + "@opentelemetry/sdk-metrics": "^2.10.0", + "@opentelemetry/sdk-trace-base": "^2.10.0", + "@opentelemetry/semantic-conventions": "^1.43.0", "tslib": "^2.8.1" }, "engines": { "node": ">=22.0.0" } }, - "node_modules/@azure/monitor-opentelemetry-exporter/node_modules/@opentelemetry/core": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz", - "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@azure/monitor-opentelemetry-exporter/node_modules/@opentelemetry/resources": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.10.0.tgz", - "integrity": "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.10.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, "node_modules/@azure/monitor-opentelemetry-exporter/node_modules/@opentelemetry/sdk-logs": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.220.0.tgz", - "integrity": "sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.220.0", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" - } - }, - "node_modules/@azure/monitor-opentelemetry-exporter/node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", - "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@azure/monitor-opentelemetry-exporter/node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", - "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@azure/monitor-opentelemetry-exporter/node_modules/@opentelemetry/sdk-trace": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz", - "integrity": "sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg==", "license": "Apache-2.0", "dependencies": { + "@opentelemetry/api-logs": "0.221.0", "@opentelemetry/core": "2.10.0", "@opentelemetry/resources": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -888,25 +861,7 @@ "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@azure/monitor-opentelemetry-exporter/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.10.0.tgz", - "integrity": "sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.10.0", - "@opentelemetry/resources": "2.10.0", - "@opentelemetry/sdk-trace": "2.10.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "node_modules/@azure/monitor-opentelemetry/node_modules/@azure/opentelemetry-instrumentation-azure-sdk": { @@ -956,104 +911,13 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@azure/monitor-opentelemetry/node_modules/@opentelemetry/context-async-hooks": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.10.0.tgz", - "integrity": "sha512-bvyMcgLEkozzSzpEEEo1OMoeQ97bxj6Qs2uN3mPrSdDvObMI1myffD/BPqcLlzZO9//d1SqQA/WPw7Cz2AiqhA==", - "license": "Apache-2.0", - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@azure/monitor-opentelemetry/node_modules/@opentelemetry/core": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz", - "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@azure/monitor-opentelemetry/node_modules/@opentelemetry/resources": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.10.0.tgz", - "integrity": "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.10.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, "node_modules/@azure/monitor-opentelemetry/node_modules/@opentelemetry/sdk-logs": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.220.0.tgz", - "integrity": "sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.220.0", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" - } - }, - "node_modules/@azure/monitor-opentelemetry/node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", - "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@azure/monitor-opentelemetry/node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", - "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@azure/monitor-opentelemetry/node_modules/@opentelemetry/sdk-trace": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz", - "integrity": "sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg==", "license": "Apache-2.0", "dependencies": { + "@opentelemetry/api-logs": "0.221.0", "@opentelemetry/core": "2.10.0", "@opentelemetry/resources": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -1062,42 +926,7 @@ "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@azure/monitor-opentelemetry/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.10.0.tgz", - "integrity": "sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.10.0", - "@opentelemetry/resources": "2.10.0", - "@opentelemetry/sdk-trace": "2.10.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@azure/monitor-opentelemetry/node_modules/@opentelemetry/sdk-trace-node": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.10.0.tgz", - "integrity": "sha512-GZK/G6oZyBLGlH1pUgeDch7D91KoHd2uotUGIkWCPi9GI5T9X0p4L7nNAMDR1BQjkRYoDqo+ddfVx9t5Uhys+Q==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/context-async-hooks": "2.10.0", - "@opentelemetry/core": "2.10.0", - "@opentelemetry/sdk-trace-base": "2.10.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "node_modules/@azure/monitor-opentelemetry/node_modules/import-in-the-middle": { @@ -1206,20 +1035,63 @@ "module-details-from-path": "^1.0.4" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@better-auth/core": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.6.9.tgz", - "integrity": "sha512-ADFk5pwmLybmc+LvYvXJ6M1x2oY/EyYLkwLuH0x28FUq12DfjL0wnE7g+WRDf3yozDO+qIxTpFGXDGwLKbfz0w==", + "version": "1.6.30", + "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.6.30.tgz", + "integrity": "sha512-Xhe7D7Zdms8FaVbdT1brYqo8biZiMEF1GIzrelhXYP4skth52RjHrW5X9HyuwIm51rOd0dmXN9IL27UMIazpkg==", "license": "MIT", "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", @@ -1227,13 +1099,13 @@ "zod": "^4.3.6" }, "peerDependencies": { - "@better-auth/utils": "0.4.0", - "@better-fetch/fetch": "1.1.21", + "@better-auth/utils": "0.4.2", + "@better-fetch/fetch": "1.3.1", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", - "better-call": "1.3.5", + "better-call": "1.4.0", "jose": "^6.1.0", - "kysely": "^0.28.5", + "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" }, "peerDependenciesMeta": { @@ -1246,22 +1118,22 @@ } }, "node_modules/@better-auth/core/node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/@better-auth/drizzle-adapter": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.6.9.tgz", - "integrity": "sha512-Lcco5hOGrMgc4XKAkvB6x72eQm4wCcya8IevMg4wBHY9W9GVg8pu23rpRX6VsVQSO4Ux13S7lFwUWtF7/r9aKw==", + "version": "1.6.30", + "resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.6.30.tgz", + "integrity": "sha512-k8cRfUViD++xzK51MmzvtcTZCurss8Oq4iheak6sqlMbqgRgmaeD85M+HXitOlZAb27uLz6gsMh9fcQwW0E8aA==", "license": "MIT", "peerDependencies": { - "@better-auth/core": "^1.6.9", - "@better-auth/utils": "0.4.0", + "@better-auth/core": "^1.6.30", + "@better-auth/utils": "0.4.2", "drizzle-orm": "^0.45.2" }, "peerDependenciesMeta": { @@ -1271,14 +1143,14 @@ } }, "node_modules/@better-auth/kysely-adapter": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.6.9.tgz", - "integrity": "sha512-gyjuuxJtZ4o9G9z9q4kqn24X2kvMSp7F+KHogYxF03SnXY/2WleAcuj57iC4wP3e9mGDbjPOrnM5K6Kr3Ktdpw==", + "version": "1.6.30", + "resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.6.30.tgz", + "integrity": "sha512-4+1vFYBnUYW4JUnGAbHOmRs4No2/bFWXkC1Rge//S0yeWGVD+Mm4oHSn5esVYry/PaXzmPbk1H1xu6Kb9ZRvaQ==", "license": "MIT", "peerDependencies": { - "@better-auth/core": "^1.6.9", - "@better-auth/utils": "0.4.0", - "kysely": "^0.28.14" + "@better-auth/core": "^1.6.30", + "@better-auth/utils": "0.4.2", + "kysely": "^0.28.17 || ^0.29.0" }, "peerDependenciesMeta": { "kysely": { @@ -1287,23 +1159,23 @@ } }, "node_modules/@better-auth/memory-adapter": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.6.9.tgz", - "integrity": "sha512-XmIG4tUnOXZ+KEcWjHUjOI9Z5donD09dC2t/AQTXifAUIqx7cySg86w0KTM09ArzAxRx1fCqO36Wkt5nULnrkQ==", + "version": "1.6.30", + "resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.6.30.tgz", + "integrity": "sha512-lHmx6Geyn6YY2YmnTNm1WqUdMRNb875l35S1dBRh5/iAwcA8NAa+1nDU9C1yS4R7bXPPi6JbvFG4ragcbJhFNw==", "license": "MIT", "peerDependencies": { - "@better-auth/core": "^1.6.9", - "@better-auth/utils": "0.4.0" + "@better-auth/core": "^1.6.30", + "@better-auth/utils": "0.4.2" } }, "node_modules/@better-auth/mongo-adapter": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.6.9.tgz", - "integrity": "sha512-h+AiRJ/TsBSi+ZDjySASBpbJ/9QCXBre34PSKgCz7QmTHrFM9Cg2EM4AM7LjR5lPXipEE+2rWPBc9wfnUBjhcw==", + "version": "1.6.30", + "resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.6.30.tgz", + "integrity": "sha512-4vhNyz2WkrYd96oqRumEYTBukx2070XH6tq2mhDHZAeIT4WjIScZWlckZfwoEYY0QLn/N0Cn4cb+IDT39R3l3Q==", "license": "MIT", "peerDependencies": { - "@better-auth/core": "^1.6.9", - "@better-auth/utils": "0.4.0", + "@better-auth/core": "^1.6.30", + "@better-auth/utils": "0.4.2", "mongodb": "^6.0.0 || ^7.0.0" }, "peerDependenciesMeta": { @@ -1313,9 +1185,9 @@ } }, "node_modules/@better-auth/passkey": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/@better-auth/passkey/-/passkey-1.6.9.tgz", - "integrity": "sha512-MFpi+2G/pG2wVcTuL/PcnWxP2ddFL4jmFByTCbgvr61tp7u96d5liBptxpTqfS5IuCi2o8bBRmjiQnDZAvxmHg==", + "version": "1.6.30", + "resolved": "https://registry.npmjs.org/@better-auth/passkey/-/passkey-1.6.30.tgz", + "integrity": "sha512-V4GNLNNpDs2bAAy+wxFi0WfLH+e/8Ex40reH/2kTMnwURDfwg0RfdtrmtPvANAR6J7PP8b9Rz5dCF9Ft8pvArw==", "license": "MIT", "dependencies": { "@simplewebauthn/browser": "^13.2.2", @@ -1323,11 +1195,11 @@ "zod": "^4.3.6" }, "peerDependencies": { - "@better-auth/core": "^1.6.9", - "@better-auth/utils": "0.4.0", - "@better-fetch/fetch": "1.1.21", - "better-auth": "^1.6.9", - "better-call": "1.3.5", + "@better-auth/core": "^1.6.30", + "@better-auth/utils": "0.4.2", + "@better-fetch/fetch": "1.3.1", + "better-auth": "^1.6.30", + "better-call": "1.4.0", "nanostores": "^1.0.1" } }, @@ -1341,13 +1213,13 @@ } }, "node_modules/@better-auth/prisma-adapter": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.6.9.tgz", - "integrity": "sha512-XHks01ntK20orqK/jICq8wmEbJ/zT6dct49Fk8zTQKN9QNGDc+Ix5+7z/Kvui0DXGFf790GfvRozquzaLtXa8Q==", + "version": "1.6.30", + "resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.6.30.tgz", + "integrity": "sha512-C4XfyWHbO8oLj5R9ws5ZSrLQir6D995HPOzfgsElvnqr4LoPoqL5H2Aryt9XChvO/JhnyBg3puagMVPRF3Hamw==", "license": "MIT", "peerDependencies": { - "@better-auth/core": "^1.6.9", - "@better-auth/utils": "0.4.0", + "@better-auth/core": "^1.6.30", + "@better-auth/utils": "0.4.2", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, @@ -1361,29 +1233,54 @@ } }, "node_modules/@better-auth/telemetry": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.6.9.tgz", - "integrity": "sha512-0u5zkhSCAQFoN3DHvUkLHOF6MBbVTDAa6mU8mhPwiysdz1x21vMzhzfaAKN/ZGWaQ09v91/F+2qu42G/bhUV4A==", + "version": "1.6.30", + "resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.6.30.tgz", + "integrity": "sha512-wvY+/rWsHsGHw7agr2VU7jasWtSIA6obsHjJGS4BwiZ82FRbM9Q1ngzDldhn3No0ZOlrJ82BGFtMQnNVMXpxrw==", "license": "MIT", "peerDependencies": { - "@better-auth/core": "^1.6.9", - "@better-auth/utils": "0.4.0", - "@better-fetch/fetch": "1.1.21" + "@better-auth/core": "^1.6.30", + "@better-auth/utils": "0.4.2", + "@better-fetch/fetch": "1.3.1" } }, "node_modules/@better-auth/utils": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.4.0.tgz", - "integrity": "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.4.2.tgz", + "integrity": "sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==", "license": "MIT", "dependencies": { "@noble/hashes": "^2.0.1" } }, "node_modules/@better-fetch/fetch": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.21.tgz", - "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==" + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.3.1.tgz", + "integrity": "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==", + "license": "MIT" + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } }, "node_modules/@clack/core": { "version": "0.5.0", @@ -1442,14 +1339,14 @@ "version": "0.10.2", "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@electric-sql/pglite": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.8.tgz", - "integrity": "sha512-n9tsbUOhwx2epK1V0ZG9Ar4SHWUju04dhmzZXiSBXwBoleOvIfals33NAaWgagQVAL4Rbvx/Ptsu3P+pA09f6Q==", - "dev": true, + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.10.tgz", + "integrity": "sha512-tXc/25U3ulOkiRQuTFdwIv+JhBqtMdAF7Gvo6CfZZWtSB4+hgC4Wr2vwK5ngvvN5PJfAAEGIsfyCVy1JhCOBYw==", + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@emnapi/core": { @@ -1508,7 +1405,7 @@ "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", "deprecated": "Merged into tsx: https://tsx.is", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "esbuild": "~0.18.20", @@ -1522,7 +1419,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1539,7 +1435,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1556,7 +1451,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1573,7 +1467,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1590,7 +1483,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1607,7 +1499,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1624,7 +1515,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1641,7 +1531,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1658,7 +1547,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1675,7 +1563,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1692,7 +1579,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1709,7 +1595,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1726,7 +1611,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1743,7 +1627,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1760,7 +1643,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1777,7 +1659,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1794,7 +1675,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1811,7 +1691,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1828,7 +1707,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1845,7 +1723,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1862,7 +1739,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1879,7 +1755,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1893,7 +1768,7 @@ "version": "0.18.20", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -1932,7 +1807,7 @@ "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", "deprecated": "Merged into tsx: https://tsx.is", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", @@ -1946,7 +1821,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1963,7 +1837,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1980,7 +1853,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1997,7 +1869,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2014,7 +1885,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2031,7 +1901,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2048,7 +1917,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2065,7 +1933,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2082,7 +1949,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2099,7 +1965,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2116,7 +1981,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2133,7 +1997,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2150,7 +2013,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2167,7 +2029,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2184,7 +2045,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2201,7 +2061,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2218,7 +2077,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2235,7 +2093,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2252,7 +2109,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2269,7 +2125,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2286,7 +2141,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2303,7 +2157,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2320,7 +2173,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2337,7 +2189,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2354,7 +2205,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2371,7 +2221,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2382,14 +2231,14 @@ } }, "node_modules/@eslint-community/eslint-plugin-eslint-comments": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-4.5.0.tgz", - "integrity": "sha512-MAhuTKlr4y/CE3WYX26raZjy+I/kS2PLKSzvfmDCGrBLTFHOYwqROZdr4XwPgXwX3K9rjzMr4pSmUWGnzsUyMg==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-4.8.1.tgz", + "integrity": "sha512-vafrdi15JHBFh1qWOAucyJwlEEuhdoMof28jOdKdgnwZ7bXwd6v4wzHYSmAVLwnx0yc7bqAtfJ0EZQV1WAzV/Q==", "dev": true, "license": "MIT", "dependencies": { "escape-string-regexp": "^4.0.0", - "ignore": "^5.2.4" + "ignore": "^7.0.5" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2398,13 +2247,23 @@ "url": "https://opencollective.com/eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@eslint-community/eslint-plugin-eslint-comments/node_modules/ignore": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.12.tgz", + "integrity": "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "dev": true, "license": "MIT", "dependencies": { @@ -2434,9 +2293,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -2444,16 +2303,19 @@ } }, "node_modules/@eslint/compat": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.1.tgz", - "integrity": "sha512-k8MHony59I5EPic6EQTCNOuPoVBnoYXkP+20xvwFjN7t0qI3ImyvyBgg+hIVPwC8JaxVjjUZld+cLfBLFDLucg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-2.1.1.tgz", + "integrity": "sha512-rMcy8GSrwNzcISX/BlTDY/GLB4eCopEuy9woIls3To+15OLxykZrxxq+WUcylCPCQ6F4MujjBM1DX5V1aqI3Vw==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "peerDependencies": { - "eslint": "^8.40 || 9" + "eslint": "^8.40 || 9 || 10" }, "peerDependenciesMeta": { "eslint": { @@ -2461,140 +2323,110 @@ } } }, - "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "node_modules/@eslint/compat/node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" + "@types/json-schema": "^7.0.15" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, - "license": "ISC", + "license": "Apache-2.0", "dependencies": { - "brace-expansion": "^1.1.7" + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" }, "engines": { - "node": "*" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "18 || 20 || >=22" } }, - "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.15" + "balanced-match": "^4.0.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "20 || >=22" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "brace-expansion": "^5.0.8" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "18 || 20 || >=22" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" + "@eslint/core": "^1.2.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/@eslint/config-helpers/node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, - "license": "ISC", + "license": "Apache-2.0", "dependencies": { - "brace-expansion": "^1.1.7" + "@types/json-schema": "^7.0.15" }, "engines": { - "node": "*" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/js": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" } }, "node_modules/@eslint/markdown": { @@ -2621,13 +2453,13 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/plugin-kit": { @@ -2645,9 +2477,9 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", - "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "version": "1.14.6", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.6.tgz", + "integrity": "sha512-SgC/B3/VZ8lTyOZ4Cwk6f1skWhmkNAaVfiX90qd5u1rRXGKk1//8FTUEN9GvOUq7rfWIqAYkA3CoyumYVpOS6g==", "license": "Apache-2.0", "dependencies": { "@grpc/proto-loader": "^0.8.0", @@ -2814,7 +2646,7 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@js-sdsl/ordered-map": { @@ -2827,6 +2659,30 @@ "url": "https://opencollective.com/js-sdsl" } }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.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/@levischuck/tiny-cbor": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz", @@ -2874,9 +2730,9 @@ } }, "node_modules/@noble/hashes": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", - "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", "license": "MIT", "engines": { "node": ">= 20.19.0" @@ -2933,9 +2789,9 @@ } }, "node_modules/@opentelemetry/api-logs": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", - "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha512-OlanaW1vv7ufTqQ3/fPLI4arGt5ZoM+P8abOMki6uEYnpRazepSWDwDnnw+la7kE26SHVC18//SMccrDvLKOXQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -2945,12 +2801,12 @@ } }, "node_modules/@opentelemetry/configuration": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/configuration/-/configuration-0.220.0.tgz", - "integrity": "sha512-glfIVKnZevRin8fY/9uES/mhRtMT1lGINLHc9MIo5fTQZXswEEHamJtgjv4MTtzgnhHGC92mIS/0lzAUZMyE0w==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/configuration/-/configuration-0.221.0.tgz", + "integrity": "sha512-uE9y56Zdi9Gt/RdxYnVOo3YmFZkKJJMA0gqtBe8wh8gdtF5Asqe+Oh/TWiDtFb1s+31jNY4CWgnfIB1KOITfFA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.9.0", + "@opentelemetry/core": "2.10.0", "yaml": "^2.8.3" }, "engines": { @@ -2961,9 +2817,9 @@ } }, "node_modules/@opentelemetry/context-async-hooks": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.9.0.tgz", - "integrity": "sha512-OQ0vzvbZBiUhjqLnUaoNfYmP8553Crr3aggB4y0ZUi815mZ7idpdJXQmoKdeBKJelYttoBlLSSHubmyw3wvX4w==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.10.0.tgz", + "integrity": "sha512-bvyMcgLEkozzSzpEEEo1OMoeQ97bxj6Qs2uN3mPrSdDvObMI1myffD/BPqcLlzZO9//d1SqQA/WPw7Cz2AiqhA==", "license": "Apache-2.0", "engines": { "node": "^18.19.0 || >=20.6.0" @@ -2973,9 +2829,9 @@ } }, "node_modules/@opentelemetry/core": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", - "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -2988,17 +2844,15 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.220.0.tgz", - "integrity": "sha512-s0sRPCSlXYqlgObOpCftomJllp3LfUL9FobQ5csg2172ydVhSEnu1ptpsVBJadazs5nUNp7vDuLE03FAFWTLOQ==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.221.0.tgz", + "integrity": "sha512-txG1G0IrYSsKKMeiWZfj/i5cQmWB+h+hf3HzPpF3RqZVwp+iQQEIsv8Vtmzy6RWVdHdJZfygmVrBI39YTBvWcw==", "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/otlp-exporter-base": "0.220.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.220.0", - "@opentelemetry/otlp-transformer": "0.220.0", - "@opentelemetry/sdk-logs": "0.220.0" + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-logs": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3008,13 +2862,13 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.220.0.tgz", - "integrity": "sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.221.0.tgz", + "integrity": "sha512-UFPIq80OH3Ns/oPFHRj14d4DTOxUo+MUFU8hUiCq5jTqFhdeJnfVSANHT+xp92409cA+oxzvlZCe6NM1wvCuBA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/otlp-transformer": "0.220.0" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3024,17 +2878,17 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.220.0.tgz", - "integrity": "sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.221.0.tgz", + "integrity": "sha512-lg6lkOU08Az23jVcn/0Els9HP+V8PnR4Km6p0KgpTggS0n/WuhnmY64rSh83Of9iR9nD+dpWr6adlcX8KzAwjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.220.0", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/sdk-logs": "0.220.0", - "@opentelemetry/sdk-metrics": "2.9.0", - "@opentelemetry/sdk-trace": "2.9.0" + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3044,14 +2898,14 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-logs": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.220.0.tgz", - "integrity": "sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.220.0", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -3061,22 +2915,6 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", - "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, "node_modules/@opentelemetry/exporter-logs-otlp-http": { "version": "0.208.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.208.0.tgz", @@ -3124,14 +2962,14 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-proto": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.220.0.tgz", - "integrity": "sha512-8LZAxdJ0ENDAFwr4j0oY35mHBltiSzvlhdQAPGiC7p9VnxtuSq4SW1gfBAdW6t6hiQG6OwUl8w7KHaOdJPKHWg==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.221.0.tgz", + "integrity": "sha512-AH6EY+47gXFaWYgG3hfeOneGiE9xIZGtDBk+9g0sM8NZWzsQhhmqPbQQXJzS7pyCh5jRRr2nYNXVrkCmoojRvQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/otlp-exporter-base": "0.220.0", - "@opentelemetry/otlp-transformer": "0.220.0", - "@opentelemetry/sdk-logs": "0.220.0" + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-logs": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3141,13 +2979,13 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.220.0.tgz", - "integrity": "sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.221.0.tgz", + "integrity": "sha512-UFPIq80OH3Ns/oPFHRj14d4DTOxUo+MUFU8hUiCq5jTqFhdeJnfVSANHT+xp92409cA+oxzvlZCe6NM1wvCuBA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/otlp-transformer": "0.220.0" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3157,17 +2995,17 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.220.0.tgz", - "integrity": "sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.221.0.tgz", + "integrity": "sha512-lg6lkOU08Az23jVcn/0Els9HP+V8PnR4Km6p0KgpTggS0n/WuhnmY64rSh83Of9iR9nD+dpWr6adlcX8KzAwjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.220.0", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/sdk-logs": "0.220.0", - "@opentelemetry/sdk-metrics": "2.9.0", - "@opentelemetry/sdk-trace": "2.9.0" + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3177,14 +3015,14 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-logs": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.220.0.tgz", - "integrity": "sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.220.0", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -3194,36 +3032,15 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", - "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.220.0.tgz", - "integrity": "sha512-U128izvJfX/dW9jRGP0gIfadR1Hg7ft3UEGIeRxLFK70m2BWw6AtNCOnsUygpw2zCgR/ygdWbGpcL6TmhW0ZGw==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.221.0.tgz", + "integrity": "sha512-KOgCtO15FC6C1T/xOqBcr7EyUs7B+7yomGNb5Y97d3s38rPbCCk5sewkmE2b0/itOkQ/PptX8CLlD+kn2mEtTg==", "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.220.0", - "@opentelemetry/otlp-exporter-base": "0.220.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.220.0", - "@opentelemetry/otlp-transformer": "0.220.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/sdk-metrics": "2.9.0" + "@opentelemetry/exporter-metrics-otlp-http": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3233,16 +3050,16 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/exporter-metrics-otlp-http": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.220.0.tgz", - "integrity": "sha512-Yqt3RBw/bRVncaE9qIIhk4WfjbAQqXuP9FgAaU+IKPndnLEp/cUqZlSC324+bpmduRz7DoTjig8Ub0PeILWXUA==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.221.0.tgz", + "integrity": "sha512-sRfCKbOzgy8xZQV2as0RzIZlnCmCseCKZGLfRcrpo2CBngJDr+rPtX0zkG0+oUCV5kfQPUoW3W3C96Ag3Y/Clg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/otlp-exporter-base": "0.220.0", - "@opentelemetry/otlp-transformer": "0.220.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/sdk-metrics": "2.9.0" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-metrics": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3252,13 +3069,13 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.220.0.tgz", - "integrity": "sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.221.0.tgz", + "integrity": "sha512-UFPIq80OH3Ns/oPFHRj14d4DTOxUo+MUFU8hUiCq5jTqFhdeJnfVSANHT+xp92409cA+oxzvlZCe6NM1wvCuBA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/otlp-transformer": "0.220.0" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3268,17 +3085,17 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.220.0.tgz", - "integrity": "sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.221.0.tgz", + "integrity": "sha512-lg6lkOU08Az23jVcn/0Els9HP+V8PnR4Km6p0KgpTggS0n/WuhnmY64rSh83Of9iR9nD+dpWr6adlcX8KzAwjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.220.0", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/sdk-logs": "0.220.0", - "@opentelemetry/sdk-metrics": "2.9.0", - "@opentelemetry/sdk-trace": "2.9.0" + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3288,14 +3105,14 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/sdk-logs": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.220.0.tgz", - "integrity": "sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.220.0", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -3305,22 +3122,6 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", - "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, "node_modules/@opentelemetry/exporter-metrics-otlp-http": { "version": "0.208.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.208.0.tgz", @@ -3455,14 +3256,14 @@ } }, "node_modules/@opentelemetry/exporter-prometheus": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.220.0.tgz", - "integrity": "sha512-JZD5DL/NBpVd2BHefvYosm3G40UZ/KzExLv5tc0eZe0CtrsHHtcOk3YPUxR2EINmUeBf8+w5UReTV8fFPn95lA==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.221.0.tgz", + "integrity": "sha512-kW79a20qWESIuAdDrxzg9WKM98twV/NBWBFRAH57ap/+ssZhiCo0hckzKT0zpuwR/gSHrFAQhJL0bYDrnEM34g==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/sdk-metrics": "2.9.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-metrics": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -3472,33 +3273,16 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", - "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.220.0.tgz", - "integrity": "sha512-bv1xmNhmNwIM6MdUBw4yYuJeVcEViVLk3uD69vOQMwueHBnfyl/u0HnBlB1FNY/Te0UOzJzvcbyR8wN6b+iGbA==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.221.0.tgz", + "integrity": "sha512-zXminlZedtq9LvOW64CnNkOqk15zV75k8JgtdTuWFge6+jk2m4GmAUm6L2eIiG1o2a2bZxXw2PDrszm+bps0IA==", "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/otlp-exporter-base": "0.220.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.220.0", - "@opentelemetry/otlp-transformer": "0.220.0", - "@opentelemetry/sdk-trace": "2.9.0" + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-trace": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3508,13 +3292,13 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.220.0.tgz", - "integrity": "sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.221.0.tgz", + "integrity": "sha512-UFPIq80OH3Ns/oPFHRj14d4DTOxUo+MUFU8hUiCq5jTqFhdeJnfVSANHT+xp92409cA+oxzvlZCe6NM1wvCuBA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/otlp-transformer": "0.220.0" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3524,17 +3308,17 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.220.0.tgz", - "integrity": "sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.221.0.tgz", + "integrity": "sha512-lg6lkOU08Az23jVcn/0Els9HP+V8PnR4Km6p0KgpTggS0n/WuhnmY64rSh83Of9iR9nD+dpWr6adlcX8KzAwjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.220.0", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/sdk-logs": "0.220.0", - "@opentelemetry/sdk-metrics": "2.9.0", - "@opentelemetry/sdk-trace": "2.9.0" + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3544,14 +3328,14 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-logs": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.220.0.tgz", - "integrity": "sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.220.0", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -3561,22 +3345,6 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", - "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, "node_modules/@opentelemetry/exporter-trace-otlp-http": { "version": "0.208.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.208.0.tgz", @@ -3645,16 +3413,14 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-proto": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.220.0.tgz", - "integrity": "sha512-voTAD8XgJxlK7zLkXh8EzMB09zrQr3tyY/BsnDTlDiQU/UdK58MZ63A3mUjdEDrxMjCVmBHU3WQJhRmQe+Dvzg==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.221.0.tgz", + "integrity": "sha512-Z9i2T7vgZbWe9rSLYxXVIbeW+XyzUq4rZanW3ZyVNwVDqCsh0EJKUgBWWQ0CZfeuUA+RQPzKgJQHMuWAUnKqXw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/otlp-exporter-base": "0.220.0", - "@opentelemetry/otlp-transformer": "0.220.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/sdk-trace": "2.9.0" + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-trace": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3664,13 +3430,13 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.220.0.tgz", - "integrity": "sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.221.0.tgz", + "integrity": "sha512-UFPIq80OH3Ns/oPFHRj14d4DTOxUo+MUFU8hUiCq5jTqFhdeJnfVSANHT+xp92409cA+oxzvlZCe6NM1wvCuBA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/otlp-transformer": "0.220.0" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3680,17 +3446,17 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.220.0.tgz", - "integrity": "sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.221.0.tgz", + "integrity": "sha512-lg6lkOU08Az23jVcn/0Els9HP+V8PnR4Km6p0KgpTggS0n/WuhnmY64rSh83Of9iR9nD+dpWr6adlcX8KzAwjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.220.0", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/sdk-logs": "0.220.0", - "@opentelemetry/sdk-metrics": "2.9.0", - "@opentelemetry/sdk-trace": "2.9.0" + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3700,14 +3466,14 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/sdk-logs": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.220.0.tgz", - "integrity": "sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.220.0", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -3717,31 +3483,15 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", - "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, "node_modules/@opentelemetry/exporter-zipkin": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.9.0.tgz", - "integrity": "sha512-RwINoce2BH8T4obT5pMcAla2sWma1YZvYuaktWmTluQ0PkQdvv5D060rWI1+kawX+J2qBRcMbwrZJJNcMJUauQ==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.10.0.tgz", + "integrity": "sha512-7gsvgf0UDoJ4l9ObrwBmz5G/ZogiPk+lq+g5GpLp24YQF/vPM/BSsnOfcLnfinast5ASUgLo78uSC/ObjlnXgg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/sdk-trace": "2.9.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -3752,12 +3502,12 @@ } }, "node_modules/@opentelemetry/instrumentation": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.220.0.tgz", - "integrity": "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.221.0.tgz", + "integrity": "sha512-cCk80Z/iRDf/5gfsKMB4f74LqVA5yKETB/9ojPzVW/6/f70iu89nJvGxsFCxx4XfSohaOofkU19kiYm84AiAlw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/api-logs": "0.221.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, @@ -3769,13 +3519,13 @@ } }, "node_modules/@opentelemetry/instrumentation-bunyan": { - "version": "0.65.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.65.0.tgz", - "integrity": "sha512-VNqQfK2DY8P5iZTIo/2qS72/fY3DSfUGyRqsfJi8HbQ3WTeWwucKcBUWFF3WMvtt4gNyRpZIk/5qKpBLoDKWZw==", + "version": "0.66.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.66.0.tgz", + "integrity": "sha512-IqYQC1dav35NHlD5nYnpBXK8tI6KJ9/MIt8LYKFxwlhMIwfBCfavaklyP8NtVKoJ0WZKzX2v9Sh+xGK1XvSniQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "^0.220.0", - "@opentelemetry/instrumentation": "^0.220.0", + "@opentelemetry/api-logs": "^0.221.0", + "@opentelemetry/instrumentation": "^0.221.0", "@opentelemetry/semantic-conventions": "^1.41.1", "@types/bunyan": "1.8.11" }, @@ -3787,13 +3537,13 @@ } }, "node_modules/@opentelemetry/instrumentation-console": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-console/-/instrumentation-console-0.2.0.tgz", - "integrity": "sha512-Gh1Z7ZbkPSVhU+gTNmLrAdNyzA2mOMT13qtV9sjO6dSpIQzbmNS58PEN6Mrp4VN5v3MsLmCQHVeiNX8BHpFlww==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-console/-/instrumentation-console-0.3.0.tgz", + "integrity": "sha512-PwMaImP/ZiNzCWio00tL+3RPlHMHb852UOHLG9WYxU18rJuCPRfU40xSc0VuMEd1C7+rNhjjVHM10sWLLlN8KQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "^0.220.0", - "@opentelemetry/instrumentation": "^0.220.0" + "@opentelemetry/api-logs": "^0.221.0", + "@opentelemetry/instrumentation": "^0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3803,13 +3553,13 @@ } }, "node_modules/@opentelemetry/instrumentation-http": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.220.0.tgz", - "integrity": "sha512-Szt4dO2Boz2CDr38DaSw/lnqwhwKl+IAdgNGEGgSm2Anb+fwPtIAGmIwkhsLLN69QQZQE96JxjMKYY4rlRkYKw==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.221.0.tgz", + "integrity": "sha512-oIP91CPIANuYr09tGFElPFKAh6JUar+awJf1kBRYlaeo9b0gDwZHEB2zBfFlvdNFHm0wAVutMZODVi5smKT30g==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/instrumentation": "0.220.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/instrumentation": "0.221.0", "@opentelemetry/semantic-conventions": "^1.29.0", "forwarded-parse": "2.1.2" }, @@ -3821,12 +3571,12 @@ } }, "node_modules/@opentelemetry/instrumentation-mongodb": { - "version": "0.73.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.73.0.tgz", - "integrity": "sha512-M61+VpaXaLj7euluV+jARBo62tBWrpubSUPIZMpUnjOM90nqCQCj8gpyhDP1rVgzQt2LoTIzcdWnKq/DEkzMog==", + "version": "0.74.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.74.0.tgz", + "integrity": "sha512-GRnHu69YLQUYgguuYkKi6wpizMY4r7gLC08rSq8cg41p6t7+1YIy5nXoGC61NA7KCZUE9jbcDnzd45i32IblZg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.220.0", + "@opentelemetry/instrumentation": "^0.221.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "engines": { @@ -3837,12 +3587,12 @@ } }, "node_modules/@opentelemetry/instrumentation-mysql": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.66.0.tgz", - "integrity": "sha512-kfbyFeHOV/RzmRifWJflpBTCrYz4vD5j8IVqjSreaAPGOvKHj/fflwqNwdi7cy4QRmm7vVkxqTvM7q0+ZF58CA==", + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.67.0.tgz", + "integrity": "sha512-G4aRrVKcd2Aodqi7WzRZ3LQJNKrM8BpsXEVMHrOb9s8Lg0jZXNvlLY0NWL1yVJrjnqI/unwEsp2aVHkVImPMeA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.220.0", + "@opentelemetry/instrumentation": "^0.221.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@types/mysql": "2.15.27" }, @@ -3854,13 +3604,13 @@ } }, "node_modules/@opentelemetry/instrumentation-pg": { - "version": "0.72.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.72.0.tgz", - "integrity": "sha512-p9xrFc/6R8t6Y293sTYLZ83LnzZo/qY0bBPA4xabdQt0Qjt8i1SlYFsIeGY2Jmf5WcESNUdjQB3NxWnt5Ox7zw==", + "version": "0.73.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.73.0.tgz", + "integrity": "sha512-yf3tBVwLHB9cZNNPSToNrthx36ouPe4FctFxy7ya6vSJ6gaiKjNfA/IgFFeuBpZflEQhy6aesPqzZo8ZjFkvNg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.220.0", + "@opentelemetry/core": "^2.9.0", + "@opentelemetry/instrumentation": "^0.221.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@opentelemetry/sql-common": "^0.42.0", "@types/pg": "8.15.6", @@ -3874,12 +3624,12 @@ } }, "node_modules/@opentelemetry/instrumentation-redis": { - "version": "0.68.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.68.0.tgz", - "integrity": "sha512-D5N4CLWLjBRFa1Ee8D1U1tWCkED+Ob0AMzQlYJjlnnqfw0ofRMaJmoUrdI5ASKEcfDezzhELT1Slo4VesDpA/w==", + "version": "0.69.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.69.0.tgz", + "integrity": "sha512-lyCIEW89cYhMwaUSMBzsKHdwH2wOoqmuwXOARJneo9UL55govLIUCbYYAJ457oM8kdKADymlY4+SUW0DKQeIHw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.220.0", + "@opentelemetry/instrumentation": "^0.221.0", "@opentelemetry/redis-common": "^0.38.3", "@opentelemetry/semantic-conventions": "^1.27.0" }, @@ -3891,13 +3641,13 @@ } }, "node_modules/@opentelemetry/instrumentation-winston": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.64.0.tgz", - "integrity": "sha512-N4dJ0Var+deL2FKQn2TNPKLma8c/vgR0dL89I57eNBcV+5rGj3tk4glSDJqd9BQqkKuZ8+C4bAlCtVHHBsqdKw==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.65.0.tgz", + "integrity": "sha512-hWSPnS530deRa+ttzY+QiGmgsK7aQHpqxbQRm56yO1j4qnIXuYYHaoSJ8/4lLOEcXB+hZs0mAAJ6QiMl1aaiGA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "^0.220.0", - "@opentelemetry/instrumentation": "^0.220.0" + "@opentelemetry/api-logs": "^0.221.0", + "@opentelemetry/instrumentation": "^0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3938,15 +3688,15 @@ } }, "node_modules/@opentelemetry/otlp-grpc-exporter-base": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.220.0.tgz", - "integrity": "sha512-/eIkBPMBTIvM3x/0mDX4aJeSkYifYClnBPr68PL1h5LV4VQv4+SV6CGrpiZ4fIWDnobVmhTWCm1J/QRdAWUfvA==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.221.0.tgz", + "integrity": "sha512-rQDmNgyiGCTrescjnzH2ntVyUKVIq6I2UjuK8+stT/Xg0ZOT71FVJqwjFdspQl6Yol/Yqsut9bDo+ame8oTmDQ==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/otlp-exporter-base": "0.220.0", - "@opentelemetry/otlp-transformer": "0.220.0" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3956,13 +3706,13 @@ } }, "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.220.0.tgz", - "integrity": "sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.221.0.tgz", + "integrity": "sha512-UFPIq80OH3Ns/oPFHRj14d4DTOxUo+MUFU8hUiCq5jTqFhdeJnfVSANHT+xp92409cA+oxzvlZCe6NM1wvCuBA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/otlp-transformer": "0.220.0" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3972,17 +3722,17 @@ } }, "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.220.0.tgz", - "integrity": "sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.221.0.tgz", + "integrity": "sha512-lg6lkOU08Az23jVcn/0Els9HP+V8PnR4Km6p0KgpTggS0n/WuhnmY64rSh83Of9iR9nD+dpWr6adlcX8KzAwjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.220.0", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/sdk-logs": "0.220.0", - "@opentelemetry/sdk-metrics": "2.9.0", - "@opentelemetry/sdk-trace": "2.9.0" + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3992,14 +3742,14 @@ } }, "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/sdk-logs": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.220.0.tgz", - "integrity": "sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.220.0", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4009,22 +3759,6 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", - "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, "node_modules/@opentelemetry/otlp-transformer": { "version": "0.208.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz", @@ -4123,12 +3857,12 @@ } }, "node_modules/@opentelemetry/propagator-b3": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.9.0.tgz", - "integrity": "sha512-WrOT1WsOUG+B7hstD2RYoMPIOK76G8E9AQHhMjUvrQaGx/oA7rPWQvvr1Rqv7+yy4R0ZMVwWLC4vW2xnkgWPAQ==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.10.0.tgz", + "integrity": "sha512-GnA5B24H+1w8BO21J0q+IWNB0z1v+AGbcquTdIt/dufibhnhgxaA8YKvz0I3akRZhB1jHT+/tlzK+qlAjEDybQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.9.0" + "@opentelemetry/core": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4138,12 +3872,12 @@ } }, "node_modules/@opentelemetry/propagator-jaeger": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.9.0.tgz", - "integrity": "sha512-4mYGty27rYvSM0jtp1ZUOqd3LfVRCYg9H5G9OFzSx5HViYToU21MFhWfco7x1HwXr7ER8yGOiCIHZUwjPksc0Q==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.10.0.tgz", + "integrity": "sha512-yw/IX8DL470dSMZJoE82ScfYGp7JWZ/G8kFJo35ZILUVTB2jFPTOaioN+8s09pH0RHsWNhweVZb+ZnjJJpCChg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.9.0" + "@opentelemetry/core": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4162,9 +3896,9 @@ } }, "node_modules/@opentelemetry/resource-detector-azure": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.28.0.tgz", - "integrity": "sha512-YMMgH/ZIiZgy2CHTHjOBNEYhcsi/l67Q272vAgwMqegRFIME4KljDhmTmjLGzjE3b0sErLtXBqF8Y/3bKn89+Q==", + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.29.0.tgz", + "integrity": "sha512-lWm0vjjlQMoc4Xvvd+dW/OZWT/SI4w+cIN7kbm8KimIZCr1EpAyvyQ7WEOrGoBoXCpQCrsZ18uKTooDgiBCGIw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", @@ -4179,12 +3913,12 @@ } }, "node_modules/@opentelemetry/resources": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", - "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.9.0", + "@opentelemetry/core": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4270,69 +4004,38 @@ "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz", - "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.10.0.tgz", - "integrity": "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==", + "node_modules/@opentelemetry/sdk-node": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.221.0.tgz", + "integrity": "sha512-UbYuvtBrQQB5Prsh9KOKy4kxzexFxfMs5MkteHeWMoswsEB7kiNhyUVkAOFW/qsEzNHtrkgyghrD2ilZJa+5YA==", "license": "Apache-2.0", "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/configuration": "0.221.0", + "@opentelemetry/context-async-hooks": "2.10.0", "@opentelemetry/core": "2.10.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-node": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.220.0.tgz", - "integrity": "sha512-wHtGyHhSKHNH3fym33xRu4Ef/HXTFvX8eQ42xdQdEO9LYx9Y2qNyBDJytyqVlvmo6abWZlNYTUthuAGUMYqYnQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.220.0", - "@opentelemetry/configuration": "0.220.0", - "@opentelemetry/context-async-hooks": "2.9.0", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/exporter-logs-otlp-grpc": "0.220.0", - "@opentelemetry/exporter-logs-otlp-http": "0.220.0", - "@opentelemetry/exporter-logs-otlp-proto": "0.220.0", - "@opentelemetry/exporter-metrics-otlp-grpc": "0.220.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.220.0", - "@opentelemetry/exporter-metrics-otlp-proto": "0.220.0", - "@opentelemetry/exporter-prometheus": "0.220.0", - "@opentelemetry/exporter-trace-otlp-grpc": "0.220.0", - "@opentelemetry/exporter-trace-otlp-http": "0.220.0", - "@opentelemetry/exporter-trace-otlp-proto": "0.220.0", - "@opentelemetry/exporter-zipkin": "2.9.0", - "@opentelemetry/instrumentation": "0.220.0", - "@opentelemetry/otlp-exporter-base": "0.220.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.220.0", - "@opentelemetry/propagator-b3": "2.9.0", - "@opentelemetry/propagator-jaeger": "2.9.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/sdk-logs": "0.220.0", - "@opentelemetry/sdk-metrics": "2.9.0", - "@opentelemetry/sdk-trace": "2.9.0", - "@opentelemetry/sdk-trace-base": "2.9.0", - "@opentelemetry/sdk-trace-node": "2.9.0", + "@opentelemetry/exporter-logs-otlp-grpc": "0.221.0", + "@opentelemetry/exporter-logs-otlp-http": "0.221.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.221.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.221.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.221.0", + "@opentelemetry/exporter-metrics-otlp-proto": "0.221.0", + "@opentelemetry/exporter-prometheus": "0.221.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.221.0", + "@opentelemetry/exporter-trace-otlp-http": "0.221.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.221.0", + "@opentelemetry/exporter-zipkin": "2.10.0", + "@opentelemetry/instrumentation": "0.221.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/propagator-b3": "2.10.0", + "@opentelemetry/propagator-jaeger": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0", + "@opentelemetry/sdk-trace-base": "2.10.0", + "@opentelemetry/sdk-trace-node": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4343,16 +4046,14 @@ } }, "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-logs-otlp-http": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.220.0.tgz", - "integrity": "sha512-8186thl+pTw64iz/qEEen5oJZoZ/gO73XruChdaGlYdWOdBIQ42r+vHLf6a7vIDqTD4b8ZOoMlyxptanECaI9A==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.221.0.tgz", + "integrity": "sha512-nKXkr4Tomi6fjYVOf+ytcW3dZAVr4v4Bv5gsT6dr2gvpUPJpKgHB4XbMufMsPotRE3g0XH2GwVVCkN2w6SON+Q==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.220.0", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/otlp-exporter-base": "0.220.0", - "@opentelemetry/otlp-transformer": "0.220.0", - "@opentelemetry/sdk-logs": "0.220.0" + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-logs": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4362,16 +4063,16 @@ } }, "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-metrics-otlp-http": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.220.0.tgz", - "integrity": "sha512-Yqt3RBw/bRVncaE9qIIhk4WfjbAQqXuP9FgAaU+IKPndnLEp/cUqZlSC324+bpmduRz7DoTjig8Ub0PeILWXUA==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.221.0.tgz", + "integrity": "sha512-sRfCKbOzgy8xZQV2as0RzIZlnCmCseCKZGLfRcrpo2CBngJDr+rPtX0zkG0+oUCV5kfQPUoW3W3C96Ag3Y/Clg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/otlp-exporter-base": "0.220.0", - "@opentelemetry/otlp-transformer": "0.220.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/sdk-metrics": "2.9.0" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-metrics": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4381,17 +4082,14 @@ } }, "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-metrics-otlp-proto": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.220.0.tgz", - "integrity": "sha512-lyO+IQBdSvqHN/ZOW/OzrSWemtfD+HgWngn+HBNLhjy0YrCQQTz0OE/kSekH2Pl340dn9DWzhqHdz5Eftr+HLA==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.221.0.tgz", + "integrity": "sha512-YMF4LveY2I3yhw61rn6nmC9FE8U24IZHPeKU1Duc5+sbwjMd8FwZAwba318ImdThCg/HuVQvhm2y6bfgNPnfYg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.220.0", - "@opentelemetry/otlp-exporter-base": "0.220.0", - "@opentelemetry/otlp-transformer": "0.220.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/sdk-metrics": "2.9.0" + "@opentelemetry/exporter-metrics-otlp-http": "0.221.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4401,16 +4099,14 @@ } }, "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-trace-otlp-http": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.220.0.tgz", - "integrity": "sha512-/+ExB3lRkf+erv4PnoywyL7RHKITidxtUpUTS55k7OQ0dB42S7gEF1gry7swb9MSm1hYLUhJg4QQh9W8SpwwqA==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.221.0.tgz", + "integrity": "sha512-AySXiKoC+meiWm6zdVj5T2LnPDZuatveBby1cMOeQteIWsYXAUxs8Sru13G2pVSPrUXz6vF+og7QVBX6GdC/oQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/otlp-exporter-base": "0.220.0", - "@opentelemetry/otlp-transformer": "0.220.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/sdk-trace": "2.9.0" + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-trace": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4420,13 +4116,13 @@ } }, "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.220.0.tgz", - "integrity": "sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.221.0.tgz", + "integrity": "sha512-UFPIq80OH3Ns/oPFHRj14d4DTOxUo+MUFU8hUiCq5jTqFhdeJnfVSANHT+xp92409cA+oxzvlZCe6NM1wvCuBA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/otlp-transformer": "0.220.0" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4436,17 +4132,17 @@ } }, "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.220.0.tgz", - "integrity": "sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.221.0.tgz", + "integrity": "sha512-lg6lkOU08Az23jVcn/0Els9HP+V8PnR4Km6p0KgpTggS0n/WuhnmY64rSh83Of9iR9nD+dpWr6adlcX8KzAwjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.220.0", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/sdk-logs": "0.220.0", - "@opentelemetry/sdk-metrics": "2.9.0", - "@opentelemetry/sdk-trace": "2.9.0" + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4456,14 +4152,14 @@ } }, "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-logs": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.220.0.tgz", - "integrity": "sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.220.0", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4473,30 +4169,14 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", - "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, "node_modules/@opentelemetry/sdk-trace": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.9.0.tgz", - "integrity": "sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz", + "integrity": "sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4507,14 +4187,14 @@ } }, "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.9.0.tgz", - "integrity": "sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.10.0.tgz", + "integrity": "sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/sdk-trace": "2.9.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4525,14 +4205,14 @@ } }, "node_modules/@opentelemetry/sdk-trace-node": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.9.0.tgz", - "integrity": "sha512-ec9a7ps37huy5itYk0MalaZdSLlM6AXWp/FhtEjgMpp5leEGojBDvAl/UWttQnkMZOvFHKzRESn8TD3yKTF5nQ==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.10.0.tgz", + "integrity": "sha512-GZK/G6oZyBLGlH1pUgeDch7D91KoHd2uotUGIkWCPi9GI5T9X0p4L7nNAMDR1BQjkRYoDqo+ddfVx9t5Uhys+Q==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/context-async-hooks": "2.9.0", - "@opentelemetry/core": "2.9.0", - "@opentelemetry/sdk-trace-base": "2.9.0" + "@opentelemetry/context-async-hooks": "2.10.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/sdk-trace-base": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4630,12 +4310,12 @@ } }, "node_modules/@opentelemetry/winston-transport": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/winston-transport/-/winston-transport-0.30.0.tgz", - "integrity": "sha512-sBuqGxyOBIRmzVtzI7K67W2KZwmjHr8nVZPCi6A3uwei8hGlgnjqVWdgX9wIjcvGxg5qs3wkOeaLWuFz/MsOYA==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/winston-transport/-/winston-transport-0.31.0.tgz", + "integrity": "sha512-higyMoGHbDTNlCX5ue1dJjDQuYSWwC0L4k6x57RKLG0uXdCIdyIu/OQYnjciL+7Kv2XmPMy4VQloeh36NTJTuQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "^0.220.0", + "@opentelemetry/api-logs": "^0.221.0", "@opentelemetry/semantic-conventions": "^1.41.1", "winston-transport": "4.*" }, @@ -5175,7 +4855,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5189,7 +4868,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5203,7 +4881,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5217,7 +4894,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5231,7 +4907,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5245,7 +4920,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5259,7 +4933,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5273,7 +4946,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5287,7 +4959,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5301,7 +4972,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5315,7 +4985,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5329,7 +4998,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5343,7 +5011,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5357,7 +5024,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5371,7 +5037,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5385,7 +5050,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5399,7 +5063,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5413,7 +5076,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5427,7 +5089,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5441,7 +5102,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5455,7 +5115,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5469,7 +5128,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5483,7 +5141,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5497,7 +5154,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5511,7 +5167,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5784,7 +5439,7 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/deep-eql": "*" @@ -5794,7 +5449,7 @@ "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/ms": "*" @@ -5804,6 +5459,13 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", "dev": true, "license": "MIT" }, @@ -5811,7 +5473,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/json-schema": { @@ -5835,7 +5497,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/mysql": { @@ -5900,21 +5562,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", - "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.2.tgz", + "integrity": "sha512-K72/pvT7qpm8q3k8G5u93NCSN1RcLxX5h7Gw0bQpbMXviBHeY6Nx1WEwmk9wP+eKTSQ8ecxvcgt/ZX/rB938xQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.38.0", - "@typescript-eslint/type-utils": "8.38.0", - "@typescript-eslint/utils": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/type-utils": "8.71.2", + "@typescript-eslint/utils": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5924,15 +5585,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.38.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "@typescript-eslint/parser": "^8.71.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.12.tgz", + "integrity": "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==", "dev": true, "license": "MIT", "engines": { @@ -5940,17 +5601,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz", - "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.2.tgz", + "integrity": "sha512-aTtmZjp/x3gsGHkDs04/1I8kNogBJkpQaeGaN0PfiG43NzDEmW2n+OP5im9wfsvyX0yP1UZWBrAGtDBfH9jEVw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.38.0", - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5960,20 +5621,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", - "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.2.tgz", + "integrity": "sha512-Pd68GsLVh5ZXlcEccFPdhSOvtHZ/2WU0XsA8zmXOS3F158lVlcTLhfQ6qvtL1RzbMie0Kp8xtbVwAg4nejKPHw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.38.0", - "@typescript-eslint/types": "^8.38.0", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.71.2", + "@typescript-eslint/types": "^8.71.2", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5983,18 +5644,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", - "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.2.tgz", + "integrity": "sha512-YF4wgg5iUX5Obywoac27B56FhozMNFhdJSbh11H5GBAwmBtfyRhmgTQqW9tRNNmHDvsZ2CX6UGzoz+YfYn2Leg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0" + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6005,9 +5666,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", - "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.2.tgz", + "integrity": "sha512-jz49TAWokm/7yWD9/o837nYax9DyALxIPOImdImvVzo6canBCZYzin8zJyqNR5k1j9K1DE0bVvr0XYB+99uAdg==", "dev": true, "license": "MIT", "engines": { @@ -6018,21 +5679,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", - "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.2.tgz", + "integrity": "sha512-cC8UaalCViJkb3rH/xLIvidEwoFRtMZMUqDjjJMGEKaTFwYRHYBcoczlEGHuvkHEO8svRDrgZ0yQgOArFmwang==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0", - "@typescript-eslint/utils": "8.38.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/utils": "8.71.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6042,14 +5703,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz", - "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.2.tgz", + "integrity": "sha512-y5Ukl3oVp7e/VGxqIoEXoeb/Roj71PAJ4WG/N/RGj+eeDvKRLM15kGr9MijLbN0i03S52zxu31Z98E5gWChJKw==", "dev": true, "license": "MIT", "engines": { @@ -6061,22 +5722,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", - "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.2.tgz", + "integrity": "sha512-DgNOEsl4pdYzwSL+N/Cl33tX1Hlw3FvId73LnKXfq7jnrF2viOEn7X+P7RBkkSWCVm92gnWd28GIC5vJuLREeA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.38.0", - "@typescript-eslint/tsconfig-utils": "8.38.0", - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/project-service": "8.71.2", + "@typescript-eslint/tsconfig-utils": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6086,20 +5746,59 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz", - "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.2.tgz", + "integrity": "sha512-RyKa1G1M9rKHDhrk+HJtiF1MvLD5Ux2JW3TSkzw+02pRALdpJb+QzG1Y5em+LvCgzGwgg8H7mBRFKljNz7ImPw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.38.0", - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6109,19 +5808,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", - "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.2.tgz", + "integrity": "sha512-zozlN0UA32bSG2zkKAgQyRYSbEISVoyU6vvkKYo2aSny74D8Jh4e+POkJ6Lk0dlezZi5x7TtD+88Xb06IB8nhg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.38.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.71.2", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6131,6 +5830,19 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@typespec/ts-http-runtime": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.5.tgz", @@ -6207,7 +5919,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", @@ -6224,7 +5936,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@vitest/spy": "3.2.4", @@ -6251,7 +5963,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" @@ -6261,7 +5973,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tinyrainbow": "^2.0.0" @@ -6274,7 +5986,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@vitest/utils": "3.2.4", @@ -6289,7 +6001,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@vitest/pretty-format": "3.2.4", @@ -6304,7 +6016,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tinyspy": "^4.0.3" @@ -6317,7 +6029,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@vitest/pretty-format": "3.2.4", @@ -6328,6 +6040,72 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vue/compiler-core": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.44.tgz", + "integrity": "sha512-k34nCytbK8PQ1gR2mGROYOw6Pnv/mumqIdrZ3x2tbyXZDWm7ViKGLTfdB+DRrWqEQsLgGJvn0WN9jBif5zCTLQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^7.29.9", + "@vue/shared": "3.5.44", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.2" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.44.tgz", + "integrity": "sha512-W5AoBFWdw1lQAKiHowkfgFKFab82oSRT6sGsPfw/q1GRjsy/ZJFLHGhJyBRIFN9APLTUduDH9U2JAiPOIyKuYg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-core": "3.5.44", + "@vue/shared": "3.5.44" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.44.tgz", + "integrity": "sha512-dzHmQ4jXTpdr5FjIQUeykVakN/uzqFbUo9/C5ikBFg3a3PeM6uRxeczx1ZbXBML0/zJstEh3ygYipzR5Z7b2Pw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^7.29.9", + "@vue/compiler-core": "3.5.44", + "@vue/compiler-dom": "3.5.44", + "@vue/compiler-ssr": "3.5.44", + "@vue/shared": "3.5.44", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.29", + "source-map-js": "^1.2.2" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.44.tgz", + "integrity": "sha512-xr5gb1JVHlfXJwSlRz1FeP6uxoazdzHuMTVLE7up+4Vrqxk219CvxLPxD9NXBt47yvZW7+p4DXfyp4O1fkByIA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.44", + "@vue/shared": "3.5.44" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.44.tgz", + "integrity": "sha512-H82XLH0DjmSOdbEwO15x3QTo1f5PWEtv237eLpYKc0mqLu+B8hnq16lFWmuBmNurkeiG3wr+oM1FhOll/YVFRg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@xmldom/xmldom": { "version": "0.8.15", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz", @@ -6350,9 +6128,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -6393,9 +6171,9 @@ } }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -6565,13 +6343,6 @@ "node": ">=14" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -6600,7 +6371,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -6714,26 +6485,26 @@ } }, "node_modules/better-auth": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.6.9.tgz", - "integrity": "sha512-EBFURtglyiEZxbx4NJBoqUD8J65dX24yC+6I9AUbIXNgUkt76mshzGbHkxZ3n/lB7Dwq3kBC+hHt0hUQsnL7HA==", - "license": "MIT", - "dependencies": { - "@better-auth/core": "1.6.9", - "@better-auth/drizzle-adapter": "1.6.9", - "@better-auth/kysely-adapter": "1.6.9", - "@better-auth/memory-adapter": "1.6.9", - "@better-auth/mongo-adapter": "1.6.9", - "@better-auth/prisma-adapter": "1.6.9", - "@better-auth/telemetry": "1.6.9", - "@better-auth/utils": "0.4.0", - "@better-fetch/fetch": "1.1.21", + "version": "1.6.30", + "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.6.30.tgz", + "integrity": "sha512-+fmPXSZFE7MDmLcppkmzUGMtQxzZXsJbKi5rUunmIYtSQZIgNCZHNSQFwK/ywx10uczkzbtVkrHm75i4o7+gpQ==", + "license": "MIT", + "dependencies": { + "@better-auth/core": "1.6.30", + "@better-auth/drizzle-adapter": "1.6.30", + "@better-auth/kysely-adapter": "1.6.30", + "@better-auth/memory-adapter": "1.6.30", + "@better-auth/mongo-adapter": "1.6.30", + "@better-auth/prisma-adapter": "1.6.30", + "@better-auth/telemetry": "1.6.30", + "@better-auth/utils": "0.4.2", + "@better-fetch/fetch": "1.3.1", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", - "better-call": "1.3.5", + "better-call": "1.4.0", "defu": "^6.1.4", "jose": "^6.1.3", - "kysely": "^0.28.14", + "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.1.1", "zod": "^4.3.6" }, @@ -6828,15 +6599,15 @@ } }, "node_modules/better-call": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.3.5.tgz", - "integrity": "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.4.0.tgz", + "integrity": "sha512-bBKOT4vv1kZLDgxVePdilk/Jwkn+dtRRsmi3DzHcDP+WnswyVl6dR59l2HEeP/0cB+bDoopASAesWDPIdd/zZA==", "license": "MIT", "dependencies": { - "@better-auth/utils": "^0.4.0", - "@better-fetch/fetch": "^1.1.21", - "rou3": "^0.7.12", - "set-cookie-parser": "^3.0.1" + "@better-auth/utils": "^0.5.0", + "@better-fetch/fetch": "^1.3.1", + "rou3": "^0.9.1", + "set-cookie-parser": "^3.1.2" }, "peerDependencies": { "zod": "^4.0.0" @@ -6847,6 +6618,15 @@ } } }, + "node_modules/better-call/node_modules/@better-auth/utils": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.5.0.tgz", + "integrity": "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.0.1" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -6972,7 +6752,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/builtin-modules": { @@ -7007,12 +6787,26 @@ "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, "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", @@ -7026,16 +6820,6 @@ "node": ">= 0.4" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001810", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", @@ -7072,7 +6856,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "assertion-error": "^2.0.1", @@ -7085,23 +6869,6 @@ "node": ">=18" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/change-case": { "version": "5.4.4", "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", @@ -7124,7 +6891,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 16" @@ -7358,13 +7125,6 @@ "node": ">= 14" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/confbox": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", @@ -7495,9 +7255,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -7529,7 +7289,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -7680,10 +7440,10 @@ } }, "node_modules/drizzle-kit": { - "version": "0.31.10", - "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz", - "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==", - "dev": true, + "version": "0.31.11", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.11.tgz", + "integrity": "sha512-YCYqxTLIB2OCqf6w9/Vef13baBVbwQIPcbT4Y56AfO6B9ajZhDhD8Jkveg/hJvT/iuMloKD/IYYkyMMNhr7Kqg==", + "devOptional": true, "license": "MIT", "dependencies": { "@drizzle-team/brocli": "^0.10.2", @@ -7872,6 +7632,16 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, + "node_modules/empathic": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.1.0.tgz", + "integrity": "sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -7896,6 +7666,20 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -7918,7 +7702,7 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/es-object-atoms": { @@ -7952,7 +7736,7 @@ "version": "0.25.8", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -8013,45 +7797,43 @@ } }, "node_modules/eslint": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", - "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.12.0.tgz", + "integrity": "sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==", "dev": true, "license": "MIT", + "workspaces": [ + "packages/*" + ], "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.31.0", - "@eslint/plugin-kit": "^0.3.1", + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", + "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", + "file-entry-cache": "11.1.5 || >11.1.6 <12", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -8059,7 +7841,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" @@ -8090,19 +7872,19 @@ } }, "node_modules/eslint-config-flat-gitignore": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-flat-gitignore/-/eslint-config-flat-gitignore-2.1.0.tgz", - "integrity": "sha512-cJzNJ7L+psWp5mXM7jBX+fjHtBvvh06RBlcweMhKD8jWqQw0G78hOW5tpVALGHGFPsBV+ot2H+pdDGJy6CV8pA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/eslint-config-flat-gitignore/-/eslint-config-flat-gitignore-2.4.0.tgz", + "integrity": "sha512-JhYC+V7qEDiCDsbJcVP8fxNc62fk4+i91YLP/YvmVHlkaQ8Xo99olYN8MO5COdxl7onGlsrfZVONe5NYsZP1UA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint/compat": "^1.2.5" + "@eslint/compat": "^2.1.0" }, "funding": { "url": "https://github.com/sponsors/antfu" }, "peerDependencies": { - "eslint": "^9.5.0" + "eslint": "^9.5.0 || ^10.0.0" } }, "node_modules/eslint-config-prettier": { @@ -8151,9 +7933,9 @@ } }, "node_modules/eslint-json-compat-utils": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/eslint-json-compat-utils/-/eslint-json-compat-utils-0.2.1.tgz", - "integrity": "sha512-YzEodbDyW8DX8bImKhAcCeu/L31Dd/70Bidx2Qex9OFUtgzXLqtfWL4Hr5fM/aCCB8QUZLuJur0S9k6UfgFkfg==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/eslint-json-compat-utils/-/eslint-json-compat-utils-0.2.3.tgz", + "integrity": "sha512-RbBmDFyu7FqnjE8F0ZxPNzx5UaptdeS9Uu50r7A+D7s/+FCX+ybiyViYEgFUaFIFqSWJgZRTpL5d8Kanxxl2lQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8164,7 +7946,7 @@ }, "peerDependencies": { "eslint": "*", - "jsonc-eslint-parser": "^2.4.0" + "jsonc-eslint-parser": "^2.4.0 || ^3.0.0" }, "peerDependenciesMeta": { "@eslint/json": { @@ -8272,78 +8054,36 @@ "eslint-formatting-reporter": "^0.0.0", "eslint-parser-plain": "^0.1.1", "prettier": "^3.4.2", - "synckit": "^0.9.2" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "eslint": "^8.40.0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-import-lite": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import-lite/-/eslint-plugin-import-lite-0.3.0.tgz", - "integrity": "sha512-dkNBAL6jcoCsXZsQ/Tt2yXmMDoNt5NaBh/U7yvccjiK8cai6Ay+MK77bMykmqQA2bTF6lngaLCDij6MTO3KkvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/types": "^8.34.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "peerDependencies": { - "eslint": ">=9.0.0", - "typescript": ">=4.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-jsdoc": { - "version": "51.4.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-51.4.1.tgz", - "integrity": "sha512-y4CA9OkachG8v5nAtrwvcvjIbdcKgSyS6U//IfQr4FZFFyeBFwZFf/tfSsMr46mWDJgidZjBTqoCRlXywfFBMg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@es-joy/jsdoccomment": "~0.52.0", - "are-docs-informative": "^0.0.2", - "comment-parser": "1.4.1", - "debug": "^4.4.1", - "escape-string-regexp": "^4.0.0", - "espree": "^10.4.0", - "esquery": "^1.6.0", - "parse-imports-exports": "^0.2.4", - "semver": "^7.7.2", - "spdx-expression-parse": "^4.0.0" + "synckit": "^0.9.2" }, - "engines": { - "node": ">=20.11.0" + "funding": { + "url": "https://github.com/sponsors/antfu" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + "eslint": "^8.40.0 || ^9.0.0" } }, - "node_modules/eslint-plugin-jsdoc/node_modules/@es-joy/jsdoccomment": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.52.0.tgz", - "integrity": "sha512-BXuN7BII+8AyNtn57euU2Yxo9yA/KUDNzrpXyi3pfqKmBhhysR6ZWOebFh3vyPoqA3/j1SOvGgucElMGwlXing==", + "node_modules/eslint-plugin-import-lite": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-lite/-/eslint-plugin-import-lite-0.3.0.tgz", + "integrity": "sha512-dkNBAL6jcoCsXZsQ/Tt2yXmMDoNt5NaBh/U7yvccjiK8cai6Ay+MK77bMykmqQA2bTF6lngaLCDij6MTO3KkvA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "^1.0.8", - "@typescript-eslint/types": "^8.34.1", - "comment-parser": "1.4.1", - "esquery": "^1.6.0", - "jsdoc-type-pratt-parser": "~4.1.0" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/types": "^8.34.0" }, "engines": { - "node": ">=20.11.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=4.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/eslint-plugin-jsonc": { @@ -8470,9 +8210,9 @@ } }, "node_modules/eslint-plugin-pnpm": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-pnpm/-/eslint-plugin-pnpm-1.1.0.tgz", - "integrity": "sha512-sL93w0muBtjnogzk/loDsxzMbmXQOLP5Blw3swLDBXZgfb+qQI73bPcUbjVR+ZL+K62vGJdErV+43i3r5DsZPg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-pnpm/-/eslint-plugin-pnpm-1.9.1.tgz", + "integrity": "sha512-8++1Egww2cqc9Wylmt7P+xXNMQVCosAZ4m4W4Gc0V5lGea8HFByHTRvUI+ZvvpO8fLXGg2O4MQc90EhuI3mMdQ==", "dev": true, "funding": [ { @@ -8486,15 +8226,65 @@ ], "license": "MIT", "dependencies": { - "find-up-simple": "^1.0.1", - "jsonc-eslint-parser": "^2.4.0", + "empathic": "^2.0.1", + "eslint-json-compat-utils": "^0.2.3", + "jsonc-eslint-parser": "^3.3.0", "pathe": "^2.0.3", - "pnpm-workspace-yaml": "1.1.0", - "tinyglobby": "^0.2.14", - "yaml-eslint-parser": "^1.3.0" + "pnpm-workspace-yaml": "1.9.1", + "tinyglobby": "^0.2.17", + "yaml": "^2.9.0", + "yaml-eslint-parser": "^2.1.0" }, "peerDependencies": { - "eslint": "^9.0.0" + "eslint": "^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-pnpm/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-pnpm/node_modules/jsonc-eslint-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/jsonc-eslint-parser/-/jsonc-eslint-parser-3.3.0.tgz", + "integrity": "sha512-hYTGkHGNRZnXOFZ1urhINADoqDrGfpy53cjw+dxk84QE0pUDujQzeUeamNs6Mz44/TKD49z2x6/GVSu4ZrtA+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.5.0", + "eslint-visitor-keys": "^5.0.0", + "verkit": "^0.3.2" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + } + }, + "node_modules/eslint-plugin-pnpm/node_modules/yaml-eslint-parser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/yaml-eslint-parser/-/yaml-eslint-parser-2.1.0.tgz", + "integrity": "sha512-1zo9KRfp6vIAXBEhWAz09ex3hUwh3T+/6dGbGteHvNseA0Uk4FgQnPES55klZ6cDzSy9FpgKS0D/CoLUvCCj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^5.0.0", + "yaml": "^2.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" } }, "node_modules/eslint-plugin-regexp": { @@ -8578,14 +8368,14 @@ } }, "node_modules/eslint-plugin-unused-imports": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz", - "integrity": "sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.4.1.tgz", + "integrity": "sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ==", "dev": true, "license": "MIT", "peerDependencies": { "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", - "eslint": "^9.0.0 || ^8.0.0" + "eslint": "^10.0.0 || ^9.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@typescript-eslint/eslint-plugin": { @@ -8594,28 +8384,32 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.3.0.tgz", - "integrity": "sha512-A0u9snqjCfYaPnqqOaH6MBLVWDUIN4trXn8J3x67uDcXvR7X6Ut8p16N+nYhMCQ9Y7edg2BIRGzfyZsY0IdqoQ==", + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.11.1.tgz", + "integrity": "sha512-pasVXhl7wUmIvhujoqs/m0mJ9r59QW1IR9XXYKGR0FETzSM+Hhlf4qKIYgWiDDP+9YKv4YfbwskD7LqIdVcgQw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", + "@eslint-community/eslint-utils": "^4.9.1", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", - "postcss-selector-parser": "^6.0.15", - "semver": "^7.6.3", - "xml-name-validator": "^4.0.0" + "postcss-selector-parser": "^7.1.4", + "semver": "^7.8.5", + "xml-name-validator": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "peerDependencies": { + "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", - "vue-eslint-parser": "^10.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "vue-eslint-parser": "^10.3.0" }, "peerDependenciesMeta": { + "@stylistic/eslint-plugin": { + "optional": true + }, "@typescript-eslint/parser": { "optional": true } @@ -8659,17 +8453,19 @@ } }, "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -8688,28 +8484,101 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/eslint/node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.8" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/espree": { @@ -8731,9 +8600,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -8766,6 +8635,14 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -8807,7 +8684,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=12.0.0" @@ -8821,9 +8698,9 @@ "license": "MIT" }, "node_modules/fast-copy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", - "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.2.0.tgz", + "integrity": "sha512-WSlnvPXaMdUjFdgxPkovE4e5aslybVod4TNKrEKKyoA8GoHZzbDLlhDXZOvqjtFgjtOHd9TYCstkUcGXy1+SEA==", "dev": true, "license": "MIT" }, @@ -8960,7 +8837,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -8981,16 +8858,13 @@ "license": "MIT" }, "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" + "flat-cache": "^6.1.23" } }, "node_modules/fill-range": { @@ -9037,23 +8911,21 @@ } }, "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", "dev": true, "license": "MIT", "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC" }, @@ -9144,7 +9016,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -9214,7 +9085,7 @@ "version": "4.10.1", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -9323,16 +9194,6 @@ "dev": true, "license": "MIT" }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -9360,6 +9221,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -9404,6 +9278,13 @@ "pino": ">=7.1.0" } }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -9475,31 +9356,14 @@ "node": ">= 4" } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/import-in-the-middle": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.3.tgz", - "integrity": "sha512-AiohS3H80sXO6owEltjGX+glb7qXaDhBoJb9XcQVH4UI207xu/bDLUcadVKp7Qe576reg9yr/PXZjV5qx8gfbA==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.5.2.tgz", + "integrity": "sha512-WseIA/4o56+GYO3RmlOPvejYi771OFeJEeK/ejROsr9bdjW+E5NXTOD1Di8TL40Qgg3JJX9aa97Hf8iiSVIKZw==", "license": "Apache-2.0", "dependencies": { "cjs-module-lexer": "^2.2.0", - "es-module-lexer": "^2.2.0", + "es-module-lexer": "^3.0.2", "module-details-from-path": "^1.0.4" }, "engines": { @@ -9507,9 +9371,9 @@ } }, "node_modules/import-in-the-middle/node_modules/es-module-lexer": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", - "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-3.0.4.tgz", + "integrity": "sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==", "license": "MIT" }, "node_modules/imurmurhash": { @@ -9703,7 +9567,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -9732,32 +9596,9 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, - "node_modules/js-yaml": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", - "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/jsdoc-type-pratt-parser": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz", @@ -9781,13 +9622,6 @@ "node": ">=6" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -9896,13 +9730,13 @@ } }, "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, "license": "MIT", "dependencies": { - "json-buffer": "3.0.1" + "@keyv/serialize": "^1.1.1" } }, "node_modules/knip": { @@ -10118,13 +9952,6 @@ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "license": "MIT" }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -10169,7 +9996,7 @@ "version": "3.1.4", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/luxon": { @@ -10185,19 +10012,19 @@ "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/mailgun.js": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/mailgun.js/-/mailgun.js-12.0.3.tgz", - "integrity": "sha512-ki4+xNDA/MjIQAWHB2TK5t5lJN99l45iIRU4SmTqhkY2RCfwQD04s59R3I5YCKdkY/Y5598XRKdkQOj29+eydw==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/mailgun.js/-/mailgun.js-14.1.0.tgz", + "integrity": "sha512-sVrc6RbyYYZt6EccHIBBWxsnywIfSMlywmeu6lZ5hBabJqXr8/csifibt/ygOEQ7SncULWoKU9lTEC2v/sOpUA==", "license": "MIT", "dependencies": { - "axios": "^1.10.0", + "axios": "^1.20.0", "base-64": "^1.0.0", "url-join": "^4.0.1" }, @@ -11480,19 +11307,6 @@ "dev": true, "license": "MIT" }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/parse-gitignore": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/parse-gitignore/-/parse-gitignore-2.0.0.tgz", @@ -11575,14 +11389,14 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/pathval": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 14.16" @@ -11695,14 +11509,14 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -11743,30 +11557,63 @@ } }, "node_modules/pino-pretty": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.0.0.tgz", - "integrity": "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==", + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.2.0.tgz", + "integrity": "sha512-jjghD1ML+Di/WxjJDz6j6Y4FT7hIXOlb8h27AyJ+ohtAXJXH4PS/xkv7si+RCCIW59HMgZsMKTDgzdr0RhuRrg==", "dev": true, "license": "MIT", "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", - "fast-copy": "^3.0.2", + "fast-copy": "^4.0.0", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", - "pump": "^3.0.0", - "secure-json-parse": "^2.4.0", - "sonic-boom": "^4.0.1", - "strip-json-comments": "^3.1.1" + "on-exit-leak-free": "^2.1.2", + "pino-abstract-transport": "^3.0.0", + "pump": "^3.0.3", + "secure-json-parse": "^4.1.0", + "sonic-boom": "^5.0.0", + "strip-json-comments": "^5.0.3" }, "bin": { "pino-pretty": "bin.js" } }, + "node_modules/pino-pretty/node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty/node_modules/sonic-boom": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-5.0.2.tgz", + "integrity": "sha512-/JEruzo6yhNVhsLblTRchgTQHIt7SsdJP0YyrfWaNKjIOQDfrdZv9gwYraYCAsmKV39/LAdV31FLL5NCDpoL/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/pino-pretty/node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pino-std-serializers": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", @@ -11809,9 +11656,9 @@ } }, "node_modules/pnpm-workspace-yaml": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pnpm-workspace-yaml/-/pnpm-workspace-yaml-1.1.0.tgz", - "integrity": "sha512-OWUzBxtitpyUV0fBYYwLAfWxn3mSzVbVB7cwgNaHvTTU9P0V2QHjyaY5i7f1hEiT9VeKsNH1Skfhe2E3lx/zhA==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/pnpm-workspace-yaml/-/pnpm-workspace-yaml-1.9.1.tgz", + "integrity": "sha512-Xj/T2X6DNAWbtk/9UQ0/TJRswltiHZevmcMjqoL7WrpJi67lu3qaslU4+I+zJ8wtwqmuvAE0KkB8rbv2ZdogBg==", "dev": true, "funding": [ { @@ -11825,14 +11672,14 @@ ], "license": "MIT", "dependencies": { - "yaml": "^2.8.0" + "yaml": "^2.9.0" } }, "node_modules/postcss": { - "version": "8.5.25", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", - "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", - "dev": true, + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", + "devOptional": true, "funding": [ { "type": "opencollective", @@ -11849,18 +11696,18 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.16", + "nanoid": "^3.3.20", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" } }, "node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -11872,10 +11719,10 @@ } }, "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", - "dev": true, + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "devOptional": true, "funding": [ { "type": "github", @@ -11953,9 +11800,9 @@ } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", "dev": true, "license": "MIT", "bin": { @@ -12090,6 +11937,26 @@ "node": ">=16.0.0" } }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, "node_modules/quansync": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz", @@ -12316,21 +12183,11 @@ "node": ">=9.3.0 || >=8.10.0 <9.0.0" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" @@ -12351,7 +12208,7 @@ "version": "4.60.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -12393,9 +12250,9 @@ } }, "node_modules/rou3": { - "version": "0.7.12", - "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.7.12.tgz", - "integrity": "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.9.2.tgz", + "integrity": "sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==", "license": "MIT" }, "node_modules/run-applescript": { @@ -12485,16 +12342,26 @@ } }, "node_modules/secure-json-parse": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", - "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.1.tgz", + "integrity": "sha512-lnd81MQEZKjhW/Gd2TQqdd4XP6QYNdDW/EP+I7nTrFHP9y2LmaGPIHk9xsnzcOiEr1c39c/e84xFgGviQKBTRw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "BSD-3-Clause" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -12519,9 +12386,9 @@ } }, "node_modules/set-cookie-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", - "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.2.1.tgz", + "integrity": "sha512-Npd1Pt+4zb3BrSZDPd2qFwRpS6b1EUEasRrO8xS0zMJi0fApXXK5YuzP0minyJnxwnFHoVoJwA55q0A0cu63gQ==", "license": "MIT" }, "node_modules/shebang-command": { @@ -12549,7 +12416,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/signal-exit": { @@ -12607,17 +12474,17 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -12627,7 +12494,7 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -12653,9 +12520,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.21", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", - "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "version": "3.0.24", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.24.tgz", + "integrity": "sha512-cLS9TtWkIQFyLkJ3/5aFQAOHOSKTlOs/7WDut/XPSdjom1fJhUdkepeJAKXa9Y05+FabHd0sti+Et559vDtkpQ==", "dev": true, "license": "CC0-1.0" }, @@ -12672,14 +12539,14 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/std-env": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/stoker": { @@ -12855,24 +12722,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strip-literal": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "js-tokens": "^9.0.1" @@ -12881,19 +12735,6 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/synckit": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.3.tgz", @@ -12966,7 +12807,7 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/tinyexec": { @@ -12977,14 +12818,14 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "devOptional": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -12997,7 +12838,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^18.0.0 || >=20.0.0" @@ -13007,7 +12848,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=14.0.0" @@ -13017,7 +12858,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=14.0.0" @@ -13115,9 +12956,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -13151,9 +12992,9 @@ } }, "node_modules/tsc-alias": { - "version": "1.8.16", - "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.16.tgz", - "integrity": "sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==", + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.9.7.tgz", + "integrity": "sha512-nGOOLMkaociegx2Y9J1lnbqrledf1EfSq3Ds4H4CLySG5RyD58KrR4ahFIAgpeMVJz4BIhJND3ETt02o9zKQAw==", "dev": true, "license": "MIT", "dependencies": { @@ -13170,6 +13011,10 @@ }, "engines": { "node": ">=16.20.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/justkey007" } }, "node_modules/tslib": { @@ -13179,14 +13024,13 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "dev": true, + "version": "4.23.15", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", + "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", + "devOptional": true, "license": "MIT", "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" + "esbuild": "~0.28.0" }, "bin": { "tsx": "dist/cli.mjs" @@ -13199,13 +13043,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13216,13 +13059,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13233,13 +13075,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13250,13 +13091,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13267,13 +13107,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13284,13 +13123,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13301,13 +13139,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13318,13 +13155,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13335,13 +13171,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13352,13 +13187,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13369,13 +13203,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13386,13 +13219,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13403,13 +13235,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13420,13 +13251,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13437,13 +13267,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13454,13 +13283,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13471,13 +13299,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13488,13 +13315,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13505,13 +13331,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13522,13 +13347,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13539,13 +13363,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13556,13 +13379,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13573,13 +13395,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13590,13 +13411,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13607,13 +13427,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13624,13 +13443,12 @@ } }, "node_modules/tsx/node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13641,10 +13459,10 @@ } }, "node_modules/tsx/node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", - "dev": true, + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "devOptional": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -13654,32 +13472,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/tsyringe": { @@ -13918,11 +13736,24 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/verkit": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.3.2.tgz", + "integrity": "sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/vite": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "esbuild": "^0.27.0", @@ -13997,7 +13828,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", @@ -14023,7 +13854,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14040,7 +13870,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14057,7 +13886,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14074,7 +13902,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14091,7 +13918,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14108,7 +13934,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14125,7 +13950,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14142,7 +13966,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14159,7 +13982,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14176,7 +13998,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14193,7 +14014,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14210,7 +14030,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14227,7 +14046,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14244,7 +14062,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14261,7 +14078,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14278,7 +14094,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14295,7 +14110,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14312,7 +14126,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14329,7 +14142,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14346,7 +14158,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14363,7 +14174,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14380,7 +14190,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14397,7 +14206,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14414,7 +14222,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14431,7 +14238,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14448,7 +14254,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14462,7 +14267,7 @@ "version": "0.27.7", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -14504,7 +14309,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", @@ -14577,20 +14382,20 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/vue-eslint-parser": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.2.0.tgz", - "integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.1.tgz", + "integrity": "sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.2.0 || ^9.0.0", + "eslint-visitor-keys": "^4.2.0 || ^5.0.0", + "espree": "^10.3.0 || ^11.0.0", "esquery": "^1.6.0", "semver": "^7.6.3" }, @@ -14601,7 +14406,7 @@ "url": "https://github.com/sponsors/mysticatea" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" } }, "node_modules/walk-up-path": { @@ -14633,7 +14438,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "siginfo": "^2.0.0", @@ -14795,13 +14600,13 @@ } }, "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/xpath": { @@ -14832,9 +14637,9 @@ } }, "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 868205f1..7bda1bca 100644 --- a/package.json +++ b/package.json @@ -60,8 +60,8 @@ "@aws-sdk/client-s3": "^3.1098.0", "@aws-sdk/lib-storage": "^3.1098.0", "@aws-sdk/s3-request-presigner": "^3.1098.0", - "@better-auth/drizzle-adapter": "^1.6.9", - "@better-auth/passkey": "^1.6.9", + "@better-auth/drizzle-adapter": "1.6.30", + "@better-auth/passkey": "1.6.30", "@hono/node-server": "1.19.15", "@hono/zod-openapi": "0.19.10", "@scalar/hono-api-reference": "0.9.48", @@ -69,14 +69,14 @@ "applicationinsights": "3.14.0", "archiver": "7.0.1", "bcryptjs": "^3.0.3", - "better-auth": "^1.6.9", + "better-auth": "1.6.30", "dotenv": "16.6.1", "dotenv-expand": "12.0.2", "drizzle-orm": "0.45.2", "drizzle-zod": "0.7.1", "hono": "4.13.5", "hono-pino": "0.8.0", - "mailgun.js": "12.0.3", + "mailgun.js": "14.1.0", "nanoid": "^6.0.2", "pg-boss": "12.1.1", "pino": "9.7.0", @@ -87,20 +87,20 @@ }, "devDependencies": { "@antfu/eslint-config": "4.18.0", - "@electric-sql/pglite": "0.5.8", + "@electric-sql/pglite": "0.5.10", "@types/archiver": "6.0.3", "@types/node": "24.1.0", "cross-env": "7.0.3", - "drizzle-kit": "0.31.10", - "eslint": "9.31.0", + "drizzle-kit": "0.31.11", + "eslint": "10.12.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-format": "1.0.1", "husky": "9.1.7", "knip": "5.88.1", - "pino-pretty": "13.0.0", - "prettier": "3.6.2", - "tsc-alias": "1.8.16", - "tsx": "4.21.0", + "pino-pretty": "13.2.0", + "prettier": "3.9.10", + "tsc-alias": "1.9.7", + "tsx": "4.23.15", "typescript": "5.8.3", "vitest": "3.2.4" }, diff --git a/src/db/seeds/audio-demo.db.test.ts b/src/db/seeds/audio-demo.db.test.ts index 9ce4c2db..9d25fbd0 100644 --- a/src/db/seeds/audio-demo.db.test.ts +++ b/src/db/seeds/audio-demo.db.test.ts @@ -13,7 +13,7 @@ import { translated_verses, users, } from '@/db/schema'; -import { findAssignmentsProgress } from '@/domains/chapter-assignments/chapter-assignments.repository'; +import { findAssignmentsProgress } from '@/domains/chapter-assignments/progress/chapter-assignments-progress.repository'; import { getChapterPericopes } from '@/domains/pericopes/pericopes.service'; import { getAssignedChaptersByUserId } from '@/domains/users/chapter-assignments/users-chapter-assignments.service'; import { auth } from '@/lib/auth'; diff --git a/src/domains/ai-suggestions/ai-pericope.repository.integration.test.ts b/src/domains/ai-suggestions/ai-pericope.repository.integration.test.ts index 1a30cf1b..f11b93fa 100644 --- a/src/domains/ai-suggestions/ai-pericope.repository.integration.test.ts +++ b/src/domains/ai-suggestions/ai-pericope.repository.integration.test.ts @@ -1,4 +1,3 @@ -import { generateDrizzleJson, generateMigration } from 'drizzle-kit/api'; import { and, eq } from 'drizzle-orm'; import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; @@ -8,6 +7,7 @@ import * as schema from '@/db/schema'; import type { PericopeSuggestionItem, PericopeUsageRequest } from './ai-suggestions.types'; import { resolvePericopes } from './ai-pericope.repository'; +import { setupPericopeIntegrationTest } from './ai-pericope.test-fixtures'; import { logAiSuggestionUsage } from './ai-suggestions.repository'; import { saveAiSuggestions, trackPericopeUsage } from './ai-suggestions.service'; @@ -55,126 +55,15 @@ describe('pericope repository with real PostgreSQL queries', () => { }); beforeAll(async () => { - const statements = await generateMigration( - generateDrizzleJson({}), - generateDrizzleJson({ - ai_pericope_suggestion_usage: schema.ai_pericope_suggestion_usage, - ai_pericope_suggestions: schema.ai_pericope_suggestions, - ai_suggestions: schema.ai_suggestions, - ai_suggestion_usage_log: schema.ai_suggestion_usage_log, - bible_texts: schema.bible_texts, - books: schema.books, - chapter_assignments: schema.chapter_assignments, - pericope_verses: schema.pericope_verses, - pericope_sets: schema.pericope_sets, - project_unit_bible_books: schema.project_unit_bible_books, - project_units: schema.project_units, - projects: schema.projects, - translated_verses: schema.translated_verses, - users: schema.users, - organizations: schema.organizations, - languages: schema.languages, - bibles: schema.bibles, - bible_provider_resources: schema.bible_provider_resources, - authUser: schema.authUser, - authSession: schema.authSession, - bibleProviderEnum: schema.bibleProviderEnum, - resourceProviderEnum: schema.resourceProviderEnum, - ttsLicenseStatusEnum: schema.ttsLicenseStatusEnum, - milestoneTypeEnum: schema.milestoneTypeEnum, - projectStatusEnum: schema.projectStatusEnum, - projectAssignmentStatusEnum: schema.projectAssignmentStatusEnum, - scriptDirectionEnum: schema.scriptDirectionEnum, - userStatusEnum: schema.userStatusEnum, - }) - ); - await client.exec(statements.join('\n')); - const suffix = Date.now().toString(); - const [org] = await db - .insert(schema.organizations) - .values({ name: `Pericope test ${suffix}` }) - .returning(); - const [language] = await db - .insert(schema.languages) - .values({ langName: `Test ${suffix}` }) - .returning(); - const [user] = await db - .insert(schema.users) - .values({ username: `pericope-${suffix}`, email: `${suffix}@example.test` }) - .returning(); - userId = user.id; - const sets = await db - .insert(schema.pericope_sets) - .values([{ name: `FIA-${suffix}` }, { name: `FCBH-${suffix}` }]) - .returning(); - pericopeSetId = sets[0].id; - otherSetId = sets[1].id; - const [project] = await db - .insert(schema.projects) - .values({ - name: 'Pericope test', - sourceLanguage: language.id, - targetLanguage: language.id, - organization: org.id, - pericopeSetId, - }) - .returning(); - projectId = project.id; - const [unit] = await db.insert(schema.project_units).values({ projectId }).returning(); - projectUnitId = unit.id; - const [bible] = await db - .insert(schema.bibles) - .values({ languageId: language.id, name: `Source ${suffix}`, abbreviation: `s-${suffix}` }) - .returning(); - bibleId = bible.id; - await db - .insert(schema.books) - .values({ code: 'GEN', eng_display_name: 'Genesis' }) - .onConflictDoNothing(); - const [book] = await db.select().from(schema.books).where(eq(schema.books.code, 'GEN')); - bookId = book.id; - await db.insert(schema.project_unit_bible_books).values({ projectUnitId, bibleId, bookId }); - await db - .insert(schema.chapter_assignments) - .values({ projectUnitId, bibleId, bookId, chapterNumber: 1, isAiEnabled: true }); - const texts = await db - .insert(schema.bible_texts) - .values( - [1, 2, 3, 4].map((verseNumber) => ({ - bibleId, - bookId, - chapterNumber: 1, - verseNumber, - text: `Source ${verseNumber}`, - })) - ) - .returning(); - bibleTextIds = texts.map((text) => text.id); - await db.insert(schema.pericope_verses).values( - [pericopeSetId, otherSetId].flatMap((setId) => - [1, 2, 3, 4].map((verseNumber) => ({ - pericopeSetId: setId, - bookId, - chapterNumber: 1, - verseNumber, - section: setId === pericopeSetId ? null : verseNumber < 4 ? 1 : 2, - pericopeNumber: setId === otherSetId || verseNumber < 4 ? '4a' : '4b', - pericopeTitle: setId === pericopeSetId && verseNumber < 4 ? 'Creation' : null, - })) - ) - ); - await db.insert(schema.translated_verses).values([ - { projectUnitId, bibleTextId: bibleTextIds[1], content: '' }, - { - projectUnitId, - bibleTextId: bibleTextIds[2], - content: 'Already translated', - markers: { headings: [{ marker: 's', text: 'An authored heading' }] }, - }, - ]); - await db - .insert(schema.ai_suggestions) - .values({ projectUnitId, bibleTextId: bibleTextIds[3], suggestedText: 'Cached scripture' }); + const fixture = await setupPericopeIntegrationTest(client, db); + projectUnitId = fixture.projectUnitId; + projectId = fixture.projectId; + bibleId = fixture.bibleId; + bookId = fixture.bookId; + pericopeSetId = fixture.pericopeSetId; + otherSetId = fixture.otherSetId; + userId = fixture.userId; + bibleTextIds = fixture.bibleTextIds; }, 30_000); afterAll(async () => { await client.close(); diff --git a/src/domains/ai-suggestions/ai-pericope.service.ts b/src/domains/ai-suggestions/ai-pericope.service.ts index 9412b062..4046732a 100644 --- a/src/domains/ai-suggestions/ai-pericope.service.ts +++ b/src/domains/ai-suggestions/ai-pericope.service.ts @@ -1,15 +1,23 @@ -import type { Result } from '@/lib/types'; +import type { AiSuggestionTriggerJob } from '@/lib/queue'; +import type { Result, User } from '@/lib/types'; +import env from '@/env'; +import { logger } from '@/lib/logger'; +import { getQueue, QUEUE_NAMES } from '@/lib/queue'; import { err, ErrorCode, ok } from '@/lib/types'; import type { PericopeContext, + PericopeRequest, PericopeSuggestionItem, + PericopeSuggestionsResponse, PericopeUsageRequest, + QueueNextVersesResponse, ResolvedPericope, } from './ai-suggestions.types'; import * as pericopeRepo from './ai-pericope.repository'; +import * as suggestionsRepo from './ai-suggestions.repository'; export function canSuggestPericopeTitle( context: PericopeContext, @@ -17,10 +25,10 @@ export function canSuggestPericopeTitle( ): boolean { return Boolean( context.isAiEnabled && - group.startsPericope && - group.sourceTitle && - group.verses.length && - !group.verses[0].hasAuthoredHeading + group.startsPericope && + group.sourceTitle && + group.verses.length && + !group.verses[0].hasAuthoredHeading ); } @@ -39,17 +47,14 @@ export async function savePericopeSuggestion( ); if (!resolved.ok) return resolved; const group = resolved.data.groups[0]; - if (!group.verses.some((verse) => verse.bibleTextId === heading.bibleTextId)) + if (!group.verses.some((v) => v.bibleTextId === heading.bibleTextId)) return err(ErrorCode.INVALID_REFERENCE); if (!canSuggestPericopeTitle(resolved.data, group)) return ok(undefined); await pericopeRepo.insertPericopeSuggestion(heading, verse); return ok(undefined); } -export async function logPericopeUsage( - userId: number, - data: PericopeUsageRequest -): Promise> { +async function logPericopeUsage(userId: number, data: PericopeUsageRequest): Promise> { const verse = await pericopeRepo.findPericopeVerse(data.bibleTextId); if (!verse) return err(ErrorCode.INVALID_REFERENCE); const saved = await pericopeRepo.findSavedPericopeSets(data, verse); @@ -75,3 +80,120 @@ export async function logPericopeUsage( await pericopeRepo.upsertPericopeUsage(userId, group.suggestion.id, data.wasUsed); return ok(undefined); } + +export async function queuePericopes( + params: PericopeRequest +): Promise> { + try { + const resolved = await pericopeRepo.resolvePericopes(params); + if (!resolved.ok) return resolved; + const thresholdMet = await suggestionsRepo.hasReachedAiActivationThreshold( + params.projectUnitId, + env.AI_ACTIVATION_THRESHOLD_VERSES + ); + if (!thresholdMet || !resolved.data.isAiEnabled) return ok({ queued: false, thresholdMet }); + const jobs: AiSuggestionTriggerJob[] = []; + const base = { + projectUnitId: params.projectUnitId, + bibleId: params.bibleId, + bookCode: params.bookCode.toUpperCase(), + chapterNumber: params.chapterNumber, + }; + for (const group of resolved.data.groups) { + for (const verse of group.verses) { + if (!verse.content?.trim() && !verse.hasSuggestion) { + jobs.push({ ...base, verseStart: verse.verseNumber, verseEnd: verse.verseNumber }); + } + } + if (canSuggestPericopeTitle(resolved.data, group) && !group.suggestion) { + jobs.push({ + ...base, + verseStart: group.verses[0].verseNumber, + verseEnd: group.verses[group.verses.length - 1].verseNumber, + pericopeNumber: group.pericopeNumber, + pericopeSetId: resolved.data.pericopeSetId, + }); + } + } + if (jobs.length === 0) return ok({ queued: false, thresholdMet }); + const boss = await getQueue(); + // Keep verse singleton keys identical to queue-next. Titles have their own identity. + const results = await Promise.allSettled( + jobs.map((job) => { + const verseKey = `${job.projectUnitId}:${job.bibleId}:${job.bookCode}:${job.chapterNumber}:${job.verseStart}`; + return boss.send(QUEUE_NAMES.AI_SUGGESTIONS, job, { + singletonKey: + job.pericopeNumber === undefined + ? verseKey + : `heading:${verseKey}:${job.verseEnd}:${job.pericopeSetId}:${job.pericopeNumber}`, + }); + }) + ); + const accepted = results.filter( + (result) => result.status === 'fulfilled' && result.value !== null + ).length; + const deduped = results.filter( + (result) => result.status === 'fulfilled' && result.value === null + ).length; + const rejected = results.filter( + (result): result is PromiseRejectedResult => result.status === 'rejected' + ); + logger.debug('Pericope AI suggestion jobs submitted to queue', { + total: results.length, + accepted, + deduped, + failed: rejected.length, + projectUnitId: params.projectUnitId, + bookCode: base.bookCode, + chapterNumber: params.chapterNumber, + }); + if (rejected.length > 0) { + logger.error({ + cause: rejected[0].reason, + message: 'Failed to enqueue some pericope AI suggestion jobs', + context: { total: results.length, accepted, deduped, failed: rejected.length }, + }); + return err(ErrorCode.INTERNAL_ERROR); + } + return ok({ queued: true, thresholdMet }); + } catch (error) { + logger.error(error); + return err(ErrorCode.INTERNAL_ERROR); + } +} + +export async function getPericopeSuggestions( + params: PericopeRequest +): Promise> { + try { + const resolved = await pericopeRepo.resolvePericopes(params); + if (!resolved.ok) return resolved; + const data = resolved.data.groups.flatMap((group) => { + if (!canSuggestPericopeTitle(resolved.data, group) || !group.suggestion) return []; + return [ + { + pericopeNumber: group.pericopeNumber, + bibleTextId: group.verses[0].bibleTextId, + suggestedText: group.suggestion.suggestedText, + modelInfo: group.suggestion.modelInfo, + }, + ]; + }); + return ok({ data }); + } catch (error) { + logger.error(error); + return err(ErrorCode.INTERNAL_ERROR); + } +} + +export async function trackPericopeUsage( + user: Pick, + data: PericopeUsageRequest +): Promise> { + try { + return await logPericopeUsage(user.id, data); + } catch (error) { + logger.error(error); + return err(ErrorCode.INTERNAL_ERROR); + } +} diff --git a/src/domains/ai-suggestions/ai-pericope.test-fixtures.ts b/src/domains/ai-suggestions/ai-pericope.test-fixtures.ts new file mode 100644 index 00000000..fb1a546b --- /dev/null +++ b/src/domains/ai-suggestions/ai-pericope.test-fixtures.ts @@ -0,0 +1,167 @@ +import type { PGlite } from '@electric-sql/pglite'; + +import { generateDrizzleJson, generateMigration } from 'drizzle-kit/api'; +import { eq } from 'drizzle-orm'; + +import type { db } from '@/db'; + +import * as schema from '@/db/schema'; + +interface PericopeIntegrationFixture { + bibleId: number; + bibleTextIds: number[]; + bookId: number; + otherSetId: number; + pericopeSetId: number; + projectId: number; + projectUnitId: number; + userId: number; +} + +export async function setupPericopeIntegrationTest( + client: PGlite, + database: typeof db +): Promise { + const statements = await generateMigration( + generateDrizzleJson({}), + generateDrizzleJson({ + ai_pericope_suggestion_usage: schema.ai_pericope_suggestion_usage, + ai_pericope_suggestions: schema.ai_pericope_suggestions, + ai_suggestions: schema.ai_suggestions, + ai_suggestion_usage_log: schema.ai_suggestion_usage_log, + bible_texts: schema.bible_texts, + books: schema.books, + chapter_assignments: schema.chapter_assignments, + pericope_verses: schema.pericope_verses, + pericope_sets: schema.pericope_sets, + project_unit_bible_books: schema.project_unit_bible_books, + project_units: schema.project_units, + projects: schema.projects, + translated_verses: schema.translated_verses, + users: schema.users, + organizations: schema.organizations, + languages: schema.languages, + bibles: schema.bibles, + bible_provider_resources: schema.bible_provider_resources, + authUser: schema.authUser, + authSession: schema.authSession, + bibleProviderEnum: schema.bibleProviderEnum, + resourceProviderEnum: schema.resourceProviderEnum, + ttsLicenseStatusEnum: schema.ttsLicenseStatusEnum, + milestoneTypeEnum: schema.milestoneTypeEnum, + projectStatusEnum: schema.projectStatusEnum, + projectAssignmentStatusEnum: schema.projectAssignmentStatusEnum, + scriptDirectionEnum: schema.scriptDirectionEnum, + userStatusEnum: schema.userStatusEnum, + }) + ); + await client.exec(statements.join('\n')); + + const suffix = Date.now().toString(); + const [org] = await database + .insert(schema.organizations) + .values({ name: `Pericope test ${suffix}` }) + .returning(); + const [language] = await database + .insert(schema.languages) + .values({ langName: `Test ${suffix}` }) + .returning(); + const [user] = await database + .insert(schema.users) + .values({ username: `pericope-${suffix}`, email: `${suffix}@example.test` }) + .returning(); + const userId = user.id; + + const sets = await database + .insert(schema.pericope_sets) + .values([{ name: `FIA-${suffix}` }, { name: `FCBH-${suffix}` }]) + .returning(); + const pericopeSetId = sets[0].id; + const otherSetId = sets[1].id; + + const [project] = await database + .insert(schema.projects) + .values({ + name: 'Pericope test', + sourceLanguage: language.id, + targetLanguage: language.id, + organization: org.id, + pericopeSetId, + }) + .returning(); + const projectId = project.id; + + const [unit] = await database.insert(schema.project_units).values({ projectId }).returning(); + const projectUnitId = unit.id; + + const [bible] = await database + .insert(schema.bibles) + .values({ languageId: language.id, name: `Source ${suffix}`, abbreviation: `s-${suffix}` }) + .returning(); + const bibleId = bible.id; + + await database + .insert(schema.books) + .values({ code: 'GEN', eng_display_name: 'Genesis' }) + .onConflictDoNothing(); + const [book] = await database.select().from(schema.books).where(eq(schema.books.code, 'GEN')); + const bookId = book.id; + + await database.insert(schema.project_unit_bible_books).values({ projectUnitId, bibleId, bookId }); + await database + .insert(schema.chapter_assignments) + .values({ projectUnitId, bibleId, bookId, chapterNumber: 1, isAiEnabled: true }); + + const texts = await database + .insert(schema.bible_texts) + .values( + [1, 2, 3, 4].map((verseNumber) => ({ + bibleId, + bookId, + chapterNumber: 1, + verseNumber, + text: `Source ${verseNumber}`, + })) + ) + .returning(); + const bibleTextIds = texts.map((text: { id: number }) => text.id); + + await database.insert(schema.pericope_verses).values( + [pericopeSetId, otherSetId].flatMap((setId) => + [1, 2, 3, 4].map((verseNumber) => ({ + pericopeSetId: setId, + bookId, + chapterNumber: 1, + verseNumber, + section: setId === pericopeSetId ? null : verseNumber < 4 ? 1 : 2, + pericopeNumber: setId === otherSetId || verseNumber < 4 ? '4a' : '4b', + pericopeTitle: setId === pericopeSetId && verseNumber < 4 ? 'Creation' : null, + })) + ) + ); + + await database.insert(schema.translated_verses).values([ + { projectUnitId, bibleTextId: bibleTextIds[1], content: '' }, + { + projectUnitId, + bibleTextId: bibleTextIds[2], + content: 'Already translated', + markers: { headings: [{ marker: 's', text: 'An authored heading' }] }, + }, + ]); + + await database + .insert(schema.ai_suggestions) + .values({ projectUnitId, bibleTextId: bibleTextIds[3], suggestedText: 'Cached scripture' }); + + return { + bibleId, + bibleTextIds, + bookId, + otherSetId, + pericopeSetId, + projectId, + projectUnitId, + userId, + }; +} diff --git a/src/domains/ai-suggestions/ai-suggestions.service.ts b/src/domains/ai-suggestions/ai-suggestions.service.ts index 9debadd4..9a7bd26e 100644 --- a/src/domains/ai-suggestions/ai-suggestions.service.ts +++ b/src/domains/ai-suggestions/ai-suggestions.service.ts @@ -1,4 +1,3 @@ -import type { AiSuggestionTriggerJob } from '@/lib/queue'; import type { DbTransaction, Result, User } from '@/lib/types'; import * as pericopesService from '@/domains/pericopes/pericopes.service'; @@ -11,10 +10,7 @@ import type { AiSuggestionItem, AiSuggestionsListResponse, GetAiSuggestionsQuery, - PericopeRequest, PericopeSuggestionItem, - PericopeSuggestionsResponse, - PericopeUsageRequest, QueueNextVersesResponse, SuggestionContextRequest, SuggestionContextResponse, @@ -22,11 +18,7 @@ import type { } from './ai-suggestions.types'; import * as pericopeRepo from './ai-pericope.repository'; -import { - canSuggestPericopeTitle, - logPericopeUsage, - savePericopeSuggestion, -} from './ai-pericope.service'; +import { canSuggestPericopeTitle, savePericopeSuggestion } from './ai-pericope.service'; import { MAX_CONTEXT_VERSES_TOTAL } from './ai-suggestions.constants'; import { checkBibleTextsExist, @@ -510,117 +502,4 @@ export async function saveAiSuggestions( return upsertAiSuggestions(items); } -export async function queuePericopes( - params: PericopeRequest -): Promise> { - try { - const resolved = await pericopeRepo.resolvePericopes(params); - if (!resolved.ok) return resolved; - const thresholdMet = await hasReachedAiActivationThreshold( - params.projectUnitId, - env.AI_ACTIVATION_THRESHOLD_VERSES - ); - if (!thresholdMet || !resolved.data.isAiEnabled) return ok({ queued: false, thresholdMet }); - const jobs: AiSuggestionTriggerJob[] = []; - const base = { - projectUnitId: params.projectUnitId, - bibleId: params.bibleId, - bookCode: params.bookCode.toUpperCase(), - chapterNumber: params.chapterNumber, - }; - for (const group of resolved.data.groups) { - for (const verse of group.verses) { - if (!verse.content?.trim() && !verse.hasSuggestion) { - jobs.push({ ...base, verseStart: verse.verseNumber, verseEnd: verse.verseNumber }); - } - } - if (canSuggestPericopeTitle(resolved.data, group) && !group.suggestion) { - jobs.push({ - ...base, - verseStart: group.verses[0].verseNumber, - verseEnd: group.verses[group.verses.length - 1].verseNumber, - pericopeNumber: group.pericopeNumber, - pericopeSetId: resolved.data.pericopeSetId, - }); - } - } - if (jobs.length === 0) return ok({ queued: false, thresholdMet }); - const boss = await getQueue(); - // Keep verse singleton keys identical to queue-next. Titles have their own identity. - const results = await Promise.allSettled( - jobs.map((job) => { - const verseKey = `${job.projectUnitId}:${job.bibleId}:${job.bookCode}:${job.chapterNumber}:${job.verseStart}`; - return boss.send(QUEUE_NAMES.AI_SUGGESTIONS, job, { - singletonKey: - job.pericopeNumber === undefined - ? verseKey - : `heading:${verseKey}:${job.verseEnd}:${job.pericopeSetId}:${job.pericopeNumber}`, - }); - }) - ); - const accepted = results.filter( - (result) => result.status === 'fulfilled' && result.value !== null - ).length; - const deduped = results.filter( - (result) => result.status === 'fulfilled' && result.value === null - ).length; - const rejected = results.filter((result) => result.status === 'rejected'); - logger.debug('Pericope AI suggestion jobs submitted to queue', { - total: results.length, - accepted, - deduped, - failed: rejected.length, - projectUnitId: params.projectUnitId, - bookCode: base.bookCode, - chapterNumber: params.chapterNumber, - }); - if (rejected.length > 0) { - logger.error({ - cause: rejected[0].reason, - message: 'Failed to enqueue some pericope AI suggestion jobs', - context: { total: results.length, accepted, deduped, failed: rejected.length }, - }); - return err(ErrorCode.INTERNAL_ERROR); - } - return ok({ queued: true, thresholdMet }); - } catch (error) { - logger.error(error); - return err(ErrorCode.INTERNAL_ERROR); - } -} - -export async function getPericopeSuggestions( - params: PericopeRequest -): Promise> { - try { - const resolved = await pericopeRepo.resolvePericopes(params); - if (!resolved.ok) return resolved; - const data = resolved.data.groups.flatMap((group) => { - if (!canSuggestPericopeTitle(resolved.data, group) || !group.suggestion) return []; - return [ - { - pericopeNumber: group.pericopeNumber, - bibleTextId: group.verses[0].bibleTextId, - suggestedText: group.suggestion.suggestedText, - modelInfo: group.suggestion.modelInfo, - }, - ]; - }); - return ok({ data }); - } catch (error) { - logger.error(error); - return err(ErrorCode.INTERNAL_ERROR); - } -} - -export async function trackPericopeUsage( - user: Pick, - data: PericopeUsageRequest -): Promise> { - try { - return await logPericopeUsage(user.id, data); - } catch (error) { - logger.error(error); - return err(ErrorCode.INTERNAL_ERROR); - } -} +export { getPericopeSuggestions, queuePericopes, trackPericopeUsage } from './ai-pericope.service'; diff --git a/src/domains/bible-provider-resources/bible-provider-resources.db.test.ts b/src/domains/bible-provider-resources/bible-provider-resources.db.test.ts index 74a180bc..0d00c6e1 100644 --- a/src/domains/bible-provider-resources/bible-provider-resources.db.test.ts +++ b/src/domains/bible-provider-resources/bible-provider-resources.db.test.ts @@ -8,7 +8,7 @@ import { bible_provider_resources, bibles, projects } from '@/db/schema'; import { AUDIO_DEMO_SEED, seedAudioDemo } from '@/db/seeds/audio-demo'; import { seedBibles } from '@/db/seeds/bibles'; import { BIBLE_PROVIDER_PREFIXES } from '@/domains/bible-provider-resources/bible-provider-resources.identity'; -import { findAssignmentsProgress } from '@/domains/chapter-assignments/chapter-assignments.repository'; +import { findAssignmentsProgress } from '@/domains/chapter-assignments/progress/chapter-assignments-progress.repository'; import { auth } from '@/lib/auth'; import { server } from '@/server/server'; import '@/domains/playback-audio/playback-audio.route'; diff --git a/src/domains/catalogue-sync.watermark.integration.test.ts b/src/domains/catalogue-sync.watermark.integration.test.ts index 6e92ad54..840cc66f 100644 --- a/src/domains/catalogue-sync.watermark.integration.test.ts +++ b/src/domains/catalogue-sync.watermark.integration.test.ts @@ -41,6 +41,7 @@ describe('catalogue sync watermarks (PGlite)', () => { }) ); await client.exec(statements.join('\n')); + await client.exec("SET timezone = 'UTC';"); // Production invariant (migration 0035): INSERT/UPDATE of license/display // columns bumps linked bibles.updated_at for any SQL/ORM writer. const triggerSql = readFileSync( diff --git a/src/domains/chapter-assignments/chapter-assignments.repository.test.ts b/src/domains/chapter-assignments/chapter-assignments.repository.test.ts index ea917e09..f28129da 100644 --- a/src/domains/chapter-assignments/chapter-assignments.repository.test.ts +++ b/src/domains/chapter-assignments/chapter-assignments.repository.test.ts @@ -1,59 +1,12 @@ -import type { SQL } from 'drizzle-orm'; - import { eq, ne } from 'drizzle-orm'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { db } from '@/db'; -import { bibles, chapter_assignments } from '@/db/schema'; -import { VERSE_AUDIO_CONFLICT_STATUS } from '@/domains/verse-audio/verse-audio.types'; -import { ErrorCode } from '@/lib/types'; +import { chapter_assignments } from '@/db/schema'; import * as repo from './chapter-assignments.repository'; import { CHAPTER_ASSIGNMENT_STATUS } from './chapter-assignments.types'; -/** Walks Drizzle SQL chunks for stable substring assertions in unit tests. */ -function sqlText(fragment: SQL): string { - const parts: string[] = []; - const walk = (chunk: unknown): void => { - if (chunk == null) { - return; - } - if (typeof chunk === 'string') { - parts.push(chunk); - return; - } - if (typeof chunk !== 'object') { - parts.push(String(chunk)); - return; - } - if ('value' in chunk && Array.isArray((chunk as { value: unknown }).value)) { - for (const piece of (chunk as { value: unknown[] }).value) { - walk(piece); - } - return; - } - if ('queryChunks' in chunk && Array.isArray((chunk as SQL).queryChunks)) { - for (const piece of (chunk as SQL).queryChunks) { - walk(piece); - } - } - }; - walk(fragment); - return parts.join(''); -} - -function buildProgressSelectChain(rows: unknown[]) { - const orderBy = vi.fn().mockResolvedValue(rows); - const groupBy = vi.fn().mockReturnValue({ orderBy }); - return { - from: vi.fn().mockReturnThis(), - innerJoin: vi.fn().mockReturnThis(), - leftJoin: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - groupBy, - }; -} - const { mockDbSelect, mockSelectChain, mockUpdateChain } = vi.hoisted(() => { const dbSelect = vi.fn<(selection?: unknown) => unknown>(); const updateChain = { @@ -127,172 +80,6 @@ describe('chapter-assignments.repository claim helpers', () => { }); }); - describe('hasConflictRollupSql', () => { - it('correlates verse bible to the assignment bible so other-Bible conflicts are excluded', () => { - const fragment = repo.hasConflictRollupSql(); - const text = sqlText(fragment); - - expect(text).toContain('bt.bible_id'); - expect(text).toContain('bt.book_id'); - expect(text).toContain('bt.chapter_number'); - expect(text).toContain('var.project_unit_id'); - expect(text).toContain('var.conflict_status'); - expect(text).not.toMatch(/conflict_status\s*=\s*'conflict'/); - expect(VERSE_AUDIO_CONFLICT_STATUS.CONFLICT).toBe('conflict'); - }); - - it('filters strictly on conflict status so clean and resolved recordings are excluded', () => { - const fragment = repo.hasConflictRollupSql(); - const text = sqlText(fragment); - - expect(text).toMatch(/var\.conflict_status\s*=\s*conflict/); - expect(text).not.toContain('resolved'); - expect(text).not.toContain('clean'); - }); - }); - - it('groups the Bible primary key when selecting license fields alongside verse counts', async () => { - const chain = buildProgressSelectChain([]); - mockDbSelect.mockReturnValue(chain); - - expect(await repo.findAssignmentsProgress({ projectId: 3 })).toEqual({ ok: true, data: [] }); - expect(db.select).toHaveBeenCalledWith( - expect.objectContaining({ - ttsLicenseStatus: expect.anything(), - textBibleProvider: expect.anything(), - textBibleExternalId: expect.anything(), - selectedRecordingProvider: expect.anything(), - selectedRecordingExternalId: expect.anything(), - }) - ); - // PostgreSQL recognizes functional dependency on the primary key, not on - // the Bible's UNIQUE name. Without this the real query fails with 42803. - expect(chain.groupBy.mock.calls[0]).toContain(bibles.id); - }); - - it('returns an operation failure when the assignment query fails', async () => { - const chain = buildProgressSelectChain([]); - chain.groupBy.mockReturnValue({ - orderBy: vi.fn().mockRejectedValue(new Error('database unavailable')), - }); - mockDbSelect.mockReturnValue(chain); - - const result = await repo.findAssignmentsProgress({ projectId: 3 }); - - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error.code).toBe(ErrorCode.INTERNAL_ERROR); - }); - - describe('findAssignmentsProgress hasConflict rollup', () => { - it('surfaces hasConflict per assignment row from the bible-scoped EXISTS', async () => { - const rows = [ - { - assignmentId: 1, - projectId: 3, - projectName: 'P', - projectUnitId: 12, - bibleId: 9, - bibleName: 'Target', - ttsLicenseStatus: 'allowed', - textBibleProvider: 'dbl', - textBibleExternalId: 'opaque-text-ID', - selectedRecordingId: 20, - selectedRecordingProvider: 'aquifer', - selectedRecordingExternalId: '20', - bookId: 1, - bookCode: 'JHN', - bookNameEng: 'John', - chapterNumber: 3, - status: CHAPTER_ASSIGNMENT_STATUS.DRAFT, - targetLanguage: 'en', - targetLangCode: 'eng', - sourceLangCode: 'grc', - totalVerses: 1, - completedVerses: 0, - assignedUserId: 5, - assignedUserDisplayName: 'u', - peerCheckerId: null, - peerCheckerDisplayName: null, - submittedTime: null, - createdAt: new Date(), - updatedAt: new Date(), - isAiEnabled: false, - hasClaimConflict: false, - claimConflictUserId: null, - hasConflict: true, - }, - { - assignmentId: 2, - projectId: 3, - projectName: 'P', - projectUnitId: 12, - bibleId: 10, - bibleName: 'Source', - ttsLicenseStatus: 'unknown', - textBibleProvider: 'dbl', - textBibleExternalId: null, - selectedRecordingId: 88, - selectedRecordingProvider: 'youversion', - selectedRecordingExternalId: '88', - bookId: 1, - bookCode: 'JHN', - bookNameEng: 'John', - chapterNumber: 3, - status: CHAPTER_ASSIGNMENT_STATUS.DRAFT, - targetLanguage: 'en', - targetLangCode: 'eng', - sourceLangCode: 'grc', - totalVerses: 1, - completedVerses: 0, - assignedUserId: 5, - assignedUserDisplayName: 'u', - peerCheckerId: null, - peerCheckerDisplayName: null, - submittedTime: null, - createdAt: new Date(), - updatedAt: new Date(), - isAiEnabled: false, - hasClaimConflict: false, - claimConflictUserId: null, - hasConflict: false, - }, - ]; - - let selection: Record | undefined; - vi.mocked(db.select).mockImplementation((sel) => { - selection = sel as Record; - return buildProgressSelectChain(rows) as any; - }); - - const result = await repo.findAssignmentsProgress({ projectId: 3 }); - - expect(selection?.hasConflict).toBeDefined(); - expect(sqlText(selection!.hasConflict as SQL)).toContain('bt.bible_id'); - expect(result.ok).toBe(true); - if (result.ok) { - expect(result.data).toHaveLength(2); - expect(result.data.map(({ assignmentId }) => assignmentId)).toEqual([1, 2]); - expect(result.data.find((r) => r.bibleId === 9)).toMatchObject({ - hasConflict: true, - textBibleKey: 'dbl-opaque-text-ID', - selectedRecordingKey: 'aq-20', - }); - expect(result.data.find((r) => r.bibleId === 10)).toMatchObject({ - hasConflict: false, - textBibleKey: null, - selectedRecordingKey: 'yv-88', - }); - for (const row of result.data) { - expect(row).not.toHaveProperty('textBibleProvider'); - expect(row).not.toHaveProperty('textBibleExternalId'); - expect(row).not.toHaveProperty('selectedRecordingId'); - expect(row).not.toHaveProperty('selectedRecordingProvider'); - expect(row).not.toHaveProperty('selectedRecordingExternalId'); - } - } - }); - }); - describe('claimIfUnassigned', () => { it('returns claimed=true when the conditional update affects a row', async () => { const record = { diff --git a/src/domains/chapter-assignments/chapter-assignments.repository.ts b/src/domains/chapter-assignments/chapter-assignments.repository.ts index c23d21f8..ffaa1914 100644 --- a/src/domains/chapter-assignments/chapter-assignments.repository.ts +++ b/src/domains/chapter-assignments/chapter-assignments.repository.ts @@ -1,37 +1,27 @@ -import { and, eq, inArray, isNull, ne, or, sql } from 'drizzle-orm'; -import { alias } from 'drizzle-orm/pg-core'; +import { and, eq, inArray, isNull, ne, or } from 'drizzle-orm'; -import type { Provider } from '@/domains/bible-provider-resources/bible-provider-resources.types'; import type { DbTransaction, Result, USJDocument } from '@/lib/types'; import type { VerseData } from '@/lib/usfm-converter'; import { db } from '@/db'; import { - bible_provider_resources, bible_texts, - bibles, books, chapter_assignment_assigned_user_history, chapter_assignment_snapshots, chapter_assignment_status_history, chapter_assignments, - languages, - project_unit_bible_books, project_units, projects, translated_verses, - users, } from '@/db/schema'; -import { bibleKey } from '@/domains/bible-provider-resources/bible-provider-resources.identity'; import { resolveIsProjectMember } from '@/domains/projects/users/project-users.service'; -import { VERSE_AUDIO_CONFLICT_STATUS } from '@/domains/verse-audio/verse-audio.types'; import { logger } from '@/lib/logger'; import { err, ErrorCode, ok } from '@/lib/types'; import { convertUSFMToUSJ, generateUSFMText } from '@/lib/usfm-converter'; import type { PolicyChapterAssignment } from './chapter-assignments.policy'; import type { - ChapterAssignmentProgressInfo, ChapterAssignmentRecord, ChapterAssignmentRecordWithOrg, ChapterAssignmentStatus, @@ -49,62 +39,6 @@ export interface ChapterAssignmentWithAuthContext extends ChapterAssignmentRecor const USJ_SPEC_VERSION = '0.0.1'; -type AssignmentProgressRow = Omit< - ChapterAssignmentProgressInfo, - 'textBibleKey' | 'selectedRecordingKey' -> & { - textBibleProvider: Provider; - textBibleExternalId: string | null; - selectedRecordingId: number | null; - selectedRecordingProvider: Provider | null; - selectedRecordingExternalId: string | null; -}; - -function toAssignmentProgressInfo({ - textBibleProvider, - textBibleExternalId, - selectedRecordingId, - selectedRecordingProvider, - selectedRecordingExternalId, - ...row -}: AssignmentProgressRow): ChapterAssignmentProgressInfo { - return { - ...row, - textBibleKey: - textBibleExternalId === null - ? null - : bibleKey({ provider: textBibleProvider, externalId: textBibleExternalId }), - selectedRecordingKey: - selectedRecordingId === null || - selectedRecordingProvider === null || - selectedRecordingExternalId === null - ? null - : bibleKey({ - provider: selectedRecordingProvider, - externalId: selectedRecordingExternalId, - }), - }; -} - -/** - * EXISTS subquery for assignment progress: true when any verse-audio unit in - * this assignment's bible/book/chapter is conflicted. Scoping by - * `chapter_assignments.bibleId` (not book/chapter alone) is what keeps a - * conflict in another Bible from lighting up the rollup. - */ -export function hasConflictRollupSql() { - return sql`EXISTS ( - SELECT 1 - FROM verse_audio_recordings var - INNER JOIN bible_texts bt ON bt.id = var.bible_text_id - WHERE var.project_unit_id = ${chapter_assignments.projectUnitId} - AND bt.bible_id = ${chapter_assignments.bibleId} - AND bt.book_id = ${chapter_assignments.bookId} - AND bt.chapter_number = ${chapter_assignments.chapterNumber} - AND var.conflict_status = ${VERSE_AUDIO_CONFLICT_STATUS.CONFLICT} - )`; -} - export async function findById( id: number, tx?: DbTransaction @@ -407,7 +341,7 @@ export async function claimIfUnassigned( export async function submitPeerCheckIfEligible( id: number, userId: number, - nextStatus: string, + nextStatus: ChapterAssignmentStatus, submittedTime: Date, tx: DbTransaction ): Promise { @@ -515,163 +449,6 @@ export async function insertSnapshot( await tx.insert(chapter_assignment_snapshots).values(data); } -export async function findAssignmentsProgress( - filters: { - projectId?: number; - orgId?: number; - assignedUserId?: number; - peerCheckerId?: number; - status?: ChapterAssignmentStatus; - }, - tx?: DbTransaction -): Promise> { - try { - const conn = tx ?? db; - const assignedUser = alias(users, 'assignedUser'); - const peerChecker = alias(users, 'peerChecker'); - const sourceLang = alias(languages, 'sourceLang'); - const targetLang = alias(languages, 'targetLang'); - const recording = alias(bible_provider_resources, 'recording'); - - const baseQuery = conn - .select({ - assignmentId: chapter_assignments.id, - projectId: projects.id, - projectName: projects.name, - projectUnitId: chapter_assignments.projectUnitId, - bibleId: chapter_assignments.bibleId, - bibleName: bibles.name, - ttsLicenseStatus: sql< - 'allowed' | 'forbidden' | 'unknown' - >`coalesce(${bible_provider_resources.ttsLicenseStatus}, 'unknown')`, - textBibleProvider: bibles.provider, - textBibleExternalId: bibles.externalId, - selectedRecordingId: recording.id, - selectedRecordingProvider: recording.provider, - selectedRecordingExternalId: recording.externalId, - bookId: chapter_assignments.bookId, - bookCode: books.code, - bookNameEng: books.eng_display_name, - chapterNumber: chapter_assignments.chapterNumber, - status: chapter_assignments.status, - targetLanguage: targetLang.langName, - // ISO 639-3 code consumed by the repeated-words check as greek-room's - // lang_code (whitelist is keyed on the code). See phase-04 BUG #2. - targetLangCode: targetLang.langCodeIso6393, - sourceLangCode: sourceLang.langCodeIso6393, - totalVerses: sql`COUNT(${bible_texts.id})`.mapWith(Number), - completedVerses: - sql`COUNT(CASE WHEN ${translated_verses.content} != '' AND ${translated_verses.content} IS NOT NULL THEN 1 END)`.mapWith( - Number - ), - assignedUserId: chapter_assignments.assignedUserId, - assignedUserDisplayName: assignedUser.username, - peerCheckerId: chapter_assignments.peerCheckerId, - peerCheckerDisplayName: peerChecker.username, - submittedTime: chapter_assignments.submittedTime, - createdAt: chapter_assignments.createdAt, - updatedAt: chapter_assignments.updatedAt, - isAiEnabled: chapter_assignments.isAiEnabled, - hasClaimConflict: chapter_assignments.hasClaimConflict, - claimConflictUserId: chapter_assignments.claimConflictUserId, - hasConflict: hasConflictRollupSql().mapWith(Boolean), - }) - .from(chapter_assignments) - .innerJoin(project_units, eq(chapter_assignments.projectUnitId, project_units.id)) - .innerJoin( - project_unit_bible_books, - and( - eq(project_unit_bible_books.projectUnitId, chapter_assignments.projectUnitId), - eq(project_unit_bible_books.bookId, chapter_assignments.bookId) - ) - ) - .innerJoin(projects, eq(project_units.projectId, projects.id)) - .innerJoin(books, eq(chapter_assignments.bookId, books.id)) - .innerJoin(bibles, eq(bibles.id, chapter_assignments.bibleId)) - // `bibles.provider` deliberately remains the narrower DBL text-ingest enum, - // while provider resources cover every playback identity. PostgreSQL cannot - // compare those enum types directly, so this relational join casts to text. - // Consolidating the broader provider vocabulary is deferred maintenance. - .leftJoin( - bible_provider_resources, - and( - sql`${bibles.provider}::text = ${bible_provider_resources.provider}::text`, - eq(bibles.externalId, bible_provider_resources.externalId) - ) - ) - .leftJoin(recording, eq(bibles.audioResourceId, recording.id)) - .leftJoin(targetLang, eq(projects.targetLanguage, targetLang.id)) - .leftJoin(sourceLang, eq(projects.sourceLanguage, sourceLang.id)) - .leftJoin(assignedUser, eq(chapter_assignments.assignedUserId, assignedUser.id)) - .leftJoin(peerChecker, eq(chapter_assignments.peerCheckerId, peerChecker.id)) - .innerJoin( - bible_texts, - and( - eq(bible_texts.bibleId, chapter_assignments.bibleId), - eq(bible_texts.bookId, chapter_assignments.bookId), - eq(bible_texts.chapterNumber, chapter_assignments.chapterNumber) - ) - ) - .leftJoin( - translated_verses, - and( - eq(translated_verses.bibleTextId, bible_texts.id), - eq(translated_verses.projectUnitId, chapter_assignments.projectUnitId) - ) - ); - - const conditions = [isNull(project_unit_bible_books.deletedAt)]; - if (filters.projectId !== undefined) { - conditions.push(eq(project_units.projectId, filters.projectId)); - } - if (filters.orgId !== undefined) { - conditions.push(eq(projects.organization, filters.orgId)); - } - if (filters.assignedUserId !== undefined) { - conditions.push(eq(chapter_assignments.assignedUserId, filters.assignedUserId)); - } - if (filters.peerCheckerId !== undefined) { - conditions.push(eq(chapter_assignments.peerCheckerId, filters.peerCheckerId)); - } - if (filters.status !== undefined) { - conditions.push(eq(chapter_assignments.status, filters.status)); - } - - let finalQuery = baseQuery; - if (conditions.length > 0) { - finalQuery = finalQuery.where(and(...conditions)) as any; - } - - const rows = await finalQuery - .groupBy( - chapter_assignments.id, - projects.id, - projects.name, - bibles.id, - bibles.name, - bible_provider_resources.id, - recording.id, - targetLang.langName, - targetLang.langCodeIso6393, - sourceLang.langCodeIso6393, - books.code, - books.eng_display_name, - assignedUser.username, - peerChecker.username - ) - .orderBy(projects.name, books.eng_display_name, chapter_assignments.chapterNumber); - - return ok(rows.map(toAssignmentProgressInfo)); - } catch (error) { - logger.error({ - cause: error, - message: 'Failed to find assignments progress', - context: { filters }, - }); - return err(ErrorCode.INTERNAL_ERROR); - } -} - export async function findProjectIdByUnitId( unitId: number, tx?: DbTransaction diff --git a/src/domains/chapter-assignments/chapter-assignments.service.ts b/src/domains/chapter-assignments/chapter-assignments.service.ts index 1a5a1970..0b45772b 100644 --- a/src/domains/chapter-assignments/chapter-assignments.service.ts +++ b/src/domains/chapter-assignments/chapter-assignments.service.ts @@ -12,7 +12,6 @@ import { err, ErrorCode, ok } from '@/lib/types'; import type { ChapterAssignmentWithAuthContext } from './chapter-assignments.repository'; import type { - ChapterAssignmentProgressInfo, ChapterAssignmentRecord, ChapterAssignmentResponse, ChapterAssignmentStatus, @@ -21,6 +20,11 @@ import type { } from './chapter-assignments.types'; import * as repo from './chapter-assignments.repository'; +import { + applyAutoTransition, + recordStatusChange, + recordUserAssignmentChanges, +} from './chapter-assignments.transitions'; import { CHAPTER_ASSIGNMENT_STATUS } from './chapter-assignments.types'; export function toChapterAssignmentResponse( @@ -55,18 +59,7 @@ export function getChapterAssignmentWithAuthContext( return repo.findByIdWithAuthContext(id, userId); } -export function getAssignmentsProgress( - filters: { - projectId?: number; - orgId?: number; - assignedUserId?: number; - peerCheckerId?: number; - status?: ChapterAssignmentStatus; - }, - tx?: DbTransaction -): Promise> { - return repo.findAssignmentsProgress(filters, tx); -} +export { getAssignmentsProgress } from './progress/chapter-assignments-progress.service'; export function getAssignmentForVerse(projectUnitId: number, bibleTextId: number) { return repo.findForVerse(projectUnitId, bibleTextId); @@ -163,15 +156,15 @@ async function createChapterAssignmentForProjectUnitInTransaction( bibleId: c.bibleId, bookId: c.bookId, chapterNumber: c.chapterNumber, - assignedUserId: null as null, - peerCheckerId: null as null, + assignedUserId: null, + peerCheckerId: null, })); const inserted = await repo.insertMany(records, tx); if (inserted.length > 0) { const historyRecords = inserted.map((a) => ({ chapterAssignmentId: a.id, - status: 'not_started' as ChapterAssignmentStatus, + status: CHAPTER_ASSIGNMENT_STATUS.NOT_STARTED, })); await repo.insertManyStatusHistory(tx, historyRecords); } @@ -203,11 +196,11 @@ export async function createChapterAssignmentForProjectUnit( ); return ok(inserted); - } catch (error: any) { + } catch (error) { logger.error({ cause: error, - message: `Failed to create chapter assignments for project unit: ${error?.message || String(error)}`, - stack: error?.stack, + message: `Failed to create chapter assignments for project unit: ${error instanceof Error ? error.message : String(error)}`, + stack: error instanceof Error ? error.stack : undefined, context: { projectUnitId, bibleId, bookIds }, }); return err(ErrorCode.INTERNAL_ERROR); @@ -459,70 +452,6 @@ export async function claimChapterAssignment(id: number, userId: number) { } } -function applyAutoTransition( - current: ChapterAssignmentRecord, - data: UpdateChapterAssignmentRequestData -): UpdateChapterAssignmentRequestData { - const hasUserAssignment = data.assignedUserId !== undefined || data.peerCheckerId !== undefined; - const shouldAutoTransitionToDraft = - current.status === CHAPTER_ASSIGNMENT_STATUS.NOT_STARTED && hasUserAssignment && !data.status; - - return { - ...data, - ...(shouldAutoTransitionToDraft && { status: CHAPTER_ASSIGNMENT_STATUS.DRAFT }), - }; -} - -async function recordStatusChange( - tx: DbTransaction, - current: ChapterAssignmentRecord, - updated: ChapterAssignmentRecord -) { - if (updated.status !== current.status) { - await repo.insertStatusHistory(tx, updated.id, updated.status as ChapterAssignmentStatus); - } -} - -async function recordUserAssignmentChanges( - tx: DbTransaction, - current: ChapterAssignmentRecord, - updated: ChapterAssignmentRecord, - data: UpdateChapterAssignmentRequestData -): Promise { - let shouldTriggerAi = false; - - if ( - data.assignedUserId !== undefined && - data.assignedUserId !== current.assignedUserId && - updated.assignedUserId - ) { - await repo.insertUserAssignmentHistory( - tx, - updated.id, - updated.assignedUserId, - 'drafter', - updated.status as ChapterAssignmentStatus - ); - shouldTriggerAi = true; - } - - if ( - data.peerCheckerId !== undefined && - data.peerCheckerId !== current.peerCheckerId && - updated.peerCheckerId - ) { - await repo.insertUserAssignmentHistory( - tx, - updated.id, - updated.peerCheckerId, - 'peer_checker', - updated.status as ChapterAssignmentStatus - ); - } - - return shouldTriggerAi; -} - export async function updateChapterAssignmentAiStatus( assignmentId: number, isAiEnabled: boolean @@ -575,8 +504,8 @@ export async function updateChapterAssignmentAiStatus( } return ok(undefined); - } catch (error: any) { - if (error.message === ErrorCode.CHAPTER_ASSIGNMENT_NOT_FOUND) { + } catch (error) { + if (error instanceof Error && error.message === ErrorCode.CHAPTER_ASSIGNMENT_NOT_FOUND) { return err(ErrorCode.CHAPTER_ASSIGNMENT_NOT_FOUND); } logger.error({ diff --git a/src/domains/chapter-assignments/chapter-assignments.transitions.test.ts b/src/domains/chapter-assignments/chapter-assignments.transitions.test.ts new file mode 100644 index 00000000..80983b9d --- /dev/null +++ b/src/domains/chapter-assignments/chapter-assignments.transitions.test.ts @@ -0,0 +1,127 @@ +import { describe, expect, it, vi } from 'vitest'; + +import type { DbTransaction } from '@/lib/types'; + +import type { ChapterAssignmentRecord } from './chapter-assignments.types'; + +import * as repo from './chapter-assignments.repository'; +import { + applyAutoTransition, + recordStatusChange, + recordUserAssignmentChanges, +} from './chapter-assignments.transitions'; +import { CHAPTER_ASSIGNMENT_STATUS } from './chapter-assignments.types'; + +vi.mock('./chapter-assignments.repository', () => ({ + insertStatusHistory: vi.fn(async () => {}), + insertUserAssignmentHistory: vi.fn(async () => {}), +})); + +const mockRecord: ChapterAssignmentRecord = { + id: 1, + projectUnitId: 10, + bibleId: 20, + bookId: 30, + chapterNumber: 1, + assignedUserId: null, + peerCheckerId: null, + status: CHAPTER_ASSIGNMENT_STATUS.NOT_STARTED, + submittedTime: null, + isAiEnabled: false, + hasClaimConflict: false, + claimConflictUserId: null, + createdAt: new Date('2026-01-01T00:00:00Z'), + updatedAt: new Date('2026-01-01T00:00:00Z'), +}; + +describe('chapter-assignments transitions', () => { + describe('applyAutoTransition', () => { + it('auto-transitions NOT_STARTED to DRAFT when assigning a user', () => { + const result = applyAutoTransition(mockRecord, { assignedUserId: 5 }); + expect(result.status).toBe(CHAPTER_ASSIGNMENT_STATUS.DRAFT); + }); + + it('auto-transitions NOT_STARTED to DRAFT when unassigning a user (assignedUserId: null)', () => { + const result = applyAutoTransition(mockRecord, { assignedUserId: null }); + expect(result.status).toBe(CHAPTER_ASSIGNMENT_STATUS.DRAFT); + }); + + it('auto-transitions NOT_STARTED to DRAFT when assigning a peer checker', () => { + const result = applyAutoTransition(mockRecord, { peerCheckerId: 8 }); + expect(result.status).toBe(CHAPTER_ASSIGNMENT_STATUS.DRAFT); + }); + + it('does not auto-transition when neither user field is provided in update', () => { + const result = applyAutoTransition(mockRecord, { isAiEnabled: true }); + expect(result.status).toBeUndefined(); + }); + + it('preserves explicit status override if provided in input', () => { + const result = applyAutoTransition(mockRecord, { + assignedUserId: 5, + status: CHAPTER_ASSIGNMENT_STATUS.COMPLETE, + }); + expect(result.status).toBe(CHAPTER_ASSIGNMENT_STATUS.COMPLETE); + }); + + it('does not transition if assignment is already in DRAFT', () => { + const current = { ...mockRecord, status: CHAPTER_ASSIGNMENT_STATUS.DRAFT }; + const result = applyAutoTransition(current, { assignedUserId: 5 }); + expect(result.status).toBeUndefined(); + }); + }); + + describe('recordStatusChange', () => { + it('inserts status history when status changed', async () => { + const tx = {} as DbTransaction; + const updated = { ...mockRecord, status: CHAPTER_ASSIGNMENT_STATUS.DRAFT }; + await recordStatusChange(tx, mockRecord, updated); + expect(repo.insertStatusHistory).toHaveBeenCalledWith( + tx, + mockRecord.id, + CHAPTER_ASSIGNMENT_STATUS.DRAFT + ); + }); + + it('does nothing when status unchanged', async () => { + vi.clearAllMocks(); + const tx = {} as DbTransaction; + await recordStatusChange(tx, mockRecord, mockRecord); + expect(repo.insertStatusHistory).not.toHaveBeenCalled(); + }); + }); + + describe('recordUserAssignmentChanges', () => { + it('records drafter change and triggers AI when assignedUserId changed', async () => { + const tx = {} as DbTransaction; + const updated = { ...mockRecord, assignedUserId: 5 }; + const shouldTriggerAi = await recordUserAssignmentChanges(tx, mockRecord, updated, { + assignedUserId: 5, + }); + expect(repo.insertUserAssignmentHistory).toHaveBeenCalledWith( + tx, + updated.id, + 5, + 'drafter', + updated.status + ); + expect(shouldTriggerAi).toBe(true); + }); + + it('records peer checker change when peerCheckerId changed', async () => { + const tx = {} as DbTransaction; + const updated = { ...mockRecord, peerCheckerId: 9 }; + const shouldTriggerAi = await recordUserAssignmentChanges(tx, mockRecord, updated, { + peerCheckerId: 9, + }); + expect(repo.insertUserAssignmentHistory).toHaveBeenCalledWith( + tx, + updated.id, + 9, + 'peer_checker', + updated.status + ); + expect(shouldTriggerAi).toBe(false); + }); + }); +}); diff --git a/src/domains/chapter-assignments/chapter-assignments.transitions.ts b/src/domains/chapter-assignments/chapter-assignments.transitions.ts new file mode 100644 index 00000000..348bbae4 --- /dev/null +++ b/src/domains/chapter-assignments/chapter-assignments.transitions.ts @@ -0,0 +1,74 @@ +import type { DbTransaction } from '@/lib/types'; + +import type { + ChapterAssignmentRecord, + ChapterAssignmentStatus, + UpdateChapterAssignmentRequestData, +} from './chapter-assignments.types'; + +import * as repo from './chapter-assignments.repository'; +import { CHAPTER_ASSIGNMENT_STATUS } from './chapter-assignments.types'; + +export function applyAutoTransition( + current: ChapterAssignmentRecord, + data: UpdateChapterAssignmentRequestData +): UpdateChapterAssignmentRequestData { + const hasUserAssignment = data.assignedUserId !== undefined || data.peerCheckerId !== undefined; + const shouldAutoTransitionToDraft = + current.status === CHAPTER_ASSIGNMENT_STATUS.NOT_STARTED && hasUserAssignment && !data.status; + + return { + ...data, + ...(shouldAutoTransitionToDraft && { status: CHAPTER_ASSIGNMENT_STATUS.DRAFT }), + }; +} + +export async function recordStatusChange( + tx: DbTransaction, + current: ChapterAssignmentRecord, + updated: ChapterAssignmentRecord +) { + if (updated.status !== current.status) { + await repo.insertStatusHistory(tx, updated.id, updated.status as ChapterAssignmentStatus); + } +} + +export async function recordUserAssignmentChanges( + tx: DbTransaction, + current: ChapterAssignmentRecord, + updated: ChapterAssignmentRecord, + data: UpdateChapterAssignmentRequestData +): Promise { + let shouldTriggerAi = false; + + if ( + data.assignedUserId !== undefined && + data.assignedUserId !== current.assignedUserId && + updated.assignedUserId + ) { + await repo.insertUserAssignmentHistory( + tx, + updated.id, + updated.assignedUserId, + 'drafter', + updated.status as ChapterAssignmentStatus + ); + shouldTriggerAi = true; + } + + if ( + data.peerCheckerId !== undefined && + data.peerCheckerId !== current.peerCheckerId && + updated.peerCheckerId + ) { + await repo.insertUserAssignmentHistory( + tx, + updated.id, + updated.peerCheckerId, + 'peer_checker', + updated.status as ChapterAssignmentStatus + ); + } + + return shouldTriggerAi; +} diff --git a/src/domains/chapter-assignments/progress/chapter-assignments-progress.repository.integration.test.ts b/src/domains/chapter-assignments/progress/chapter-assignments-progress.repository.integration.test.ts new file mode 100644 index 00000000..03bdab7d --- /dev/null +++ b/src/domains/chapter-assignments/progress/chapter-assignments-progress.repository.integration.test.ts @@ -0,0 +1,222 @@ +import { generateDrizzleJson, generateMigration } from 'drizzle-kit/api'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; + +import * as schema from '@/db/schema'; +import { VERSE_AUDIO_CONFLICT_STATUS } from '@/domains/verse-audio/verse-audio.types'; + +import { CHAPTER_ASSIGNMENT_STATUS } from '../chapter-assignments.types'; +import * as progressRepo from './chapter-assignments-progress.repository'; + +const { client, database } = await vi.hoisted(async () => { + const { PGlite } = await import('@electric-sql/pglite'); + const { drizzle } = await import('drizzle-orm/pglite'); + const client = new PGlite(); + return { client, database: drizzle(client) }; +}); + +vi.mock('@/db', () => ({ db: database })); +vi.mock('@/lib/logger', () => ({ + logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() }, +})); + +describe('chapter-assignments-progress repository (PGlite integration)', () => { + let projectId: number; + let projectUnitId: number; + let bibleId: number; + let bookId: number; + let chapterAssignmentId: number; + + beforeAll(async () => { + const statements = await generateMigration( + generateDrizzleJson({}), + generateDrizzleJson({ + organizations: schema.organizations, + languages: schema.languages, + users: schema.users, + projects: schema.projects, + pericope_sets: schema.pericope_sets, + project_units: schema.project_units, + books: schema.books, + bible_provider_resources: schema.bible_provider_resources, + bibles: schema.bibles, + project_unit_bible_books: schema.project_unit_bible_books, + chapter_assignments: schema.chapter_assignments, + bible_texts: schema.bible_texts, + translated_verses: schema.translated_verses, + storage_objects: schema.storage_objects, + verse_audio_recordings: schema.verse_audio_recordings, + verse_audio_takes: schema.verse_audio_takes, + authUser: schema.authUser, + authSession: schema.authSession, + bibleProviderEnum: schema.bibleProviderEnum, + resourceProviderEnum: schema.resourceProviderEnum, + ttsLicenseStatusEnum: schema.ttsLicenseStatusEnum, + scriptDirectionEnum: schema.scriptDirectionEnum, + projectAssignmentStatusEnum: schema.projectAssignmentStatusEnum, + verseAudioConflictStatusEnum: schema.verseAudioConflictStatusEnum, + userStatusEnum: schema.userStatusEnum, + milestoneTypeEnum: schema.milestoneTypeEnum, + projectStatusEnum: schema.projectStatusEnum, + }) + ); + await client.exec(statements.join('\n')); + + const [org] = await database + .insert(schema.organizations) + .values({ name: 'Test Org' }) + .returning(); + + const [lang] = await database + .insert(schema.languages) + .values({ langName: 'English', langCodeIso6393: 'eng' }) + .returning(); + + const [user] = await database + .insert(schema.users) + .values({ username: 'testuser', email: 'test@example.com' }) + .returning(); + + const [checker] = await database + .insert(schema.users) + .values({ username: 'checkeruser', email: 'checker@example.com' }) + .returning(); + + const [project] = await database + .insert(schema.projects) + .values({ + name: 'Test Project', + organization: org.id, + sourceLanguage: lang.id, + targetLanguage: lang.id, + }) + .returning(); + projectId = project.id; + + const [unit] = await database.insert(schema.project_units).values({ projectId }).returning(); + projectUnitId = unit.id; + + const [book] = await database + .insert(schema.books) + .values({ code: 'JHN', eng_display_name: 'John' }) + .returning(); + bookId = book.id; + + const [, audioResource] = await database + .insert(schema.bible_provider_resources) + .values([ + { + provider: 'dbl', + externalId: 'opaque-text-ID', + ttsLicenseStatus: 'allowed', + }, + { + provider: 'aquifer', + externalId: 'res-rec-1', + ttsLicenseStatus: 'unknown', + }, + ]) + .returning(); + + const [bible] = await database + .insert(schema.bibles) + .values({ + name: 'Test Bible', + abbreviation: 'TB', + languageId: lang.id, + provider: 'dbl', + externalId: 'opaque-text-ID', + audioResourceId: audioResource.id, + }) + .returning(); + bibleId = bible.id; + + await database.insert(schema.project_unit_bible_books).values({ + projectUnitId, + bibleId, + bookId, + }); + + const [assignment] = await database + .insert(schema.chapter_assignments) + .values({ + projectUnitId, + bibleId, + bookId, + chapterNumber: 3, + status: CHAPTER_ASSIGNMENT_STATUS.DRAFT, + assignedUserId: user.id, + peerCheckerId: checker.id, + isAiEnabled: false, + }) + .returning(); + chapterAssignmentId = assignment.id; + + const [text1, text2] = await database + .insert(schema.bible_texts) + .values([ + { bibleId, bookId, chapterNumber: 3, verseNumber: 1, text: 'Verse 1' }, + { bibleId, bookId, chapterNumber: 3, verseNumber: 2, text: 'Verse 2' }, + ]) + .returning(); + + await database.insert(schema.translated_verses).values([ + { projectUnitId, bibleTextId: text1.id, content: 'Translated 1' }, + { projectUnitId, bibleTextId: text2.id, content: '' }, + ]); + + await database.insert(schema.verse_audio_recordings).values({ + projectUnitId, + bibleTextId: text1.id, + uploadedBy: user.id, + contentType: 'audio/mp4', + sizeBytes: 100, + conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT, + }); + }, 30_000); + + afterAll(async () => { + await client.close(); + }); + + it('surfaces assignment progress, counts, keys, and hasConflict from real PostgreSQL', async () => { + const result = await progressRepo.findAssignmentsProgress({ projectId }); + + expect(result.ok).toBe(true); + if (!result.ok) return; + + expect(result.data).toHaveLength(1); + const row = result.data[0]; + expect(row).toMatchObject({ + assignmentId: chapterAssignmentId, + projectId, + projectName: 'Test Project', + projectUnitId, + bibleId, + bibleName: 'Test Bible', + ttsLicenseStatus: 'allowed', + textBibleKey: 'dbl-opaque-text-ID', + selectedRecordingKey: 'aq-res-rec-1', + bookId, + bookCode: 'JHN', + bookNameEng: 'John', + chapterNumber: 3, + status: CHAPTER_ASSIGNMENT_STATUS.DRAFT, + targetLanguage: 'English', + targetLangCode: 'eng', + sourceLangCode: 'eng', + totalVerses: 2, + completedVerses: 1, + assignedUserDisplayName: 'testuser', + peerCheckerDisplayName: 'checkeruser', + hasConflict: true, + }); + }); + + it('filters by non-matching projectId and returns empty array', async () => { + const result = await progressRepo.findAssignmentsProgress({ projectId: 9999 }); + + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.data).toEqual([]); + }); +}); diff --git a/src/domains/chapter-assignments/progress/chapter-assignments-progress.repository.test.ts b/src/domains/chapter-assignments/progress/chapter-assignments-progress.repository.test.ts new file mode 100644 index 00000000..b4fa5a85 --- /dev/null +++ b/src/domains/chapter-assignments/progress/chapter-assignments-progress.repository.test.ts @@ -0,0 +1,63 @@ +import type { SQL } from 'drizzle-orm'; + +import { describe, expect, it } from 'vitest'; + +import { VERSE_AUDIO_CONFLICT_STATUS } from '@/domains/verse-audio/verse-audio.types'; + +import * as progressRepo from './chapter-assignments-progress.repository'; + +function sqlText(fragment: SQL): string { + const parts: string[] = []; + const walk = (chunk: unknown): void => { + if (chunk == null) { + return; + } + if (typeof chunk === 'string') { + parts.push(chunk); + return; + } + if (typeof chunk !== 'object') { + parts.push(String(chunk)); + return; + } + if ('value' in chunk && Array.isArray((chunk as { value: unknown[] }).value)) { + for (const piece of (chunk as { value: unknown[] }).value) { + walk(piece); + } + return; + } + if ('queryChunks' in chunk && Array.isArray((chunk as SQL).queryChunks)) { + for (const piece of (chunk as SQL).queryChunks) { + walk(piece); + } + } + }; + walk(fragment); + return parts.join(''); +} + +describe('chapter-assignments-progress.repository', () => { + describe('hasConflictRollupSql', () => { + it('correlates verse bible to the assignment bible so other-Bible conflicts are excluded', () => { + const fragment = progressRepo.hasConflictRollupSql(); + const text = sqlText(fragment); + + expect(text).toContain('bt.bible_id'); + expect(text).toContain('bt.book_id'); + expect(text).toContain('bt.chapter_number'); + expect(text).toContain('var.project_unit_id'); + expect(text).toContain('var.conflict_status'); + expect(text).not.toMatch(/conflict_status\s*=\s*'conflict'/); + expect(VERSE_AUDIO_CONFLICT_STATUS.CONFLICT).toBe('conflict'); + }); + + it('filters strictly on conflict status so clean and resolved recordings are excluded', () => { + const fragment = progressRepo.hasConflictRollupSql(); + const text = sqlText(fragment); + + expect(text).toMatch(/var\.conflict_status\s*=\s*conflict/); + expect(text).not.toContain('resolved'); + expect(text).not.toContain('clean'); + }); + }); +}); diff --git a/src/domains/chapter-assignments/progress/chapter-assignments-progress.repository.ts b/src/domains/chapter-assignments/progress/chapter-assignments-progress.repository.ts new file mode 100644 index 00000000..2a4ed2a7 --- /dev/null +++ b/src/domains/chapter-assignments/progress/chapter-assignments-progress.repository.ts @@ -0,0 +1,230 @@ +import { and, eq, isNull, sql } from 'drizzle-orm'; +import { alias } from 'drizzle-orm/pg-core'; + +import type { Provider } from '@/domains/bible-provider-resources/bible-provider-resources.types'; +import type { DbTransaction, Result } from '@/lib/types'; + +import { db } from '@/db'; +import { + bible_provider_resources, + bible_texts, + bibles, + books, + chapter_assignments, + languages, + project_unit_bible_books, + project_units, + projects, + translated_verses, + users, +} from '@/db/schema'; +import { bibleKey } from '@/domains/bible-provider-resources/bible-provider-resources.identity'; +import { VERSE_AUDIO_CONFLICT_STATUS } from '@/domains/verse-audio/verse-audio.types'; +import { logger } from '@/lib/logger'; +import { err, ErrorCode, ok } from '@/lib/types'; + +import type { ChapterAssignmentProgressInfo } from '../chapter-assignments.types'; +import type { ChapterAssignmentsProgressFilters } from './chapter-assignments-progress.types'; + +type AssignmentProgressRow = Omit< + ChapterAssignmentProgressInfo, + 'textBibleKey' | 'selectedRecordingKey' +> & { + textBibleProvider: Provider; + textBibleExternalId: string | null; + selectedRecordingId: number | null; + selectedRecordingProvider: Provider | null; + selectedRecordingExternalId: string | null; +}; + +function toAssignmentProgressInfo({ + textBibleProvider, + textBibleExternalId, + selectedRecordingId, + selectedRecordingProvider, + selectedRecordingExternalId, + ...row +}: AssignmentProgressRow): ChapterAssignmentProgressInfo { + return { + ...row, + textBibleKey: + textBibleExternalId === null + ? null + : bibleKey({ provider: textBibleProvider, externalId: textBibleExternalId }), + selectedRecordingKey: + selectedRecordingId === null || + selectedRecordingProvider === null || + selectedRecordingExternalId === null + ? null + : bibleKey({ + provider: selectedRecordingProvider, + externalId: selectedRecordingExternalId, + }), + }; +} + +/** + * EXISTS subquery for assignment progress: true when any verse-audio unit in + * this assignment's bible/book/chapter is conflicted. Scoping by + * `chapter_assignments.bibleId` (not book/chapter alone) is what keeps a + * conflict in another Bible from lighting up the rollup. + */ +export function hasConflictRollupSql() { + return sql`EXISTS ( + SELECT 1 + FROM verse_audio_recordings var + INNER JOIN bible_texts bt ON bt.id = var.bible_text_id + WHERE var.project_unit_id = ${chapter_assignments.projectUnitId} + AND bt.bible_id = ${chapter_assignments.bibleId} + AND bt.book_id = ${chapter_assignments.bookId} + AND bt.chapter_number = ${chapter_assignments.chapterNumber} + AND var.conflict_status = ${VERSE_AUDIO_CONFLICT_STATUS.CONFLICT} + )`; +} + +export async function findAssignmentsProgress( + filters: ChapterAssignmentsProgressFilters, + tx?: DbTransaction +): Promise> { + try { + const conn = tx ?? db; + const assignedUser = alias(users, 'assignedUser'); + const peerChecker = alias(users, 'peerChecker'); + const sourceLang = alias(languages, 'sourceLang'); + const targetLang = alias(languages, 'targetLang'); + const recording = alias(bible_provider_resources, 'recording'); + + const baseQuery = conn + .select({ + assignmentId: chapter_assignments.id, + projectId: projects.id, + projectName: projects.name, + projectUnitId: chapter_assignments.projectUnitId, + bibleId: chapter_assignments.bibleId, + bibleName: bibles.name, + ttsLicenseStatus: sql< + 'allowed' | 'forbidden' | 'unknown' + >`coalesce(${bible_provider_resources.ttsLicenseStatus}, 'unknown')`, + textBibleProvider: bibles.provider, + textBibleExternalId: bibles.externalId, + selectedRecordingId: recording.id, + selectedRecordingProvider: recording.provider, + selectedRecordingExternalId: recording.externalId, + bookId: chapter_assignments.bookId, + bookCode: books.code, + bookNameEng: books.eng_display_name, + chapterNumber: chapter_assignments.chapterNumber, + status: chapter_assignments.status, + targetLanguage: targetLang.langName, + // ISO 639-3 code consumed by the repeated-words check as greek-room's + // lang_code (whitelist is keyed on the code). See phase-04 BUG #2. + targetLangCode: targetLang.langCodeIso6393, + sourceLangCode: sourceLang.langCodeIso6393, + totalVerses: sql`COUNT(${bible_texts.id})`.mapWith(Number), + completedVerses: + sql`COUNT(CASE WHEN ${translated_verses.content} != '' AND ${translated_verses.content} IS NOT NULL THEN 1 END)`.mapWith( + Number + ), + assignedUserId: chapter_assignments.assignedUserId, + assignedUserDisplayName: assignedUser.username, + peerCheckerId: chapter_assignments.peerCheckerId, + peerCheckerDisplayName: peerChecker.username, + submittedTime: chapter_assignments.submittedTime, + createdAt: chapter_assignments.createdAt, + updatedAt: chapter_assignments.updatedAt, + isAiEnabled: chapter_assignments.isAiEnabled, + hasClaimConflict: chapter_assignments.hasClaimConflict, + claimConflictUserId: chapter_assignments.claimConflictUserId, + hasConflict: hasConflictRollupSql().mapWith(Boolean), + }) + .from(chapter_assignments) + .innerJoin(project_units, eq(chapter_assignments.projectUnitId, project_units.id)) + .innerJoin( + project_unit_bible_books, + and( + eq(project_unit_bible_books.projectUnitId, chapter_assignments.projectUnitId), + eq(project_unit_bible_books.bookId, chapter_assignments.bookId) + ) + ) + .innerJoin(projects, eq(project_units.projectId, projects.id)) + .innerJoin(books, eq(chapter_assignments.bookId, books.id)) + .innerJoin(bibles, eq(bibles.id, chapter_assignments.bibleId)) + // `bibles.provider` deliberately remains the narrower DBL text-ingest enum, + // while provider resources cover every playback identity. PostgreSQL cannot + // compare those enum types directly, so this relational join casts to text. + // Consolidating the broader provider vocabulary is deferred maintenance. + .leftJoin( + bible_provider_resources, + and( + sql`${bibles.provider}::text = ${bible_provider_resources.provider}::text`, + eq(bibles.externalId, bible_provider_resources.externalId) + ) + ) + .leftJoin(recording, eq(bibles.audioResourceId, recording.id)) + .leftJoin(targetLang, eq(projects.targetLanguage, targetLang.id)) + .leftJoin(sourceLang, eq(projects.sourceLanguage, sourceLang.id)) + .leftJoin(assignedUser, eq(chapter_assignments.assignedUserId, assignedUser.id)) + .leftJoin(peerChecker, eq(chapter_assignments.peerCheckerId, peerChecker.id)) + .innerJoin( + bible_texts, + and( + eq(bible_texts.bibleId, chapter_assignments.bibleId), + eq(bible_texts.bookId, chapter_assignments.bookId), + eq(bible_texts.chapterNumber, chapter_assignments.chapterNumber) + ) + ) + .leftJoin( + translated_verses, + and( + eq(translated_verses.bibleTextId, bible_texts.id), + eq(translated_verses.projectUnitId, chapter_assignments.projectUnitId) + ) + ); + + const conditions = [isNull(project_unit_bible_books.deletedAt)]; + if (filters.projectId !== undefined) { + conditions.push(eq(project_units.projectId, filters.projectId)); + } + if (filters.orgId !== undefined) { + conditions.push(eq(projects.organization, filters.orgId)); + } + if (filters.assignedUserId !== undefined) { + conditions.push(eq(chapter_assignments.assignedUserId, filters.assignedUserId)); + } + if (filters.peerCheckerId !== undefined) { + conditions.push(eq(chapter_assignments.peerCheckerId, filters.peerCheckerId)); + } + if (filters.status !== undefined) { + conditions.push(eq(chapter_assignments.status, filters.status)); + } + + const rows = await baseQuery + .where(and(...conditions)) + .groupBy( + chapter_assignments.id, + projects.id, + projects.name, + bibles.id, + bibles.name, + bible_provider_resources.id, + recording.id, + targetLang.langName, + targetLang.langCodeIso6393, + sourceLang.langCodeIso6393, + books.code, + books.eng_display_name, + assignedUser.username, + peerChecker.username + ) + .orderBy(projects.name, books.eng_display_name, chapter_assignments.chapterNumber); + + return ok(rows.map(toAssignmentProgressInfo)); + } catch (error) { + logger.error({ + cause: error, + message: 'Failed to find assignments progress', + context: { filters }, + }); + return err(ErrorCode.INTERNAL_ERROR); + } +} diff --git a/src/domains/chapter-assignments/progress/chapter-assignments-progress.service.ts b/src/domains/chapter-assignments/progress/chapter-assignments-progress.service.ts new file mode 100644 index 00000000..697f8ce3 --- /dev/null +++ b/src/domains/chapter-assignments/progress/chapter-assignments-progress.service.ts @@ -0,0 +1,13 @@ +import type { DbTransaction, Result } from '@/lib/types'; + +import type { ChapterAssignmentProgressInfo } from '../chapter-assignments.types'; +import type { ChapterAssignmentsProgressFilters } from './chapter-assignments-progress.types'; + +import * as progressRepo from './chapter-assignments-progress.repository'; + +export function getAssignmentsProgress( + filters: ChapterAssignmentsProgressFilters, + tx?: DbTransaction +): Promise> { + return progressRepo.findAssignmentsProgress(filters, tx); +} diff --git a/src/domains/chapter-assignments/progress/chapter-assignments-progress.types.ts b/src/domains/chapter-assignments/progress/chapter-assignments-progress.types.ts new file mode 100644 index 00000000..3cd8bfc5 --- /dev/null +++ b/src/domains/chapter-assignments/progress/chapter-assignments-progress.types.ts @@ -0,0 +1,9 @@ +import type { ChapterAssignmentStatus } from '../chapter-assignments.types'; + +export interface ChapterAssignmentsProgressFilters { + projectId?: number; + orgId?: number; + assignedUserId?: number; + peerCheckerId?: number; + status?: ChapterAssignmentStatus; +} diff --git a/src/domains/source-audio/source-audio.manifest.test.ts b/src/domains/source-audio/source-audio.manifest.test.ts new file mode 100644 index 00000000..b8fd6ee7 --- /dev/null +++ b/src/domains/source-audio/source-audio.manifest.test.ts @@ -0,0 +1,177 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { Bible } from '@/domains/bibles/bibles.types'; + +import * as biblesRepo from '@/domains/bibles/bibles.repository'; +import { getBibles, getBibleText } from '@/lib/services/aquifer/aquifer.client'; +import { ok } from '@/lib/types'; + +import { getSourceAudioManifest } from './source-audio.service'; + +vi.mock('@/domains/bibles/bibles.repository', () => ({ + getById: vi.fn(), +})); + +vi.mock('@/domains/books/books.service', () => ({ + getBookByCode: vi.fn(), +})); + +vi.mock('@/domains/bibles/bible-audio/bible-audio.service', () => ({ + getSourceAudio: vi.fn(), +})); + +vi.mock('@/lib/services/aquifer/aquifer.client', () => ({ + getBibles: vi.fn(), + getBibleText: vi.fn(), +})); + +vi.mock('./source-audio.repository', () => ({ + isBibleBookLinkedToProject: vi.fn(), +})); + +vi.mock('@/lib/logger', () => ({ + logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() }, +})); + +const fluentBible: Bible = { + id: 1, + name: 'Berean Standard Bible', + abbreviation: 'BSB', + languageId: 1, + provider: 'dbl', + externalId: null, + audioResourceId: null, + hasAudio: false, + createdAt: new Date(), + updatedAt: new Date(), +}; + +describe('getSourceAudioManifest', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(biblesRepo.getById).mockResolvedValue(ok(fluentBible)); + }); + + it('returns empty items when no Aquifer bible matches', async () => { + vi.mocked(getBibles).mockResolvedValue( + ok([{ id: 10, name: 'Unrelated', abbreviation: 'X', isLanguageDefault: true }]) + ); + + const result = await getSourceAudioManifest({ + projectId: 10, + languageCode: 'eng', + fluentBibleId: 1, + bookCode: 'MRK', + startChapter: 14, + endChapter: 14, + }); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.items).toEqual([]); + expect(result.data.totalBytes).toBe(0); + } + expect(getBibleText).not.toHaveBeenCalled(); + }); + + it('does not fall through to a sibling edition when the exact match has no audio', async () => { + vi.mocked(getBibles).mockResolvedValue( + ok([ + { + id: 11, + name: 'Berean Standard Bible', + abbreviation: 'BSB', + hasAudio: false, + }, + { id: 12, name: 'Sibling Edition', abbreviation: 'SIB', hasAudio: true }, + ]) + ); + + const result = await getSourceAudioManifest({ + projectId: 10, + languageCode: 'eng', + fluentBibleId: 1, + bookCode: 'MRK', + startChapter: 14, + endChapter: 14, + }); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.items).toEqual([]); + } + expect(getBibleText).not.toHaveBeenCalled(); + }); +}); + +describe('legacy source and manifest isolation from explicit playback selections', () => { + it('manifest still name-matches and retains download metadata while omitting unknown sizes', async () => { + vi.mocked(biblesRepo.getById).mockResolvedValue(ok({ ...fluentBible, audioResourceId: 999 })); + vi.mocked(getBibles).mockResolvedValue( + ok([{ id: 11, name: 'Berean Standard Bible', abbreviation: 'BSB', hasAudio: true }]) + ); + vi.mocked(getBibleText).mockResolvedValue( + ok({ + bibleId: 11, + bibleName: 'Berean Standard Bible', + bibleAbbreviation: 'BSB', + bookName: 'Mark', + bookCode: 'MRK', + chapters: [ + { + number: 14, + audio: { + mp3: { url: 'https://example.com/a.mp3', size: 99 }, + webm: { url: 'https://example.com/a.webm' }, + }, + verses: [], + }, + ], + }) + ); + expect( + await getSourceAudioManifest({ + projectId: 10, + fluentBibleId: 1, + languageCode: 'eng', + bookCode: 'MRK', + startChapter: 14, + endChapter: 14, + }) + ).toEqual( + ok({ + projectId: 10, + sourceLanguageCode: 'eng', + provider: 'aquifer', + totalBytes: 99, + items: [ + { + id: 'source-audio-11-MRK-14-mp3', + tier: 1, + kind: 'audio', + resourceName: 'Source Bible Audio', + label: 'BSB MRK 14 (mp3)', + required: true, + removable: false, + bytesTotal: 99, + sourceUrl: 'https://example.com/a.mp3', + fileExt: 'mp3', + languageCode: 'eng', + bookCode: 'MRK', + startChapter: 14, + endChapter: 14, + format: 'mp3', + aquiferBibleId: 11, + }, + ], + }) + ); + expect(getBibleText).toHaveBeenCalledWith({ + aquiferBibleId: 11, + bookCode: 'MRK', + startChapter: 14, + endChapter: 14, + includeAudio: true, + }); + }); +}); diff --git a/src/domains/source-audio/source-audio.service.test.ts b/src/domains/source-audio/source-audio.service.test.ts index 340ca08c..2b2a9ac9 100644 --- a/src/domains/source-audio/source-audio.service.test.ts +++ b/src/domains/source-audio/source-audio.service.test.ts @@ -12,7 +12,6 @@ import { err, ErrorCode, ok } from '@/lib/types'; import * as sourceAudioRepo from './source-audio.repository'; import { getChapterSourceAudio, - getSourceAudioManifest, isBibleBookLinkedToProject, matchAquiferBible, } from './source-audio.service'; @@ -466,64 +465,6 @@ describe('getChapterSourceAudio', () => { }); }); -describe('getSourceAudioManifest', () => { - beforeEach(() => { - vi.clearAllMocks(); - vi.mocked(biblesRepo.getById).mockResolvedValue(ok(fluentBible)); - }); - - it('returns empty items when no Aquifer bible matches', async () => { - vi.mocked(getBibles).mockResolvedValue( - ok([{ id: 10, name: 'Unrelated', abbreviation: 'X', isLanguageDefault: true }]) - ); - - const result = await getSourceAudioManifest({ - projectId: 10, - languageCode: 'eng', - fluentBibleId: 1, - bookCode: 'MRK', - startChapter: 14, - endChapter: 14, - }); - - expect(result.ok).toBe(true); - if (result.ok) { - expect(result.data.items).toEqual([]); - expect(result.data.totalBytes).toBe(0); - } - expect(getBibleText).not.toHaveBeenCalled(); - }); - - it('does not fall through to a sibling edition when the exact match has no audio', async () => { - vi.mocked(getBibles).mockResolvedValue( - ok([ - { - id: 11, - name: 'Berean Standard Bible', - abbreviation: 'BSB', - hasAudio: false, - }, - { id: 12, name: 'Sibling Edition', abbreviation: 'SIB', hasAudio: true }, - ]) - ); - - const result = await getSourceAudioManifest({ - projectId: 10, - languageCode: 'eng', - fluentBibleId: 1, - bookCode: 'MRK', - startChapter: 14, - endChapter: 14, - }); - - expect(result.ok).toBe(true); - if (result.ok) { - expect(result.data.items).toEqual([]); - } - expect(getBibleText).not.toHaveBeenCalled(); - }); -}); - describe('legacy source and manifest isolation from explicit playback selections', () => { beforeEach(() => { vi.clearAllMocks(); @@ -532,23 +473,24 @@ describe('legacy source and manifest isolation from explicit playback selections ok({ id: 41, code: 'MRK', eng_display_name: 'Mark', updatedAt: null }) ); }); + it('keeps linked windowless DBL ahead of Aquifer despite a selected recording FK', async () => { vi.mocked(bibleAudioService.getSourceAudio).mockResolvedValue( ok([ { audioBibleId: 'linked-audio', - name: 'DBL', chapterId: 'MRK.14', - resourceUrl: 'https://example.com/dbl.mp3', expiresAt: null, + name: 'DBL', + resourceUrl: 'https://example.com/dbl.mp3', }, ]) ); const result = await getChapterSourceAudio({ - fluentBibleId: 1, - languageCode: 'eng', bookCode: 'MRK', chapter: 14, + fluentBibleId: 1, + languageCode: 'eng', }); expect(result).toMatchObject({ ok: true, @@ -561,72 +503,4 @@ describe('legacy source and manifest isolation from explicit playback selections expect(result.data).not.toHaveProperty('verseAddressable'); } }); - it('manifest still name-matches and retains download metadata while omitting unknown sizes', async () => { - vi.mocked(getBibles).mockResolvedValue( - ok([{ id: 11, name: 'Berean Standard Bible', abbreviation: 'BSB', hasAudio: true }]) - ); - vi.mocked(getBibleText).mockResolvedValue( - ok({ - bibleId: 11, - bibleName: 'Berean Standard Bible', - bibleAbbreviation: 'BSB', - bookName: 'Mark', - bookCode: 'MRK', - chapters: [ - { - number: 14, - audio: { - mp3: { url: 'https://example.com/a.mp3', size: 99 }, - webm: { url: 'https://example.com/a.webm' }, - }, - verses: [], - }, - ], - }) - ); - expect( - await getSourceAudioManifest({ - projectId: 10, - fluentBibleId: 1, - languageCode: 'eng', - bookCode: 'MRK', - startChapter: 14, - endChapter: 14, - }) - ).toEqual( - ok({ - projectId: 10, - sourceLanguageCode: 'eng', - provider: 'aquifer', - totalBytes: 99, - items: [ - { - id: 'source-audio-11-MRK-14-mp3', - tier: 1, - kind: 'audio', - resourceName: 'Source Bible Audio', - label: 'BSB MRK 14 (mp3)', - required: true, - removable: false, - bytesTotal: 99, - sourceUrl: 'https://example.com/a.mp3', - fileExt: 'mp3', - languageCode: 'eng', - bookCode: 'MRK', - startChapter: 14, - endChapter: 14, - format: 'mp3', - aquiferBibleId: 11, - }, - ], - }) - ); - expect(getBibleText).toHaveBeenCalledWith({ - aquiferBibleId: 11, - bookCode: 'MRK', - startChapter: 14, - endChapter: 14, - includeAudio: true, - }); - }); }); diff --git a/src/domains/translation-resources/translation-resources.license.test.ts b/src/domains/translation-resources/translation-resources.license.test.ts index 736b08ef..4e153dd8 100644 --- a/src/domains/translation-resources/translation-resources.license.test.ts +++ b/src/domains/translation-resources/translation-resources.license.test.ts @@ -6,12 +6,14 @@ import { ok } from '@/lib/types'; import { getPrepareOfflineManifest, + getTranslationImages, getTranslationNotes, getTranslationQuestions, } from './translation-resources.service'; -import { searchEnumeration } from './translation-resources.test-fixtures'; +import { imageSearchHit, searchEnumeration } from './translation-resources.test-fixtures'; import { prepareOfflineManifestResponseSchema, + translationImagesResponseSchema, translationNotesResponseSchema, translationQuestionsResponseSchema, } from './translation-resources.types'; @@ -76,6 +78,113 @@ describe('resource license attribution', () => { } ); + it('carries licenseInfo through images — CC BY-SA attribution obligation; do not drop', async () => { + const imageHit = imageSearchHit(hit.id); + // Mock Aquifer image details (not a live payload). + const imageDetail = aquiferResourceDetailsSchema.parse({ + ...detail, + mediaType: 'Image', + content: { url: 'https://cdn.example/mock-image.jpg', size: 42 }, + grouping: { + type: 'Images', + name: 'Images', + mediaType: 'Image', + licenseInfo, + collectionCode: 'Images', + }, + }); + vi.mocked(searchAllResources).mockResolvedValue(searchEnumeration([imageHit])); + vi.mocked(getResource).mockResolvedValue(ok(imageDetail)); + + const result = await getTranslationImages({ + bookCode: 'GEN', + chapter: 3, + verse: 1, + languageCode: 'eng', + }); + expect(result.ok).toBe(true); + if (!result.ok) throw new Error('Image hydration failed'); + expect(translationImagesResponseSchema.parse(result.data).items[0]?.licenseInfo).toEqual( + licenseInfo + ); + }); + + it('passes through mock caption and attribution on images when Aquifer content has them (#273 / mobile #594)', async () => { + const imageHit = imageSearchHit(hit.id); + // Mock Aquifer image details only — not live payload proof. + const imageDetail = aquiferResourceDetailsSchema.parse({ + ...detail, + content: { + url: 'https://cdn.example/mock-image.jpg', + thumbnailUrl: 'https://cdn.example/mock-thumb.jpg', + caption: 'Mock map caption', + attribution: 'Mock explicit credit', + size: 11, + }, + grouping: { + type: 'Images', + name: 'Images', + mediaType: 'Image', + licenseInfo, + collectionCode: 'Images', + }, + }); + vi.mocked(searchAllResources).mockResolvedValue(searchEnumeration([imageHit])); + vi.mocked(getResource).mockResolvedValue(ok(imageDetail)); + + const result = await getTranslationImages({ + bookCode: 'GEN', + chapter: 3, + verse: 1, + languageCode: 'eng', + }); + expect(result.ok).toBe(true); + if (!result.ok) throw new Error('Image hydration failed'); + const item = translationImagesResponseSchema.parse(result.data).items[0]; + expect(item).toMatchObject({ + url: 'https://cdn.example/mock-image.jpg', + thumbnailUrl: 'https://cdn.example/mock-thumb.jpg', + caption: 'Mock map caption', + attribution: 'Mock explicit credit', + licenseInfo, + size: 11, + }); + }); + + it('omits caption, attribution, and licenseInfo on images when Aquifer mock omits them (#273 / mobile #594)', async () => { + const imageHit = imageSearchHit(hit.id); + const imageDetail = aquiferResourceDetailsSchema.parse({ + ...detail, + content: { url: 'https://cdn.example/mock-image.jpg', size: 3 }, + grouping: { + type: 'Images', + name: 'Images', + mediaType: 'Image', + collectionCode: 'Images', + }, + }); + vi.mocked(searchAllResources).mockResolvedValue(searchEnumeration([imageHit])); + vi.mocked(getResource).mockResolvedValue(ok(imageDetail)); + + const result = await getTranslationImages({ + bookCode: 'GEN', + chapter: 3, + verse: 1, + languageCode: 'eng', + }); + expect(result.ok).toBe(true); + if (!result.ok) throw new Error('Image hydration failed'); + const item = translationImagesResponseSchema.parse(result.data).items[0]; + expect(item).toMatchObject({ + url: 'https://cdn.example/mock-image.jpg', + size: 3, + }); + expect(item).not.toHaveProperty('caption'); + expect(item).not.toHaveProperty('attribution'); + expect(item).not.toHaveProperty('licenseInfo'); + expect(item).not.toHaveProperty('thumbnailUrl'); + }); + it.each([true, false])( 'carries licenseInfo through the manifest (includeContent=%s) — CC BY-SA attribution obligation; do not drop', async (includeContent) => { diff --git a/src/domains/translation-resources/translation-resources.manifest-completeness.test.ts b/src/domains/translation-resources/translation-resources.manifest-completeness.test.ts new file mode 100644 index 00000000..70b6fe7e --- /dev/null +++ b/src/domains/translation-resources/translation-resources.manifest-completeness.test.ts @@ -0,0 +1,258 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { logger } from '@/lib/logger'; +import * as aquiferClient from '@/lib/services/aquifer/aquifer.client'; +import { err, ErrorCode, ok } from '@/lib/types'; +import { server } from '@/server/server'; +import '@/domains/translation-resources/translation-resources.route'; + +import { + asAuthenticatedUser, + asProjectMember, + MANIFEST_PATH, + searchEnumeration, + searchHit, + textDetails, +} from './translation-resources.test-fixtures'; + +vi.mock('@/lib/auth', () => ({ + auth: { + api: { getSession: vi.fn() }, + handler: vi.fn(), + }, +})); + +vi.mock('@/db', () => { + const mockQueryBuilder = { + from: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + limit: vi.fn().mockResolvedValue([{ activeOrgId: 1 }]), + }; + return { + db: { select: vi.fn(() => mockQueryBuilder), insert: vi.fn(), update: vi.fn() }, + }; +}); + +vi.mock('@/lib/logger', () => ({ + logger: { info: vi.fn(), error: vi.fn(), debug: vi.fn(), warn: vi.fn() }, +})); + +vi.mock('@/domains/users/users.service', () => ({ + getUserByEmail: vi.fn(), +})); + +vi.mock('@/domains/user-roles/user-roles.repository', () => ({ + findGrantsByUserId: vi.fn(), +})); + +vi.mock('@/domains/projects/projects.service', () => ({ + getProjectById: vi.fn(), +})); + +vi.mock('@/domains/projects/users/project-users.service', () => ({ + resolveIsProjectMember: vi.fn(), +})); + +vi.mock('@/lib/services/aquifer/aquifer.client', () => ({ + searchResources: vi.fn(), + searchAllResources: vi.fn(), + getResource: vi.fn(), +})); + +describe('translation-resources manifest completeness', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('keeps every required resource when one chapter has more than 100 notes (#431)', async () => { + asAuthenticatedUser(); + asProjectMember(); + const hits = Array.from({ length: 101 }, (_, index) => ({ + ...searchHit, + id: index + 1, + })); + vi.mocked(aquiferClient.searchAllResources) + .mockResolvedValueOnce(searchEnumeration(hits)) + .mockResolvedValue(searchEnumeration([])); + vi.mocked(aquiferClient.getResource).mockImplementation(async (id) => + ok({ ...textDetails, id }) + ); + + const res = await server.request(MANIFEST_PATH, { method: 'GET' }); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.truncated).toBe(false); + expect(body.items).toHaveLength(101); + expect(body.items.map((item: { aquiferContentId: number }) => item.aquiferContentId)).toEqual( + hits.map((hit) => hit.id) + ); + expect(body.totalBytes).toBe( + body.items.reduce((sum: number, item: { bytesTotal: number }) => sum + item.bytesTotal, 0) + ); + expect(body.totalBytes).toBeGreaterThan(0); + expect(aquiferClient.getResource).toHaveBeenCalledTimes(101); + }); + + it('sets truncated and warns when Aquifer search enumeration is incomplete (#431)', async () => { + asAuthenticatedUser(); + asProjectMember(); + const hits = Array.from({ length: 50 }, (_, index) => ({ + ...searchHit, + id: index + 1, + })); + vi.mocked(aquiferClient.searchAllResources) + .mockResolvedValueOnce(searchEnumeration(hits, false)) + .mockResolvedValue(searchEnumeration([])); + vi.mocked(aquiferClient.getResource).mockImplementation(async (id) => + ok({ ...textDetails, id }) + ); + + const res = await server.request(MANIFEST_PATH, { method: 'GET' }); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.truncated).toBe(true); + expect(body.items).toHaveLength(50); + expect(logger.warn).toHaveBeenCalledWith({ + message: 'Aquifer prepare-offline search hit page ceiling; marking manifest truncated', + context: expect.objectContaining({ + resourceCollectionCode: 'UWTranslationNotes', + bookCode: 'MRK', + startChapter: 14, + endChapter: 14, + collectedCount: 50, + }), + }); + }); + + it('returns 502 when one manifest resource fails to hydrate (#432)', async () => { + asAuthenticatedUser(); + asProjectMember(); + const hits = Array.from({ length: 100 }, (_, index) => ({ + ...searchHit, + id: index + 1, + })); + vi.mocked(aquiferClient.searchAllResources) + .mockResolvedValueOnce(searchEnumeration(hits)) + .mockResolvedValue(searchEnumeration([])); + vi.mocked(aquiferClient.getResource).mockImplementation(async (id) => + id === 100 ? err(ErrorCode.AQUIFER_SERVICE_UNAVAILABLE) : ok({ ...textDetails, id }) + ); + + const res = await server.request(MANIFEST_PATH, { method: 'GET' }); + + expect(res.status).toBe(502); + expect(await res.json()).toEqual({ message: 'Aquifer service is unavailable' }); + expect(aquiferClient.getResource).toHaveBeenCalledTimes(100); + }); + it('returns empty manifest when Aquifer has no resources', async () => { + asAuthenticatedUser(); + asProjectMember(); + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration([])); + + const res = await server.request(MANIFEST_PATH, { method: 'GET' }); + + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ + projectId: 10, + sourceLanguageCode: 'eng', + items: [], + totalBytes: 0, + truncated: false, + }); + // TN, TW, TQ, StudyNotes, Images + expect(aquiferClient.searchAllResources).toHaveBeenCalledTimes(5); + }); + + it('returns hydrated manifest items without serializedContent by default', async () => { + asAuthenticatedUser(); + asProjectMember(); + vi.mocked(aquiferClient.searchAllResources) + .mockResolvedValueOnce(searchEnumeration([searchHit])) + .mockResolvedValue(searchEnumeration([])); + vi.mocked(aquiferClient.getResource).mockResolvedValue(ok(textDetails)); + + const res = await server.request(MANIFEST_PATH, { method: 'GET' }); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.projectId).toBe(10); + expect(body.truncated).toBe(false); + expect(body.items).toHaveLength(1); + expect(body.items[0]).toMatchObject({ + id: 'aquifer-101-text', + tier: 2, + kind: 'text', + resourceName: 'Translation Notes', + aquiferContentId: 101, + bookCode: 'MRK', + startChapter: 14, + endChapter: 14, + collectionCode: 'UWTranslationNotes', + fileExt: 'json', + }); + expect(body.items[0].bytesTotal).toBeGreaterThan(0); + expect(body.totalBytes).toBe(body.items[0].bytesTotal); + expect(body.items[0].serializedContent).toBeUndefined(); + }); + + it('includes serializedContent when includeContent=true', async () => { + asAuthenticatedUser(); + asProjectMember(); + vi.mocked(aquiferClient.searchAllResources) + .mockResolvedValueOnce(searchEnumeration([searchHit])) + .mockResolvedValue(searchEnumeration([])); + vi.mocked(aquiferClient.getResource).mockResolvedValue(ok(textDetails)); + + const res = await server.request(`${MANIFEST_PATH}&includeContent=true`, { method: 'GET' }); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(typeof body.items[0].serializedContent).toBe('string'); + const parsedContent = JSON.parse(body.items[0].serializedContent); + expect(parsedContent).toEqual(textDetails.content); + expect(body.items[0].bytesTotal).toBe( + new TextEncoder().encode(body.items[0].serializedContent).byteLength + ); + }); + + it('returns 502 when Aquifer fails mid-manifest', async () => { + asAuthenticatedUser(); + asProjectMember(); + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue( + err(ErrorCode.AQUIFER_SERVICE_UNAVAILABLE) + ); + + const res = await server.request(MANIFEST_PATH, { method: 'GET' }); + + expect(res.status).toBe(502); + expect(await res.json()).toEqual({ message: 'Aquifer service is unavailable' }); + }); + + it('returns 400 when endChapter is before startChapter', async () => { + asAuthenticatedUser(); + asProjectMember(); + + const res = await server.request( + '/projects/10/translation-resources/manifest?languageCode=eng&bookCode=MRK&startChapter=150&endChapter=1', + { method: 'GET' } + ); + + expect(res.status).toBe(400); + expect(aquiferClient.searchAllResources).not.toHaveBeenCalled(); + }); + + it('returns 400 when the chapter span exceeds the cap', async () => { + asAuthenticatedUser(); + asProjectMember(); + + const res = await server.request( + '/projects/10/translation-resources/manifest?languageCode=eng&bookCode=PSA&startChapter=1&endChapter=150', + { method: 'GET' } + ); + + expect(res.status).toBe(400); + expect(aquiferClient.searchAllResources).not.toHaveBeenCalled(); + }); +}); diff --git a/src/domains/translation-resources/translation-resources.range.test.ts b/src/domains/translation-resources/translation-resources.range.test.ts index a753ff4f..71abc561 100644 --- a/src/domains/translation-resources/translation-resources.range.test.ts +++ b/src/domains/translation-resources/translation-resources.range.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { auth } from '@/lib/auth'; +import { logger } from '@/lib/logger'; import * as aquiferClient from '@/lib/services/aquifer/aquifer.client'; import { err, ErrorCode, ok } from '@/lib/types'; import { server } from '@/server/server'; @@ -113,6 +114,30 @@ describe('translation-resources verse-range routes', () => { expect(aquiferClient.searchAllResources).toHaveBeenCalledTimes(1); }); + it('warns when Aquifer range search is incomplete and still returns collected items', async () => { + asAuthenticatedUser(); + asProjectMember(); + const hits = [{ ...searchHit, id: 101 }]; + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration(hits, false)); + vi.mocked(aquiferClient.getResource).mockResolvedValue(ok(textDetails)); + + const res = await server.request(NOTES_RANGE_PATH, { method: 'GET' }); + + expect(res.status).toBe(200); + expect((await res.json()).items).toHaveLength(1); + expect(logger.warn).toHaveBeenCalledWith({ + message: 'Aquifer verse-range search hit page ceiling; returning partial items', + context: expect.objectContaining({ + resourceCollectionCode: 'UWTranslationNotes', + bookCode: 'MRK', + chapter: 1, + startVerse: 1, + endVerse: 8, + collectedCount: 1, + }), + }); + }); + it('returns 502 with a generic message when Aquifer search fails', async () => { asAuthenticatedUser(); asProjectMember(); diff --git a/src/domains/translation-resources/translation-resources.service.ts b/src/domains/translation-resources/translation-resources.service.ts index 39f7957f..378edbc7 100644 --- a/src/domains/translation-resources/translation-resources.service.ts +++ b/src/domains/translation-resources/translation-resources.service.ts @@ -6,6 +6,7 @@ import type { } from '@/lib/services/aquifer/aquifer.types'; import type { Result } from '@/lib/types'; +import { logger } from '@/lib/logger'; import { getResource, searchAllResources } from '@/lib/services/aquifer/aquifer.client'; import { ok } from '@/lib/types'; @@ -118,6 +119,8 @@ interface ExtractedAquiferAsset { url: string; size?: number; thumbnailUrl?: string; + caption?: string; + attribution?: string; } function ownString(record: Record, fieldName: string): string | undefined { @@ -125,19 +128,20 @@ function ownString(record: Record, fieldName: string): string | return typeof value === 'string' && value.length > 0 ? value : undefined; } -/** - * Read url/href, size, and thumbnailUrl from a single Aquifer asset record. - * Nested sibling records must not contribute mixed fields. - */ +/** Own-record url/href + optional media metadata; do not mix nested siblings. */ function readAssetFromRecord(record: Record): ExtractedAquiferAsset | undefined { const url = ownString(record, 'url') ?? ownString(record, 'href'); if (!url) return undefined; const size = typeof record.size === 'number' ? record.size : undefined; const thumbnailUrl = ownString(record, 'thumbnailUrl'); + const caption = ownString(record, 'caption'); + const attribution = ownString(record, 'attribution'); return { url, ...(size !== undefined ? { size } : {}), ...(thumbnailUrl ? { thumbnailUrl } : {}), + ...(caption ? { caption } : {}), + ...(attribution ? { attribution } : {}), }; } @@ -190,12 +194,14 @@ function mediaFallbackExt(mediaType: AquiferResourceMediaType): string { * Map items with a fixed concurrency pool. Individual mapper failures are * skipped so one bad Aquifer content id cannot drop the rest of the section. * If every item fails, the first failure is returned so the route can 502 - * instead of looking like an empty collection. + * instead of looking like an empty collection. Offline manifests require every + * item, so their mapper opts into returning the first failure even after successes. */ async function mapWithConcurrency( items: T[], concurrency: number, - mapper: (item: T) => Promise> + mapper: (item: T) => Promise>, + failurePolicy: 'skip' | 'fail' = 'skip' ): Promise> { if (items.length === 0) return ok([]); @@ -222,7 +228,7 @@ async function mapWithConcurrency( else firstFailure ??= slot; } - if (successes.length === 0 && firstFailure) return firstFailure; + if (firstFailure && (failurePolicy === 'fail' || successes.length === 0)) return firstFailure; return ok(successes); } @@ -278,6 +284,20 @@ async function searchVerseRangeCollection( resourceCollectionCode, }); if (!search.ok) return search; + if (!search.data.complete) { + logger.warn({ + message: 'Aquifer verse-range search hit page ceiling; returning partial items', + context: { + resourceCollectionCode, + bookCode: ref.bookCode, + chapter: ref.chapter, + startVerse: ref.startVerse, + endVerse: ref.endVerse, + languageCode: ref.languageCode, + collectedCount: search.data.items.length, + }, + }); + } return ok(dedupeSearchHitsById(search.data.items)); } @@ -333,6 +353,20 @@ export async function getTranslationImagesForRange( resourceType: 'Images', }); if (!search.ok) return search; + if (!search.data.complete) { + logger.warn({ + message: 'Aquifer images verse-range search hit page ceiling; returning partial items', + context: { + resourceType: 'Images', + bookCode: ref.bookCode, + chapter: ref.chapter, + startVerse: ref.startVerse, + endVerse: ref.endVerse, + languageCode: ref.languageCode, + collectedCount: search.data.items.length, + }, + }); + } const uniqueHits = dedupeSearchHitsById(search.data.items); const hydrated = await mapWithConcurrency(uniqueHits, HYDRATE_CONCURRENCY, async (hit) => { @@ -352,7 +386,12 @@ export async function getTranslationImagesForRange( localizedName: hit.localizedName || hit.name, url: asset.url, ...(asset.thumbnailUrl ? { thumbnailUrl: asset.thumbnailUrl } : {}), + ...(asset.caption ? { caption: asset.caption } : {}), + ...(asset.attribution ? { attribution: asset.attribution } : {}), size: asset.size, + ...(details.grouping.licenseInfo !== undefined + ? { licenseInfo: details.grouping.licenseInfo } + : {}), }); } return ok({ items }); @@ -462,6 +501,19 @@ export async function getPrepareOfflineManifest(params: { // ceiling). Do not re-slice a successful enumeration — mobile rejects truncated. if (!search.data.complete) { truncated = true; + logger.warn({ + message: 'Aquifer prepare-offline search hit page ceiling; marking manifest truncated', + context: { + resourceCollectionCode: config.collectionCode, + resourceType: config.resourceType, + resourceName: config.resourceName, + bookCode, + startChapter, + endChapter, + languageCode, + collectedCount: search.data.items.length, + }, + }); } const hydrated = await mapWithConcurrency( @@ -471,7 +523,8 @@ export async function getPrepareOfflineManifest(params: { const details = await getResource(searchItem.id); if (!details.ok) return details; return ok({ searchItem, details: details.data }); - } + }, + 'fail' ); if (!hydrated.ok) return hydrated; diff --git a/src/domains/translation-resources/translation-resources.test.ts b/src/domains/translation-resources/translation-resources.test.ts index 57f1615a..42e2558f 100644 --- a/src/domains/translation-resources/translation-resources.test.ts +++ b/src/domains/translation-resources/translation-resources.test.ts @@ -13,7 +13,6 @@ import { asProjectMember, IMAGES_PATH, imageSearchHit, - MANIFEST_PATH, MOCK_PROJECT, NOTES_PATH, QUESTIONS_PATH, @@ -410,168 +409,4 @@ describe('translation-resources routes', () => { expect(await res.json()).toEqual({ items: [] }); }); }); - - describe('gET manifest', () => { - it('keeps every required resource when one chapter has more than 100 notes (#431)', async () => { - asAuthenticatedUser(); - asProjectMember(); - const hits = Array.from({ length: 101 }, (_, index) => ({ - ...searchHit, - id: index + 1, - })); - vi.mocked(aquiferClient.searchAllResources) - .mockResolvedValueOnce(searchEnumeration(hits)) - .mockResolvedValue(searchEnumeration([])); - vi.mocked(aquiferClient.getResource).mockImplementation(async (id) => - ok({ ...textDetails, id }) - ); - - const res = await server.request(MANIFEST_PATH, { method: 'GET' }); - - expect(res.status).toBe(200); - const body = await res.json(); - expect(body.truncated).toBe(false); - expect(body.items).toHaveLength(101); - expect(body.items.map((item: { aquiferContentId: number }) => item.aquiferContentId)).toEqual( - hits.map((hit) => hit.id) - ); - expect(body.totalBytes).toBe( - body.items.reduce((sum: number, item: { bytesTotal: number }) => sum + item.bytesTotal, 0) - ); - expect(body.totalBytes).toBeGreaterThan(0); - expect(aquiferClient.getResource).toHaveBeenCalledTimes(101); - }); - - it('sets truncated when Aquifer search enumeration is incomplete (#431)', async () => { - asAuthenticatedUser(); - asProjectMember(); - const hits = Array.from({ length: 50 }, (_, index) => ({ - ...searchHit, - id: index + 1, - })); - vi.mocked(aquiferClient.searchAllResources) - .mockResolvedValueOnce(searchEnumeration(hits, false)) - .mockResolvedValue(searchEnumeration([])); - vi.mocked(aquiferClient.getResource).mockImplementation(async (id) => - ok({ ...textDetails, id }) - ); - - const res = await server.request(MANIFEST_PATH, { method: 'GET' }); - - expect(res.status).toBe(200); - const body = await res.json(); - expect(body.truncated).toBe(true); - expect(body.items).toHaveLength(50); - }); - - it('returns empty manifest when Aquifer has no resources', async () => { - asAuthenticatedUser(); - asProjectMember(); - vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration([])); - - const res = await server.request(MANIFEST_PATH, { method: 'GET' }); - - expect(res.status).toBe(200); - expect(await res.json()).toEqual({ - projectId: 10, - sourceLanguageCode: 'eng', - items: [], - totalBytes: 0, - truncated: false, - }); - // TN, TW, TQ, StudyNotes, Images - expect(aquiferClient.searchAllResources).toHaveBeenCalledTimes(5); - }); - - it('returns hydrated manifest items without serializedContent by default', async () => { - asAuthenticatedUser(); - asProjectMember(); - vi.mocked(aquiferClient.searchAllResources) - .mockResolvedValueOnce(searchEnumeration([searchHit])) - .mockResolvedValue(searchEnumeration([])); - vi.mocked(aquiferClient.getResource).mockResolvedValue(ok(textDetails)); - - const res = await server.request(MANIFEST_PATH, { method: 'GET' }); - - expect(res.status).toBe(200); - const body = await res.json(); - expect(body.projectId).toBe(10); - expect(body.truncated).toBe(false); - expect(body.items).toHaveLength(1); - expect(body.items[0]).toMatchObject({ - id: 'aquifer-101-text', - tier: 2, - kind: 'text', - resourceName: 'Translation Notes', - aquiferContentId: 101, - bookCode: 'MRK', - startChapter: 14, - endChapter: 14, - collectionCode: 'UWTranslationNotes', - fileExt: 'json', - }); - expect(body.items[0].bytesTotal).toBeGreaterThan(0); - expect(body.totalBytes).toBe(body.items[0].bytesTotal); - expect(body.items[0].serializedContent).toBeUndefined(); - }); - - it('includes serializedContent when includeContent=true', async () => { - asAuthenticatedUser(); - asProjectMember(); - vi.mocked(aquiferClient.searchAllResources) - .mockResolvedValueOnce(searchEnumeration([searchHit])) - .mockResolvedValue(searchEnumeration([])); - vi.mocked(aquiferClient.getResource).mockResolvedValue(ok(textDetails)); - - const res = await server.request(`${MANIFEST_PATH}&includeContent=true`, { method: 'GET' }); - - expect(res.status).toBe(200); - const body = await res.json(); - expect(typeof body.items[0].serializedContent).toBe('string'); - const parsedContent = JSON.parse(body.items[0].serializedContent); - expect(parsedContent).toEqual(textDetails.content); - expect(body.items[0].bytesTotal).toBe( - new TextEncoder().encode(body.items[0].serializedContent).byteLength - ); - }); - - it('returns 502 when Aquifer fails mid-manifest', async () => { - asAuthenticatedUser(); - asProjectMember(); - vi.mocked(aquiferClient.searchAllResources).mockResolvedValue( - err(ErrorCode.AQUIFER_SERVICE_UNAVAILABLE) - ); - - const res = await server.request(MANIFEST_PATH, { method: 'GET' }); - - expect(res.status).toBe(502); - expect(await res.json()).toEqual({ message: 'Aquifer service is unavailable' }); - }); - - it('returns 400 when endChapter is before startChapter', async () => { - asAuthenticatedUser(); - asProjectMember(); - - const res = await server.request( - '/projects/10/translation-resources/manifest?languageCode=eng&bookCode=MRK&startChapter=150&endChapter=1', - { method: 'GET' } - ); - - expect(res.status).toBe(400); - expect(aquiferClient.searchAllResources).not.toHaveBeenCalled(); - }); - - it('returns 400 when the chapter span exceeds the cap', async () => { - asAuthenticatedUser(); - asProjectMember(); - - const res = await server.request( - '/projects/10/translation-resources/manifest?languageCode=eng&bookCode=PSA&startChapter=1&endChapter=150', - { method: 'GET' } - ); - - expect(res.status).toBe(400); - expect(aquiferClient.searchAllResources).not.toHaveBeenCalled(); - }); - }); }); diff --git a/src/domains/translation-resources/translation-resources.types.ts b/src/domains/translation-resources/translation-resources.types.ts index 05774c6b..97c97044 100644 --- a/src/domains/translation-resources/translation-resources.types.ts +++ b/src/domains/translation-resources/translation-resources.types.ts @@ -262,8 +262,22 @@ export const translationImageItemSchema = z title: z.string(), localizedName: z.string(), url: z.string(), - thumbnailUrl: z.string().optional(), + thumbnailUrl: z + .string() + .optional() + .openapi({ description: 'Optional Aquifer content.thumbnailUrl when present (non-empty)' }), + caption: z.string().optional().openapi({ + description: + 'Optional Aquifer content.caption string when present; omitted when absent (no fabricated caption)', + }), + attribution: z.string().optional().openapi({ + description: + 'Optional Aquifer content.attribution string when present; clients may also derive credit from licenseInfo', + }), size: z.number().int().nonnegative().optional(), + licenseInfo: aquiferLicenseInfoWireSchema.optional().openapi({ + description: 'Aquifer grouping.licenseInfo when present (same wire as TN/TQ)', + }), }) .openapi('TranslationImageItem'); @@ -314,7 +328,7 @@ export const prepareOfflineManifestResponseSchema = z totalBytes: z.number().int().nonnegative(), truncated: z.boolean().openapi({ description: - 'True when one or more resource configs could not be fully enumerated within the server bounded Aquifer search page budget (provider total / short-page proof missing). False when every matching resource was collected and hydrated. Clients must not treat a truncated catalogue as complete.', + 'True when one or more resource configs could not be fully enumerated within the server bounded Aquifer search page budget (provider total / short-page proof missing). False when every matching resource was collected; hydrate failures may still drop items (or 502 on the manifest fail-closed path) and are not covered by this flag. Clients must not treat a truncated catalogue as complete.', }), }) .openapi('PrepareOfflineResourceManifest'); diff --git a/src/domains/verse-audio/verse-audio.concurrency.service.test.ts b/src/domains/verse-audio/verse-audio.concurrency.service.test.ts new file mode 100644 index 00000000..4af89e11 --- /dev/null +++ b/src/domains/verse-audio/verse-audio.concurrency.service.test.ts @@ -0,0 +1,449 @@ +import { Buffer } from 'node:buffer'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { uploadVerseAudio } from '@/lib/audio-storage'; +import { err, ErrorCode, ok } from '@/lib/types'; + +import * as storageRepo from './storage-objects.repository'; +import * as repo from './verse-audio.repository'; +import { uploadRecording } from './verse-audio.service'; +import { hashOf, record, take, uploadInput } from './verse-audio.test-fixtures'; +import { VERSE_AUDIO_CONFLICT_STATUS } from './verse-audio.types'; + +vi.mock('@/lib/audio-storage', () => ({ + audioBlobName: vi.fn( + (unitId: number, textId: number, hash: string) => `unit-${unitId}/text-${textId}/${hash}` + ), + audioBucket: vi.fn(() => 'verse-audio'), + deleteVerseAudio: vi.fn(async () => {}), + generateAudioDownloadUrl: vi.fn(async (key: string) => `https://r2.example/${key}?sig=x`), + uploadVerseAudio: vi.fn(async () => {}), +})); + +vi.mock('./storage-objects.repository', () => ({ + claim: vi.fn(), + findOrphans: vi.fn(), + getById: vi.fn(), + getByIds: vi.fn(), + markDeleted: vi.fn(), + reclaimOrphanIfUnreferenced: vi.fn(), +})); + +vi.mock('./verse-audio.repository', () => ({ + createFirstRecordingWithTake: vi.fn(), + findTakeByContentHash: vi.fn(), + get: vi.fn(), + getTakeById: vi.fn(), + insertTake: vi.fn(), + listByChapter: vi.fn(), + listTakesByRecordingIds: vi.fn(), + listTakesForRecording: vi.fn(), + markConflictPreservingActive: vi.fn(), + pruneSupersededTakes: vi.fn(), + remove: vi.fn(), + updateRecordingStateIfVersion: vi.fn(), +})); + +vi.mock('@/lib/logger', () => ({ + logger: { debug: vi.fn(), error: vi.fn(), info: vi.fn(), warn: vi.fn() }, +})); + +describe('verse-audio service', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(storageRepo.claim).mockResolvedValue( + ok({ + bucket: 'verse-audio', + createdAt: new Date(), + deletedAt: null, + id: 55, + key: `unit-12/text-3401/${take.contentHash}`, + }) + ); + vi.mocked(storageRepo.getById).mockResolvedValue( + ok({ + bucket: 'verse-audio', + createdAt: new Date(), + deletedAt: null, + id: 55, + key: `unit-12/text-3401/${take.contentHash}`, + }) + ); + vi.mocked(storageRepo.getByIds).mockResolvedValue(ok([])); + vi.mocked(storageRepo.markDeleted).mockResolvedValue(ok(undefined)); + vi.mocked(uploadVerseAudio).mockResolvedValue(undefined); + vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([take])); + vi.mocked(repo.listTakesByRecordingIds).mockResolvedValue(ok([take])); + }); + + describe('uploadRecording', () => { + it('does not flag conflict when a concurrent first upload reloads the same active take', async () => { + vi.mocked(repo.get) + .mockResolvedValueOnce(err(ErrorCode.VERSE_AUDIO_NOT_FOUND)) + .mockResolvedValue(ok(record)); + vi.mocked(repo.createFirstRecordingWithTake).mockResolvedValue( + ok({ outcome: 'concurrent', record, take }) + ); + + const result = await uploadRecording(uploadInput); + + expect(result.ok).toBe(true); + expect(repo.markConflictPreservingActive).not.toHaveBeenCalled(); + }); + + it('flags conflict when a concurrent first upload introduces a different take', async () => { + const winnerTake = { ...take, contentHash: hashOf(Buffer.from('other')), id: 11 }; + const winner = { ...record, activeTakeId: 11, versionToken: 2 }; + vi.mocked(repo.get) + .mockResolvedValueOnce(err(ErrorCode.VERSE_AUDIO_NOT_FOUND)) + .mockResolvedValue(ok({ ...winner, conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT })); + vi.mocked(repo.createFirstRecordingWithTake).mockResolvedValue( + ok({ outcome: 'concurrent', record: winner, take }) + ); + vi.mocked(repo.markConflictPreservingActive).mockResolvedValue( + ok({ ...winner, conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT }) + ); + vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([take, winnerTake])); + + const result = await uploadRecording(uploadInput); + + expect(repo.markConflictPreservingActive).toHaveBeenCalledWith(winner.id); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.conflictStatus).toBe('conflict'); + } + }); + + it('suppresses false conflicts when content hash already exists on the active take', async () => { + vi.mocked(repo.get).mockResolvedValue(ok(record)); + vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(take)); + + const result = await uploadRecording({ ...uploadInput, baseVersionToken: 2 }); + + expect(result.ok).toBe(true); + expect(repo.insertTake).not.toHaveBeenCalled(); + expect(uploadVerseAudio).not.toHaveBeenCalled(); + expect(repo.updateRecordingStateIfVersion).not.toHaveBeenCalled(); + }); + + it('preserves conflict when a legacy client replaces the active take', async () => { + const conflicted = { + ...record, + conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT, + }; + const newData = Buffer.from('legacy-over-conflict'); + const newHash = hashOf(newData); + const newTake = { ...take, contentHash: newHash, id: 11, storageObjectId: 56 }; + + vi.mocked(repo.get) + .mockResolvedValueOnce(ok(conflicted)) + .mockResolvedValue( + ok({ + ...conflicted, + activeTakeId: 11, + conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT, + storageObjectId: 56, + versionToken: 2, + }) + ); + vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(null)); + vi.mocked(storageRepo.claim).mockResolvedValue( + ok({ + bucket: 'verse-audio', + createdAt: new Date(), + deletedAt: null, + id: 56, + key: `unit-12/text-3401/${newHash}`, + }) + ); + vi.mocked(repo.insertTake).mockResolvedValue(ok(newTake)); + vi.mocked(repo.updateRecordingStateIfVersion).mockResolvedValue( + ok({ + applied: true, + record: { + ...conflicted, + activeTakeId: 11, + conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT, + versionToken: 2, + }, + }) + ); + vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([take, newTake])); + + const result = await uploadRecording({ + ...uploadInput, + data: newData, + }); + + expect(repo.updateRecordingStateIfVersion).toHaveBeenCalledWith( + 1, + 1, + expect.objectContaining({ + activeTakeId: 11, + versionToken: 2, + }) + ); + expect(vi.mocked(repo.updateRecordingStateIfVersion).mock.calls[0]![2]).not.toHaveProperty( + 'conflictStatus' + ); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.conflictStatus).toBe('conflict'); + expect(result.data.activeTakeId).toBe(11); + } + }); + + it('does not reopen a resolved conflict when a stale base re-submits a known non-active take', async () => { + const prior = { ...take, id: 10 }; + const active = { + ...take, + contentHash: hashOf(Buffer.from('newer')), + id: 11, + storageObjectId: 56, + }; + const resolved = { + ...record, + activeTakeId: 11, + conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CLEAN, + storageObjectId: 56, + versionToken: 4, + }; + + vi.mocked(repo.get).mockResolvedValue(ok(resolved)); + vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(prior)); + vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([prior, active])); + + const result = await uploadRecording({ + ...uploadInput, + baseVersionToken: 1, + data: Buffer.from('abcd'), + }); + + expect(repo.updateRecordingStateIfVersion).not.toHaveBeenCalled(); + expect(repo.markConflictPreservingActive).not.toHaveBeenCalled(); + expect(result).toEqual( + expect.objectContaining({ + data: expect.objectContaining({ conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CLEAN }), + ok: true, + }) + ); + }); + + it('does not let a losing client clear its own conflict by retrying the same bytes', async () => { + // The stale-base response hands the loser the current token. Re-sending the + // same bytes may promote its take, but must not settle the contest for it. + const losing = { ...take, id: 10 }; + const active = { ...take, contentHash: hashOf(Buffer.from('newer')), id: 11 }; + const conflicted = { + ...record, + activeTakeId: 11, + conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT, + versionToken: 3, + }; + + vi.mocked(repo.get) + .mockResolvedValueOnce(ok(conflicted)) + .mockResolvedValue(ok({ ...conflicted, activeTakeId: 10, versionToken: 4 })); + vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(losing)); + vi.mocked(repo.updateRecordingStateIfVersion).mockResolvedValue( + ok({ applied: true, record: { ...conflicted, activeTakeId: 10, versionToken: 4 } }) + ); + vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([losing, active])); + + const result = await uploadRecording({ ...uploadInput, baseVersionToken: 3 }); + + expect(vi.mocked(repo.updateRecordingStateIfVersion).mock.calls[0]![2]).not.toHaveProperty( + 'conflictStatus' + ); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.conflictStatus).toBe('conflict'); + } + }); + + it('does not reopen a conflict when resolve wins a duplicate-take promotion race', async () => { + const prior = { ...take, id: 10 }; + const active = { + ...take, + contentHash: hashOf(Buffer.from('newer')), + id: 11, + storageObjectId: 56, + }; + const conflicted = { + ...record, + activeTakeId: 11, + conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT, + versionToken: 3, + }; + const resolved = { + ...conflicted, + conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CLEAN, + versionToken: 4, + }; + + vi.mocked(repo.get).mockResolvedValueOnce(ok(conflicted)).mockResolvedValue(ok(resolved)); + vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(prior)); + vi.mocked(repo.updateRecordingStateIfVersion).mockResolvedValue( + ok({ applied: false, record: null }) + ); + vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([prior, active])); + + const result = await uploadRecording({ ...uploadInput, baseVersionToken: 3 }); + + expect(repo.markConflictPreservingActive).not.toHaveBeenCalled(); + expect(result).toEqual( + expect.objectContaining({ + data: expect.objectContaining({ conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CLEAN }), + ok: true, + }) + ); + }); + + it('leaves an open conflict alone when a matching-token upload brings new bytes', async () => { + const newData = Buffer.from('fresh-over-conflict'); + const newHash = hashOf(newData); + const newTake = { ...take, contentHash: newHash, id: 12, storageObjectId: 56 }; + const conflicted = { + ...record, + conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT, + }; + + vi.mocked(repo.get) + .mockResolvedValueOnce(ok(conflicted)) + .mockResolvedValue(ok({ ...conflicted, activeTakeId: 12, versionToken: 2 })); + vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(null)); + vi.mocked(storageRepo.claim).mockResolvedValue( + ok({ + bucket: 'verse-audio', + createdAt: new Date(), + deletedAt: null, + id: 56, + key: `unit-12/text-3401/${newHash}`, + }) + ); + vi.mocked(repo.insertTake).mockResolvedValue(ok(newTake)); + vi.mocked(repo.updateRecordingStateIfVersion).mockResolvedValue( + ok({ applied: true, record: { ...conflicted, activeTakeId: 12, versionToken: 2 } }) + ); + vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([take, newTake])); + + const result = await uploadRecording({ + ...uploadInput, + baseVersionToken: 1, + data: newData, + }); + + expect(vi.mocked(repo.updateRecordingStateIfVersion).mock.calls[0]![2]).not.toHaveProperty( + 'conflictStatus' + ); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.conflictStatus).toBe('conflict'); + } + }); + + it('keeps a new take and marks conflict when the clean-update CAS loses', async () => { + const newData = Buffer.from('concurrent'); + const newHash = hashOf(newData); + const newTake = { ...take, contentHash: newHash, id: 11, storageObjectId: 56 }; + const conflicted = { + ...record, + conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT, + }; + + vi.mocked(repo.get).mockResolvedValueOnce(ok(record)).mockResolvedValue(ok(conflicted)); + vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(null)); + vi.mocked(storageRepo.claim).mockResolvedValue( + ok({ + bucket: 'verse-audio', + createdAt: new Date(), + deletedAt: null, + id: 56, + key: `unit-12/text-3401/${newHash}`, + }) + ); + vi.mocked(repo.insertTake).mockResolvedValue(ok(newTake)); + vi.mocked(repo.updateRecordingStateIfVersion).mockResolvedValue( + ok({ applied: false, record: null }) + ); + vi.mocked(repo.markConflictPreservingActive).mockResolvedValue(ok(conflicted)); + vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([take, newTake])); + + const result = await uploadRecording({ + ...uploadInput, + baseVersionToken: 1, + data: newData, + }); + + expect(repo.insertTake).toHaveBeenCalledWith( + expect.objectContaining({ contentHash: newHash }) + ); + expect(repo.markConflictPreservingActive).toHaveBeenCalledWith(record.id); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.conflictStatus).toBe('conflict'); + expect(result.data.takes).toHaveLength(2); + } + }); + + it('marks conflict when baseVersionToken is stale', async () => { + const newData = Buffer.from('conflict'); + const newHash = hashOf(newData); + const conflictTake = { ...take, contentHash: newHash, id: 11, uploadedBy: 9 }; + + vi.mocked(repo.get) + .mockResolvedValueOnce(ok(record)) + .mockResolvedValue( + ok({ + ...record, + conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT, + versionToken: 2, + }) + ); + vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(null)); + vi.mocked(storageRepo.claim).mockResolvedValue( + ok({ + bucket: 'verse-audio', + createdAt: new Date(), + deletedAt: null, + id: 56, + key: `unit-12/text-3401/${newHash}`, + }) + ); + vi.mocked(repo.insertTake).mockResolvedValue(ok(conflictTake)); + vi.mocked(repo.updateRecordingStateIfVersion).mockResolvedValue( + ok({ + applied: true, + record: { + ...record, + conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT, + versionToken: 2, + }, + }) + ); + vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([take, conflictTake])); + + const result = await uploadRecording({ + ...uploadInput, + baseVersionToken: 2, + data: newData, + uploadedBy: 9, + }); + + expect(repo.updateRecordingStateIfVersion).toHaveBeenCalledWith( + 1, + 1, + expect.objectContaining({ + conflictStatus: 'conflict', + versionToken: 2, + }) + ); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.conflictStatus).toBe('conflict'); + expect(result.data.takes).toHaveLength(2); + expect(result.data.activeTakeId).toBe(10); + } + }); + }); +}); diff --git a/src/domains/verse-audio/verse-audio.reclaim.service.test.ts b/src/domains/verse-audio/verse-audio.reclaim.service.test.ts new file mode 100644 index 00000000..11154522 --- /dev/null +++ b/src/domains/verse-audio/verse-audio.reclaim.service.test.ts @@ -0,0 +1,101 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { deleteVerseAudio } from '@/lib/audio-storage'; +import { err, ErrorCode, ok } from '@/lib/types'; + +import * as storageRepo from './storage-objects.repository'; +import { reclaimOrphanedStorageObjects } from './verse-audio.reclaim.service'; +import * as repo from './verse-audio.repository'; + +vi.mock('@/lib/audio-storage', () => ({ + deleteVerseAudio: vi.fn(async () => {}), +})); + +vi.mock('./storage-objects.repository', () => ({ + findOrphans: vi.fn(), + markDeleted: vi.fn(), + reclaimOrphanIfUnreferenced: vi.fn(), +})); + +vi.mock('./verse-audio.repository', () => ({ + pruneSupersededTakes: vi.fn(), +})); + +vi.mock('@/lib/logger', () => ({ + logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() }, +})); + +describe('verse-audio service', () => { + describe('reclaimOrphanedStorageObjects', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(storageRepo.markDeleted).mockResolvedValue(ok(undefined)); + vi.mocked(storageRepo.reclaimOrphanIfUnreferenced).mockResolvedValue(ok(false)); + vi.mocked(deleteVerseAudio).mockResolvedValue(undefined); + vi.mocked(repo.pruneSupersededTakes).mockResolvedValue(ok(0)); + }); + + const orphan = (id: number, key: string) => ({ + bucket: 'verse-audio', + createdAt: new Date(), + deletedAt: null, + id, + key, + }); + + it('locks and revalidates each candidate before deleting its object', async () => { + const candidates = [orphan(1, 'unit-9/text-1/a'), orphan(2, 'unit-9/text-2/b')]; + vi.mocked(storageRepo.findOrphans).mockResolvedValue(ok(candidates)); + vi.mocked(storageRepo.reclaimOrphanIfUnreferenced).mockImplementation( + async (id, _graceMs, deleteObject) => { + const candidate = candidates.find((item) => item.id === id)!; + await deleteObject(candidate); + return ok(true); + } + ); + + const result = await reclaimOrphanedStorageObjects(); + + expect(storageRepo.findOrphans).toHaveBeenCalledWith(expect.any(Number)); + expect(storageRepo.reclaimOrphanIfUnreferenced).toHaveBeenCalledTimes(2); + expect(deleteVerseAudio).toHaveBeenCalledTimes(2); + expect(result).toEqual(ok(2)); + }); + + it('prunes superseded takes before the orphan sweep, leaving their objects to it', async () => { + vi.mocked(repo.pruneSupersededTakes).mockResolvedValue(ok(1)); + vi.mocked(storageRepo.findOrphans).mockResolvedValue(ok([])); + + const result = await reclaimOrphanedStorageObjects(); + + expect(repo.pruneSupersededTakes).toHaveBeenCalledWith(expect.any(Number)); + // The prune drops rows only. Deleting the blob here would race a concurrent + // re-upload of the same bytes, which revives that very storage_objects row. + expect(deleteVerseAudio).not.toHaveBeenCalled(); + expect(storageRepo.markDeleted).not.toHaveBeenCalled(); + expect(result).toEqual(ok(0)); + }); + + it('still runs the orphan sweep when the take prune fails', async () => { + vi.mocked(repo.pruneSupersededTakes).mockResolvedValue(err(ErrorCode.INTERNAL_ERROR)); + const candidate = orphan(3, 'unit-9/text-3/c'); + vi.mocked(storageRepo.findOrphans).mockResolvedValue(ok([candidate])); + vi.mocked(storageRepo.reclaimOrphanIfUnreferenced).mockImplementation( + async (_id, _graceMs, deleteObject) => { + await deleteObject(candidate); + return ok(true); + } + ); + + const result = await reclaimOrphanedStorageObjects(); + + expect(storageRepo.findOrphans).toHaveBeenCalled(); + expect(storageRepo.reclaimOrphanIfUnreferenced).toHaveBeenCalledWith( + 3, + expect.any(Number), + expect.any(Function) + ); + expect(result).toEqual(ok(1)); + }); + }); +}); diff --git a/src/domains/verse-audio/verse-audio.reclaim.service.ts b/src/domains/verse-audio/verse-audio.reclaim.service.ts new file mode 100644 index 00000000..620401ab --- /dev/null +++ b/src/domains/verse-audio/verse-audio.reclaim.service.ts @@ -0,0 +1,64 @@ +import type { Result } from '@/lib/types'; + +import env from '@/env'; +import { deleteVerseAudio } from '@/lib/audio-storage'; +import { logger } from '@/lib/logger'; +import { ok } from '@/lib/types'; + +import * as storageRepo from './storage-objects.repository'; +import * as repo from './verse-audio.repository'; + +/** + * Deletes objects nothing references any more and stamps their rows. + * + * Runs in two independent phases. First it drops superseded takes on settled + * clean units: hash-keyed blobs are per-take, so without this a verse re-recorded + * ten times would keep ten objects forever. Conflicted units are left intact + * until resolve. That phase only deletes take rows — the objects they freed fall + * to the orphan pass on a later sweep, which is what keeps a concurrent + * re-upload of the same bytes from having its revived row stamped deleted. + * + * The orphan pass is the counterpart to the cascade: dropping a project unit + * cascades its recordings away, but Postgres cannot touch a bucket, so without + * this the audio would sit there forever. Runs on an interval from the server + * entrypoint. + */ +export async function reclaimOrphanedStorageObjects(): Promise> { + // Independent of the orphan pass below: a persistent failure here must not + // starve reclamation of objects left behind by deleted project units. + const pruned = await repo.pruneSupersededTakes(env.AUDIO_TAKE_RETENTION_MS); + if (!pruned.ok) { + logger.error({ + message: 'Superseded take prune failed; continuing to the orphan sweep', + context: { code: pruned.error.code }, + }); + } else if (pruned.data > 0) { + logger.info('Pruned superseded verse audio takes', { pruned: pruned.data }); + } + + let reclaimed = 0; + const orphans = await storageRepo.findOrphans(env.AUDIO_RECLAIM_GRACE_MS); + if (!orphans.ok) { + return orphans; + } + + for (const orphan of orphans.data) { + // The candidate list is only a hint. Lock and revalidate immediately before + // touching the bucket so an upload cannot gain a live reference between the + // database check and object deletion. + const result = await storageRepo.reclaimOrphanIfUnreferenced( + orphan.id, + env.AUDIO_RECLAIM_GRACE_MS, + async (locked) => deleteVerseAudio(locked.key) + ); + if (!result.ok || !result.data) { + continue; + } + reclaimed++; + } + + if (reclaimed > 0) { + logger.info('Reclaimed orphaned verse audio objects', { reclaimed }); + } + return ok(reclaimed); +} diff --git a/src/domains/verse-audio/verse-audio.repository.test.ts b/src/domains/verse-audio/verse-audio.repository.test.ts index b773b882..4956ab0b 100644 --- a/src/domains/verse-audio/verse-audio.repository.test.ts +++ b/src/domains/verse-audio/verse-audio.repository.test.ts @@ -94,27 +94,6 @@ describe('verse-audio repository', () => { expect(result).toEqual(err(ErrorCode.VERSE_AUDIO_NOT_FOUND)); }); - it('maps a reclaimed storage object during first recording insert to 409', async () => { - vi.mocked(db.insert).mockImplementation(() => { - throw foreignKeyRace('verse_audio_recordings_storage_object_id_storage_objects_id_fk'); - }); - - const result = await repo.insertRecording({ - projectUnitId: 1, - bibleTextId: 2, - uploadedBy: 3, - storageObjectId: 4, - contentType: 'audio/mp4', - sizeBytes: 5, - durationSeconds: null, - versionToken: 1, - conflictStatus: 'clean', - activeTakeId: null, - }); - - expect(result).toEqual(err(ErrorCode.VERSE_AUDIO_VERSION_CONFLICT)); - }); - it('maps a pruned active take during conditional promotion to 409', async () => { vi.mocked(db.update).mockImplementation(() => { throw foreignKeyRace('verse_audio_recordings_active_take_id_verse_audio_takes_id_fk'); diff --git a/src/domains/verse-audio/verse-audio.repository.ts b/src/domains/verse-audio/verse-audio.repository.ts index b86d321c..96eca3fc 100644 --- a/src/domains/verse-audio/verse-audio.repository.ts +++ b/src/domains/verse-audio/verse-audio.repository.ts @@ -110,48 +110,6 @@ export async function listByChapter( } } -/** - * Insert-only unit create. On a concurrent first-upload race, reloads the - * winner instead of overwriting its metadata / version token. - */ -export async function insertRecording( - input: UpsertVerseAudioInput -): Promise> { - try { - const [row] = await db - .insert(verse_audio_recordings) - .values(input) - .onConflictDoNothing({ - target: [verse_audio_recordings.projectUnitId, verse_audio_recordings.bibleTextId], - }) - .returning(); - - if (!row) { - const existing = await get(input.projectUnitId, input.bibleTextId); - if (!existing.ok) { - return existing; - } - return ok(existing.data); - } - - const result = await get(row.projectUnitId, row.bibleTextId); - if (!result.ok) { - return err(ErrorCode.INTERNAL_ERROR); - } - return ok(result.data); - } catch (error) { - logger.error({ - cause: error, - message: 'Failed to insert verse audio recording', - context: { input: { ...input, sizeBytes: input.sizeBytes } }, - }); - if (hasPostgresErrorCode(error, '23503')) { - return mapForeignKeyViolation(error); - } - return err(ErrorCode.INTERNAL_ERROR); - } -} - export type FirstRecordingOutcome = 'created' | 'concurrent' | 'link-conflict'; /** diff --git a/src/domains/verse-audio/verse-audio.responses.ts b/src/domains/verse-audio/verse-audio.responses.ts new file mode 100644 index 00000000..0c8d8764 --- /dev/null +++ b/src/domains/verse-audio/verse-audio.responses.ts @@ -0,0 +1,188 @@ +import type { Result } from '@/lib/types'; + +import { audioBlobName, generateAudioDownloadUrl } from '@/lib/audio-storage'; +import { err, ErrorCode, ok } from '@/lib/types'; + +import type { + VerseAudioRecord, + VerseAudioTakeRecord, + VerseAudioTakeWithUrl, + VerseAudioWithUrl, +} from './verse-audio.types'; + +import * as storageRepo from './storage-objects.repository'; +import * as repo from './verse-audio.repository'; + +/** Migration 0025 placeholders — real bytes live at the pre-hash key shape. */ +const LEGACY_CONTENT_HASH_PREFIX = 'legacy-'; + +function toIso(date: Date): string { + return date.toISOString(); +} + +function isLegacyContentHash(contentHash: string): boolean { + return contentHash.startsWith(LEGACY_CONTENT_HASH_PREFIX); +} + +/** Pre-hash object key (`unit-{id}/text-{id}`) used before contentHash entered the path. */ +function legacyBlobName(projectUnitId: number, bibleTextId: number): string { + return `unit-${projectUnitId}/text-${bibleTextId}`; +} + +export function fallbackBlobKey( + projectUnitId: number, + bibleTextId: number, + contentHash: string +): string { + return isLegacyContentHash(contentHash) + ? legacyBlobName(projectUnitId, bibleTextId) + : audioBlobName(projectUnitId, bibleTextId, contentHash); +} + +async function storageKeysById(ids: number[]): Promise> { + const map = new Map(); + if (ids.length === 0) { + return map; + } + + const result = await storageRepo.getByIds(ids); + if (!result.ok) { + return map; + } + for (const row of result.data) { + map.set(row.id, row.key); + } + return map; +} + +/** + * Modern takes use a deterministic hash-keyed blob name — no DB round trip. + * Legacy backfill rows still need storage_objects.key (old path without hash). + */ +function resolveDownloadUrl( + storageObjectId: number | null, + contentHash: string, + projectUnitId: number, + bibleTextId: number, + keysById: Map +): Promise { + if (isLegacyContentHash(contentHash) && storageObjectId !== null) { + const key = keysById.get(storageObjectId); + if (key) { + return generateAudioDownloadUrl(key); + } + } + return generateAudioDownloadUrl(fallbackBlobKey(projectUnitId, bibleTextId, contentHash)); +} + +function collectLegacyStorageObjectIds( + entries: Array<{ record: VerseAudioRecord; takes: VerseAudioTakeRecord[] }> +): number[] { + const ids = new Set(); + for (const { record, takes } of entries) { + for (const take of takes) { + if (isLegacyContentHash(take.contentHash) && take.storageObjectId !== null) { + ids.add(take.storageObjectId); + } + } + + const active = takes.find((t) => t.id === record.activeTakeId) ?? takes[takes.length - 1]; + const activeIsLegacy = active ? isLegacyContentHash(active.contentHash) : true; + if (activeIsLegacy && record.storageObjectId !== null) { + ids.add(record.storageObjectId); + } + } + return [...ids]; +} + +async function takeWithUrl( + take: VerseAudioTakeRecord, + projectUnitId: number, + bibleTextId: number, + keysById: Map +): Promise { + return { + id: take.id, + uploadedBy: take.uploadedBy, + contentType: take.contentType, + sizeBytes: take.sizeBytes, + durationSeconds: take.durationSeconds, + contentHash: take.contentHash, + downloadUrl: await resolveDownloadUrl( + take.storageObjectId, + take.contentHash, + projectUnitId, + bibleTextId, + keysById + ), + createdAt: toIso(take.createdAt), + updatedAt: toIso(take.updatedAt), + }; +} + +async function withTakesAndUrl( + record: VerseAudioRecord, + takes: VerseAudioTakeRecord[], + keysById: Map +): Promise { + const takeViews = await Promise.all( + takes.map((take) => takeWithUrl(take, record.projectUnitId, record.bibleTextId, keysById)) + ); + + const active = + takeViews.find((t) => t.id === record.activeTakeId) ?? takeViews[takeViews.length - 1]; + + const activeHash = active?.contentHash ?? `${LEGACY_CONTENT_HASH_PREFIX}missing`; + + return { + id: record.id, + projectUnitId: record.projectUnitId, + bibleTextId: record.bibleTextId, + uploadedBy: record.uploadedBy, + contentType: record.contentType, + sizeBytes: record.sizeBytes, + durationSeconds: record.durationSeconds, + versionToken: record.versionToken, + conflictStatus: record.conflictStatus, + activeTakeId: record.activeTakeId, + verseNumber: record.verseNumber, + downloadUrl: await resolveDownloadUrl( + record.storageObjectId, + activeHash, + record.projectUnitId, + record.bibleTextId, + keysById + ), + takes: takeViews, + createdAt: toIso(record.createdAt), + updatedAt: toIso(record.updatedAt), + }; +} + +export async function buildRecordingResponses( + entries: Array<{ record: VerseAudioRecord; takes: VerseAudioTakeRecord[] }> +): Promise { + const keysById = await storageKeysById(collectLegacyStorageObjectIds(entries)); + return Promise.all(entries.map(({ record, takes }) => withTakesAndUrl(record, takes, keysById))); +} + +export async function loadUnitResponse( + projectUnitId: number, + bibleTextId: number +): Promise> { + const recording = await repo.get(projectUnitId, bibleTextId); + if (!recording.ok) { + return recording; + } + + const takes = await repo.listTakesForRecording(recording.data.id); + if (!takes.ok) { + return takes; + } + + const [response] = await buildRecordingResponses([{ record: recording.data, takes: takes.data }]); + if (!response) { + return err(ErrorCode.INTERNAL_ERROR); + } + return ok(response); +} diff --git a/src/domains/verse-audio/verse-audio.service.test.ts b/src/domains/verse-audio/verse-audio.service.test.ts index 36006dc3..c0cbff7e 100644 --- a/src/domains/verse-audio/verse-audio.service.test.ts +++ b/src/domains/verse-audio/verse-audio.service.test.ts @@ -1,16 +1,9 @@ -import { Buffer } from 'node:buffer'; -import { createHash } from 'node:crypto'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { - audioBlobName, - deleteVerseAudio, - generateAudioDownloadUrl, - uploadVerseAudio, -} from '@/lib/audio-storage'; +import { deleteVerseAudio, generateAudioDownloadUrl } from '@/lib/audio-storage'; import { err, ErrorCode, ok } from '@/lib/types'; -import type { VerseAudioRecord, VerseAudioTakeRecord } from './verse-audio.types'; +import type { VerseAudioTakeRecord } from './verse-audio.types'; import * as storageRepo from './storage-objects.repository'; import * as repo from './verse-audio.repository'; @@ -18,10 +11,9 @@ import { deleteRecording, getRecording, listChapterRecordings, - reclaimOrphanedStorageObjects, resolveConflict, - uploadRecording, } from './verse-audio.service'; +import { record, take } from './verse-audio.test-fixtures'; import { VERSE_AUDIO_CONFLICT_STATUS } from './verse-audio.types'; vi.mock('@/lib/audio-storage', () => ({ @@ -29,700 +21,55 @@ vi.mock('@/lib/audio-storage', () => ({ (unitId: number, textId: number, hash: string) => `unit-${unitId}/text-${textId}/${hash}` ), audioBucket: vi.fn(() => 'verse-audio'), - uploadVerseAudio: vi.fn(async () => {}), deleteVerseAudio: vi.fn(async () => {}), generateAudioDownloadUrl: vi.fn(async (key: string) => `https://r2.example/${key}?sig=x`), + uploadVerseAudio: vi.fn(async () => {}), })); vi.mock('./storage-objects.repository', () => ({ claim: vi.fn(), + findOrphans: vi.fn(), getById: vi.fn(), getByIds: vi.fn(), markDeleted: vi.fn(), - findOrphans: vi.fn(), reclaimOrphanIfUnreferenced: vi.fn(), })); vi.mock('./verse-audio.repository', () => ({ + createFirstRecordingWithTake: vi.fn(), + findTakeByContentHash: vi.fn(), get: vi.fn(), + getTakeById: vi.fn(), + insertTake: vi.fn(), listByChapter: vi.fn(), listTakesByRecordingIds: vi.fn(), listTakesForRecording: vi.fn(), - findTakeByContentHash: vi.fn(), - getTakeById: vi.fn(), - insertTake: vi.fn(), - insertRecording: vi.fn(), - createFirstRecordingWithTake: vi.fn(), - updateRecordingStateIfVersion: vi.fn(), markConflictPreservingActive: vi.fn(), - remove: vi.fn(), pruneSupersededTakes: vi.fn(), + remove: vi.fn(), + updateRecordingStateIfVersion: vi.fn(), })); vi.mock('@/lib/logger', () => ({ - logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() }, + logger: { debug: vi.fn(), error: vi.fn(), info: vi.fn(), warn: vi.fn() }, })); -const hashOf = (data: Buffer) => createHash('sha256').update(data).digest('hex'); - -const record: VerseAudioRecord = { - id: 1, - projectUnitId: 12, - bibleTextId: 3401, - uploadedBy: 7, - storageObjectId: 55, - contentType: 'audio/mp4', - sizeBytes: 4, - durationSeconds: 12.5, - versionToken: 1, - conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CLEAN, - activeTakeId: 10, - verseNumber: 3, - createdAt: new Date('2026-07-01T00:00:00Z'), - updatedAt: new Date('2026-07-01T00:00:00Z'), -}; - -const take: VerseAudioTakeRecord = { - id: 10, - recordingId: 1, - uploadedBy: 7, - storageObjectId: 55, - contentType: 'audio/mp4', - sizeBytes: 4, - durationSeconds: 12.5, - contentHash: hashOf(Buffer.from('abcd')), - createdAt: new Date('2026-07-01T00:00:00Z'), - updatedAt: new Date('2026-07-01T00:00:00Z'), -}; - -const uploadInput = { - projectUnitId: 12, - bibleTextId: 3401, - uploadedBy: 7, - contentType: 'audio/mp4', - data: Buffer.from('abcd'), - durationSeconds: 12.5, -}; - describe('verse-audio service', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(storageRepo.claim).mockResolvedValue( + vi.mocked(storageRepo.getById).mockResolvedValue( ok({ - id: 55, bucket: 'verse-audio', - key: `unit-12/text-3401/${take.contentHash}`, createdAt: new Date(), deletedAt: null, - }) - ); - vi.mocked(storageRepo.getById).mockResolvedValue( - ok({ id: 55, - bucket: 'verse-audio', key: `unit-12/text-3401/${take.contentHash}`, - createdAt: new Date(), - deletedAt: null, }) ); vi.mocked(storageRepo.getByIds).mockResolvedValue(ok([])); - vi.mocked(storageRepo.markDeleted).mockResolvedValue(ok(undefined)); - vi.mocked(storageRepo.reclaimOrphanIfUnreferenced).mockResolvedValue(ok(false)); - vi.mocked(uploadVerseAudio).mockResolvedValue(undefined); vi.mocked(deleteVerseAudio).mockResolvedValue(undefined); vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([take])); vi.mocked(repo.listTakesByRecordingIds).mockResolvedValue(ok([take])); - vi.mocked(repo.pruneSupersededTakes).mockResolvedValue(ok(0)); - }); - - describe('uploadRecording', () => { - it('creates the first take cleanly when no unit exists', async () => { - vi.mocked(repo.get) - .mockResolvedValueOnce(err(ErrorCode.VERSE_AUDIO_NOT_FOUND)) - .mockResolvedValue(ok({ ...record, activeTakeId: 10, versionToken: 2 })); - vi.mocked(repo.createFirstRecordingWithTake).mockResolvedValue( - ok({ - outcome: 'created', - record: { ...record, activeTakeId: 10, versionToken: 2 }, - take, - }) - ); - - const result = await uploadRecording(uploadInput); - - expect(result.ok).toBe(true); - if (result.ok) { - expect(result.data.versionToken).toBe(2); - expect(result.data.conflictStatus).toBe('clean'); - expect(result.data.takes).toHaveLength(1); - } - expect(repo.createFirstRecordingWithTake).toHaveBeenCalledWith( - expect.objectContaining({ - projectUnitId: 12, - bibleTextId: 3401, - versionToken: 1, - activeTakeId: null, - }), - expect.objectContaining({ contentHash: take.contentHash }) - ); - expect(repo.markConflictPreservingActive).not.toHaveBeenCalled(); - }); - - it('does not flag conflict when a concurrent first upload reloads the same active take', async () => { - vi.mocked(repo.get) - .mockResolvedValueOnce(err(ErrorCode.VERSE_AUDIO_NOT_FOUND)) - .mockResolvedValue(ok(record)); - vi.mocked(repo.createFirstRecordingWithTake).mockResolvedValue( - ok({ outcome: 'concurrent', record, take }) - ); - - const result = await uploadRecording(uploadInput); - - expect(result.ok).toBe(true); - expect(repo.markConflictPreservingActive).not.toHaveBeenCalled(); - }); - - it('flags conflict when a concurrent first upload introduces a different take', async () => { - const winnerTake = { ...take, id: 11, contentHash: hashOf(Buffer.from('other')) }; - const winner = { ...record, activeTakeId: 11, versionToken: 2 }; - vi.mocked(repo.get) - .mockResolvedValueOnce(err(ErrorCode.VERSE_AUDIO_NOT_FOUND)) - .mockResolvedValue(ok({ ...winner, conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT })); - vi.mocked(repo.createFirstRecordingWithTake).mockResolvedValue( - ok({ outcome: 'concurrent', record: winner, take }) - ); - vi.mocked(repo.markConflictPreservingActive).mockResolvedValue( - ok({ ...winner, conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT }) - ); - vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([take, winnerTake])); - - const result = await uploadRecording(uploadInput); - - expect(repo.markConflictPreservingActive).toHaveBeenCalledWith(winner.id); - expect(result.ok).toBe(true); - if (result.ok) { - expect(result.data.conflictStatus).toBe('conflict'); - } - }); - - it('rejects unsupported content types without touching storage', async () => { - const result = await uploadRecording({ ...uploadInput, contentType: 'video/mp4' }); - - expect(result).toEqual(err(ErrorCode.UNSUPPORTED_AUDIO_TYPE)); - expect(uploadVerseAudio).not.toHaveBeenCalled(); - }); - - it('rejects empty files without touching storage', async () => { - const result = await uploadRecording({ ...uploadInput, data: Buffer.alloc(0) }); - - expect(result).toEqual(err(ErrorCode.EMPTY_AUDIO_FILE)); - expect(uploadVerseAudio).not.toHaveBeenCalled(); - }); - - it('suppresses false conflicts when content hash already exists on the active take', async () => { - vi.mocked(repo.get).mockResolvedValue(ok(record)); - vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(take)); - - const result = await uploadRecording({ ...uploadInput, baseVersionToken: 2 }); - - expect(result.ok).toBe(true); - expect(repo.insertTake).not.toHaveBeenCalled(); - expect(uploadVerseAudio).not.toHaveBeenCalled(); - expect(repo.updateRecordingStateIfVersion).not.toHaveBeenCalled(); - }); - - it('promotes an existing non-active take when base is fresh (intentional revert)', async () => { - const prior = { ...take, id: 10 }; - const active = { - ...take, - id: 11, - contentHash: hashOf(Buffer.from('newer')), - storageObjectId: 56, - }; - const unit = { ...record, versionToken: 3, activeTakeId: 11, storageObjectId: 56 }; - - vi.mocked(repo.get) - .mockResolvedValueOnce(ok(unit)) - .mockResolvedValue(ok({ ...unit, versionToken: 4, activeTakeId: 10, storageObjectId: 55 })); - vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(prior)); - vi.mocked(repo.updateRecordingStateIfVersion).mockResolvedValue( - ok({ - applied: true, - record: { ...unit, versionToken: 4, activeTakeId: 10, storageObjectId: 55 }, - }) - ); - vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([prior, active])); - - const result = await uploadRecording({ - ...uploadInput, - data: Buffer.from('abcd'), - baseVersionToken: 3, - }); - - expect(repo.insertTake).not.toHaveBeenCalled(); - expect(uploadVerseAudio).not.toHaveBeenCalled(); - expect(repo.updateRecordingStateIfVersion).toHaveBeenCalledWith( - 1, - 3, - expect.objectContaining({ - activeTakeId: 10, - versionToken: 4, - }) - ); - expect(result.ok).toBe(true); - if (result.ok) { - expect(result.data.activeTakeId).toBe(10); - } - }); - - it('preserves conflict when a legacy client replaces the active take', async () => { - const conflicted = { - ...record, - conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT, - }; - const newData = Buffer.from('legacy-over-conflict'); - const newHash = hashOf(newData); - const newTake = { ...take, id: 11, contentHash: newHash, storageObjectId: 56 }; - - vi.mocked(repo.get) - .mockResolvedValueOnce(ok(conflicted)) - .mockResolvedValue( - ok({ - ...conflicted, - versionToken: 2, - activeTakeId: 11, - storageObjectId: 56, - conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT, - }) - ); - vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(null)); - vi.mocked(storageRepo.claim).mockResolvedValue( - ok({ - id: 56, - bucket: 'verse-audio', - key: `unit-12/text-3401/${newHash}`, - createdAt: new Date(), - deletedAt: null, - }) - ); - vi.mocked(repo.insertTake).mockResolvedValue(ok(newTake)); - vi.mocked(repo.updateRecordingStateIfVersion).mockResolvedValue( - ok({ - applied: true, - record: { - ...conflicted, - versionToken: 2, - activeTakeId: 11, - conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT, - }, - }) - ); - vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([take, newTake])); - - const result = await uploadRecording({ - ...uploadInput, - data: newData, - }); - - expect(repo.updateRecordingStateIfVersion).toHaveBeenCalledWith( - 1, - 1, - expect.objectContaining({ - versionToken: 2, - activeTakeId: 11, - }) - ); - expect(vi.mocked(repo.updateRecordingStateIfVersion).mock.calls[0]![2]).not.toHaveProperty( - 'conflictStatus' - ); - expect(result.ok).toBe(true); - if (result.ok) { - expect(result.data.conflictStatus).toBe('conflict'); - expect(result.data.activeTakeId).toBe(11); - } - }); - - it('does not reopen a resolved conflict when a stale base re-submits a known non-active take', async () => { - const prior = { ...take, id: 10 }; - const active = { - ...take, - id: 11, - contentHash: hashOf(Buffer.from('newer')), - storageObjectId: 56, - }; - const resolved = { - ...record, - versionToken: 4, - activeTakeId: 11, - storageObjectId: 56, - conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CLEAN, - }; - - vi.mocked(repo.get).mockResolvedValue(ok(resolved)); - vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(prior)); - vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([prior, active])); - - const result = await uploadRecording({ - ...uploadInput, - data: Buffer.from('abcd'), - baseVersionToken: 1, - }); - - expect(repo.updateRecordingStateIfVersion).not.toHaveBeenCalled(); - expect(repo.markConflictPreservingActive).not.toHaveBeenCalled(); - expect(result).toEqual( - expect.objectContaining({ - ok: true, - data: expect.objectContaining({ conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CLEAN }), - }) - ); - }); - - it('logs when a legacy client omits baseVersionToken', async () => { - const { logger } = await import('@/lib/logger'); - vi.mocked(repo.get).mockResolvedValue(ok(record)); - vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(take)); - - await uploadRecording(uploadInput); - - expect(logger.info).toHaveBeenCalledWith( - expect.objectContaining({ event: 'verse_audio_upload_legacy_no_token' }), - expect.stringContaining('legacy replace') - ); - }); - - it('does not let a losing client clear its own conflict by retrying the same bytes', async () => { - // The stale-base response hands the loser the current token. Re-sending the - // same bytes may promote its take, but must not settle the contest for it. - const losing = { ...take, id: 10 }; - const active = { ...take, id: 11, contentHash: hashOf(Buffer.from('newer')) }; - const conflicted = { - ...record, - versionToken: 3, - activeTakeId: 11, - conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT, - }; - - vi.mocked(repo.get) - .mockResolvedValueOnce(ok(conflicted)) - .mockResolvedValue(ok({ ...conflicted, versionToken: 4, activeTakeId: 10 })); - vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(losing)); - vi.mocked(repo.updateRecordingStateIfVersion).mockResolvedValue( - ok({ applied: true, record: { ...conflicted, versionToken: 4, activeTakeId: 10 } }) - ); - vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([losing, active])); - - const result = await uploadRecording({ ...uploadInput, baseVersionToken: 3 }); - - expect(vi.mocked(repo.updateRecordingStateIfVersion).mock.calls[0]![2]).not.toHaveProperty( - 'conflictStatus' - ); - expect(result.ok).toBe(true); - if (result.ok) { - expect(result.data.conflictStatus).toBe('conflict'); - } - }); - - it('does not reopen a conflict when resolve wins a duplicate-take promotion race', async () => { - const prior = { ...take, id: 10 }; - const active = { - ...take, - id: 11, - contentHash: hashOf(Buffer.from('newer')), - storageObjectId: 56, - }; - const conflicted = { - ...record, - versionToken: 3, - activeTakeId: 11, - conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT, - }; - const resolved = { - ...conflicted, - versionToken: 4, - conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CLEAN, - }; - - vi.mocked(repo.get).mockResolvedValueOnce(ok(conflicted)).mockResolvedValue(ok(resolved)); - vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(prior)); - vi.mocked(repo.updateRecordingStateIfVersion).mockResolvedValue( - ok({ applied: false, record: null }) - ); - vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([prior, active])); - - const result = await uploadRecording({ ...uploadInput, baseVersionToken: 3 }); - - expect(repo.markConflictPreservingActive).not.toHaveBeenCalled(); - expect(result).toEqual( - expect.objectContaining({ - ok: true, - data: expect.objectContaining({ conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CLEAN }), - }) - ); - }); - - it('leaves an open conflict alone when a matching-token upload brings new bytes', async () => { - const newData = Buffer.from('fresh-over-conflict'); - const newHash = hashOf(newData); - const newTake = { ...take, id: 12, contentHash: newHash, storageObjectId: 56 }; - const conflicted = { - ...record, - conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT, - }; - - vi.mocked(repo.get) - .mockResolvedValueOnce(ok(conflicted)) - .mockResolvedValue(ok({ ...conflicted, versionToken: 2, activeTakeId: 12 })); - vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(null)); - vi.mocked(storageRepo.claim).mockResolvedValue( - ok({ - id: 56, - bucket: 'verse-audio', - key: `unit-12/text-3401/${newHash}`, - createdAt: new Date(), - deletedAt: null, - }) - ); - vi.mocked(repo.insertTake).mockResolvedValue(ok(newTake)); - vi.mocked(repo.updateRecordingStateIfVersion).mockResolvedValue( - ok({ applied: true, record: { ...conflicted, versionToken: 2, activeTakeId: 12 } }) - ); - vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([take, newTake])); - - const result = await uploadRecording({ - ...uploadInput, - data: newData, - baseVersionToken: 1, - }); - - expect(vi.mocked(repo.updateRecordingStateIfVersion).mock.calls[0]![2]).not.toHaveProperty( - 'conflictStatus' - ); - expect(result.ok).toBe(true); - if (result.ok) { - expect(result.data.conflictStatus).toBe('conflict'); - } - }); - - it('replaces cleanly when baseVersionToken is omitted (legacy clients)', async () => { - const newData = Buffer.from('legacy-replace'); - const newHash = hashOf(newData); - const newTake = { ...take, id: 11, contentHash: newHash, storageObjectId: 56 }; - - vi.mocked(repo.get) - .mockResolvedValueOnce(ok(record)) - .mockResolvedValue(ok({ ...record, versionToken: 2, activeTakeId: 11 })); - vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(null)); - vi.mocked(storageRepo.claim).mockResolvedValue( - ok({ - id: 56, - bucket: 'verse-audio', - key: `unit-12/text-3401/${newHash}`, - createdAt: new Date(), - deletedAt: null, - }) - ); - vi.mocked(repo.insertTake).mockResolvedValue(ok(newTake)); - vi.mocked(repo.updateRecordingStateIfVersion).mockResolvedValue( - ok({ - applied: true, - record: { - ...record, - versionToken: 2, - activeTakeId: 11, - storageObjectId: 56, - sizeBytes: newData.length, - }, - }) - ); - vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([take, newTake])); - - const result = await uploadRecording({ - ...uploadInput, - data: newData, - }); - - expect(repo.updateRecordingStateIfVersion).toHaveBeenCalledWith( - 1, - 1, - expect.objectContaining({ - versionToken: 2, - activeTakeId: 11, - }) - ); - expect(result.ok).toBe(true); - if (result.ok) { - expect(result.data.conflictStatus).toBe('clean'); - } - }); - - it('updates cleanly when baseVersionToken matches', async () => { - const newData = Buffer.from('efgh'); - const newHash = hashOf(newData); - const newTake = { ...take, id: 11, contentHash: newHash, storageObjectId: 56 }; - - vi.mocked(repo.get) - .mockResolvedValueOnce(ok(record)) - .mockResolvedValue(ok({ ...record, versionToken: 2, activeTakeId: 11 })); - vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(null)); - vi.mocked(storageRepo.claim).mockResolvedValue( - ok({ - id: 56, - bucket: 'verse-audio', - key: `unit-12/text-3401/${newHash}`, - createdAt: new Date(), - deletedAt: null, - }) - ); - vi.mocked(repo.insertTake).mockResolvedValue(ok(newTake)); - vi.mocked(repo.updateRecordingStateIfVersion).mockResolvedValue( - ok({ - applied: true, - record: { - ...record, - versionToken: 2, - activeTakeId: 11, - storageObjectId: 56, - sizeBytes: 4, - }, - }) - ); - vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([take, newTake])); - - const result = await uploadRecording({ - ...uploadInput, - data: newData, - baseVersionToken: 1, - }); - - expect(repo.updateRecordingStateIfVersion).toHaveBeenCalledWith( - 1, - 1, - expect.objectContaining({ - versionToken: 2, - activeTakeId: 11, - }) - ); - expect(result.ok).toBe(true); - if (result.ok) { - expect(result.data.conflictStatus).toBe('clean'); - expect(result.data.versionToken).toBe(2); - } - }); - - it('keeps a new take and marks conflict when the clean-update CAS loses', async () => { - const newData = Buffer.from('concurrent'); - const newHash = hashOf(newData); - const newTake = { ...take, id: 11, contentHash: newHash, storageObjectId: 56 }; - const conflicted = { - ...record, - conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT, - }; - - vi.mocked(repo.get).mockResolvedValueOnce(ok(record)).mockResolvedValue(ok(conflicted)); - vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(null)); - vi.mocked(storageRepo.claim).mockResolvedValue( - ok({ - id: 56, - bucket: 'verse-audio', - key: `unit-12/text-3401/${newHash}`, - createdAt: new Date(), - deletedAt: null, - }) - ); - vi.mocked(repo.insertTake).mockResolvedValue(ok(newTake)); - vi.mocked(repo.updateRecordingStateIfVersion).mockResolvedValue( - ok({ applied: false, record: null }) - ); - vi.mocked(repo.markConflictPreservingActive).mockResolvedValue(ok(conflicted)); - vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([take, newTake])); - - const result = await uploadRecording({ - ...uploadInput, - data: newData, - baseVersionToken: 1, - }); - - expect(repo.insertTake).toHaveBeenCalledWith( - expect.objectContaining({ contentHash: newHash }) - ); - expect(repo.markConflictPreservingActive).toHaveBeenCalledWith(record.id); - expect(result.ok).toBe(true); - if (result.ok) { - expect(result.data.conflictStatus).toBe('conflict'); - expect(result.data.takes).toHaveLength(2); - } - }); - - it('marks conflict when baseVersionToken is stale', async () => { - const newData = Buffer.from('conflict'); - const newHash = hashOf(newData); - const conflictTake = { ...take, id: 11, contentHash: newHash, uploadedBy: 9 }; - - vi.mocked(repo.get) - .mockResolvedValueOnce(ok(record)) - .mockResolvedValue( - ok({ - ...record, - versionToken: 2, - conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT, - }) - ); - vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(null)); - vi.mocked(storageRepo.claim).mockResolvedValue( - ok({ - id: 56, - bucket: 'verse-audio', - key: `unit-12/text-3401/${newHash}`, - createdAt: new Date(), - deletedAt: null, - }) - ); - vi.mocked(repo.insertTake).mockResolvedValue(ok(conflictTake)); - vi.mocked(repo.updateRecordingStateIfVersion).mockResolvedValue( - ok({ - applied: true, - record: { - ...record, - versionToken: 2, - conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT, - }, - }) - ); - vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([take, conflictTake])); - - const result = await uploadRecording({ - ...uploadInput, - data: newData, - uploadedBy: 9, - baseVersionToken: 2, - }); - - expect(repo.updateRecordingStateIfVersion).toHaveBeenCalledWith( - 1, - 1, - expect.objectContaining({ - versionToken: 2, - conflictStatus: 'conflict', - }) - ); - expect(result.ok).toBe(true); - if (result.ok) { - expect(result.data.conflictStatus).toBe('conflict'); - expect(result.data.takes).toHaveLength(2); - expect(result.data.activeTakeId).toBe(10); - } - }); - - it('returns INTERNAL_ERROR and skips the DB when the blob upload throws', async () => { - vi.mocked(repo.get).mockResolvedValue(err(ErrorCode.VERSE_AUDIO_NOT_FOUND)); - vi.mocked(uploadVerseAudio).mockRejectedValue(new Error('r2 down')); - - const result = await uploadRecording(uploadInput); - - expect(result).toEqual(err(ErrorCode.INTERNAL_ERROR)); - expect(repo.insertRecording).not.toHaveBeenCalled(); - }); }); describe('getRecording', () => { @@ -755,11 +102,11 @@ describe('verse-audio service', () => { vi.mocked(storageRepo.getByIds).mockResolvedValue( ok([ { - id: 55, bucket: 'verse-audio', - key: 'unit-12/text-3401', createdAt: new Date(), deletedAt: null, + id: 55, + key: 'unit-12/text-3401', }, ]) ); @@ -791,10 +138,10 @@ describe('verse-audio service', () => { it('returns items and hasConflict rollup', async () => { const conflicted = { ...record, - id: 2, bibleTextId: 3402, - verseNumber: 4, conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT, + id: 2, + verseNumber: 4, }; vi.mocked(repo.listByChapter).mockResolvedValue(ok([record, conflicted])); vi.mocked(repo.listTakesByRecordingIds).mockResolvedValue( @@ -815,10 +162,10 @@ describe('verse-audio service', () => { it('excludes resolved recordings from hasConflict rollup', async () => { const resolved = { ...record, - id: 2, bibleTextId: 3402, - verseNumber: 4, conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.RESOLVED, + id: 2, + verseNumber: 4, }; vi.mocked(repo.listByChapter).mockResolvedValue(ok([record, resolved])); vi.mocked(repo.listTakesByRecordingIds).mockResolvedValue( @@ -838,18 +185,18 @@ describe('verse-audio service', () => { const legacyA = { ...record, storageObjectId: 55 }; const legacyB = { ...record, - id: 2, + activeTakeId: 20, bibleTextId: 3402, - verseNumber: 4, + id: 2, storageObjectId: 66, - activeTakeId: 20, + verseNumber: 4, }; const takeA: VerseAudioTakeRecord = { ...take, contentHash: 'legacy-1', storageObjectId: 55 }; const takeB: VerseAudioTakeRecord = { ...take, + contentHash: 'legacy-2', id: 20, recordingId: 2, - contentHash: 'legacy-2', storageObjectId: 66, }; vi.mocked(repo.listByChapter).mockResolvedValue(ok([legacyA, legacyB])); @@ -857,18 +204,18 @@ describe('verse-audio service', () => { vi.mocked(storageRepo.getByIds).mockResolvedValue( ok([ { - id: 55, bucket: 'verse-audio', - key: 'unit-12/text-3401', createdAt: new Date(), deletedAt: null, + id: 55, + key: 'unit-12/text-3401', }, { - id: 66, bucket: 'verse-audio', - key: 'unit-12/text-3402', createdAt: new Date(), deletedAt: null, + id: 66, + key: 'unit-12/text-3402', }, ]) ); @@ -892,7 +239,7 @@ describe('verse-audio service', () => { describe('resolveConflict', () => { it('sets the chosen take active and marks the conflict resolved', async () => { - const other = { ...take, id: 11, uploadedBy: 9, contentHash: 'other' }; + const other = { ...take, contentHash: 'other', id: 11, uploadedBy: 9 }; vi.mocked(repo.get) .mockResolvedValueOnce( ok({ ...record, conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CONFLICT }) @@ -900,9 +247,9 @@ describe('verse-audio service', () => { .mockResolvedValue( ok({ ...record, - versionToken: 2, - conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.RESOLVED, activeTakeId: 11, + conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.RESOLVED, + versionToken: 2, }) ); vi.mocked(repo.getTakeById).mockResolvedValue(ok(other)); @@ -911,17 +258,17 @@ describe('verse-audio service', () => { applied: true, record: { ...record, - versionToken: 2, - conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.RESOLVED, activeTakeId: 11, + conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.RESOLVED, + versionToken: 2, }, }) ); vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([take, other])); const result = await resolveConflict({ - projectUnitId: 12, bibleTextId: 3401, + projectUnitId: 12, takeId: 11, }); @@ -931,8 +278,8 @@ describe('verse-audio service', () => { expect.objectContaining({ activeTakeId: 11, conflictStatus: 'resolved', - versionToken: 2, storageObjectId: other.storageObjectId, + versionToken: 2, }) ); expect(result.ok).toBe(true); @@ -952,8 +299,8 @@ describe('verse-audio service', () => { ); const result = await resolveConflict({ - projectUnitId: 12, bibleTextId: 3401, + projectUnitId: 12, takeId: 10, }); @@ -965,8 +312,8 @@ describe('verse-audio service', () => { vi.mocked(repo.getTakeById).mockResolvedValue(ok({ ...take, recordingId: 999 })); const result = await resolveConflict({ - projectUnitId: 12, bibleTextId: 3401, + projectUnitId: 12, takeId: 10, }); @@ -980,8 +327,8 @@ describe('verse-audio service', () => { vi.mocked(repo.getTakeById).mockResolvedValue(ok(take)); const result = await resolveConflict({ - projectUnitId: 12, bibleTextId: 3401, + projectUnitId: 12, takeId: 10, }); @@ -996,8 +343,8 @@ describe('verse-audio service', () => { vi.mocked(repo.getTakeById).mockResolvedValue(ok(take)); const result = await resolveConflict({ - projectUnitId: 12, bibleTextId: 3401, + projectUnitId: 12, takeId: 10, }); @@ -1010,8 +357,8 @@ describe('verse-audio service', () => { it('removes the row then each take object', async () => { const secondTake: VerseAudioTakeRecord = { ...take, - id: 11, contentHash: 'second-hash', + id: 11, storageObjectId: 56, }; vi.mocked(repo.get).mockResolvedValue(ok(record)); @@ -1020,20 +367,20 @@ describe('verse-audio service', () => { vi.mocked(storageRepo.getById) .mockResolvedValueOnce( ok({ - id: 55, bucket: 'verse-audio', - key: `unit-12/text-3401/${take.contentHash}`, createdAt: new Date(), deletedAt: null, + id: 55, + key: `unit-12/text-3401/${take.contentHash}`, }) ) .mockResolvedValueOnce( ok({ - id: 56, bucket: 'verse-audio', - key: 'unit-12/text-3401/second-hash', createdAt: new Date(), deletedAt: null, + id: 56, + key: 'unit-12/text-3401/second-hash', }) ); @@ -1056,103 +403,4 @@ describe('verse-audio service', () => { expect(repo.remove).not.toHaveBeenCalled(); }); }); - - describe('storage object tracking', () => { - it('claims the storage row before writing the object on first upload', async () => { - const order: string[] = []; - vi.mocked(repo.get) - .mockResolvedValueOnce(err(ErrorCode.VERSE_AUDIO_NOT_FOUND)) - .mockResolvedValue(ok(record)); - vi.mocked(storageRepo.claim).mockImplementation(async () => { - order.push('claim'); - return ok({ - id: 55, - bucket: 'verse-audio', - key: `unit-12/text-3401/${take.contentHash}`, - createdAt: new Date(), - deletedAt: null, - }); - }); - vi.mocked(uploadVerseAudio).mockImplementation(async () => { - order.push('upload'); - }); - vi.mocked(repo.createFirstRecordingWithTake).mockResolvedValue( - ok({ outcome: 'created', record, take }) - ); - - await uploadRecording(uploadInput); - - expect(order).toEqual(['claim', 'upload']); - expect(storageRepo.claim).toHaveBeenCalledWith( - 'verse-audio', - `unit-12/text-3401/${take.contentHash}` - ); - expect(audioBlobName).toHaveBeenCalledWith(12, 3401, take.contentHash); - }); - }); - - describe('reclaimOrphanedStorageObjects', () => { - const orphan = (id: number, key: string) => ({ - id, - key, - bucket: 'verse-audio', - createdAt: new Date(), - deletedAt: null, - }); - - it('locks and revalidates each candidate before deleting its object', async () => { - const candidates = [orphan(1, 'unit-9/text-1/a'), orphan(2, 'unit-9/text-2/b')]; - vi.mocked(storageRepo.findOrphans).mockResolvedValue(ok(candidates)); - vi.mocked(storageRepo.reclaimOrphanIfUnreferenced).mockImplementation( - async (id, _graceMs, deleteObject) => { - const candidate = candidates.find((item) => item.id === id)!; - await deleteObject(candidate); - return ok(true); - } - ); - - const result = await reclaimOrphanedStorageObjects(); - - expect(storageRepo.findOrphans).toHaveBeenCalledWith(expect.any(Number)); - expect(storageRepo.reclaimOrphanIfUnreferenced).toHaveBeenCalledTimes(2); - expect(deleteVerseAudio).toHaveBeenCalledTimes(2); - expect(result).toEqual(ok(2)); - }); - - it('prunes superseded takes before the orphan sweep, leaving their objects to it', async () => { - vi.mocked(repo.pruneSupersededTakes).mockResolvedValue(ok(1)); - vi.mocked(storageRepo.findOrphans).mockResolvedValue(ok([])); - - const result = await reclaimOrphanedStorageObjects(); - - expect(repo.pruneSupersededTakes).toHaveBeenCalledWith(expect.any(Number)); - // The prune drops rows only. Deleting the blob here would race a concurrent - // re-upload of the same bytes, which revives that very storage_objects row. - expect(deleteVerseAudio).not.toHaveBeenCalled(); - expect(storageRepo.markDeleted).not.toHaveBeenCalled(); - expect(result).toEqual(ok(0)); - }); - - it('still runs the orphan sweep when the take prune fails', async () => { - vi.mocked(repo.pruneSupersededTakes).mockResolvedValue(err(ErrorCode.INTERNAL_ERROR)); - const candidate = orphan(3, 'unit-9/text-3/c'); - vi.mocked(storageRepo.findOrphans).mockResolvedValue(ok([candidate])); - vi.mocked(storageRepo.reclaimOrphanIfUnreferenced).mockImplementation( - async (_id, _graceMs, deleteObject) => { - await deleteObject(candidate); - return ok(true); - } - ); - - const result = await reclaimOrphanedStorageObjects(); - - expect(storageRepo.findOrphans).toHaveBeenCalled(); - expect(storageRepo.reclaimOrphanIfUnreferenced).toHaveBeenCalledWith( - 3, - expect.any(Number), - expect.any(Function) - ); - expect(result).toEqual(ok(1)); - }); - }); }); diff --git a/src/domains/verse-audio/verse-audio.service.ts b/src/domains/verse-audio/verse-audio.service.ts index af74d452..3e8e1e63 100644 --- a/src/domains/verse-audio/verse-audio.service.ts +++ b/src/domains/verse-audio/verse-audio.service.ts @@ -4,12 +4,10 @@ import { createHash } from 'node:crypto'; import type { Result } from '@/lib/types'; -import env from '@/env'; import { audioBlobName, audioBucket, deleteVerseAudio, - generateAudioDownloadUrl, uploadVerseAudio, } from '@/lib/audio-storage'; import { logger } from '@/lib/logger'; @@ -18,190 +16,23 @@ import { err, ErrorCode, ok } from '@/lib/types'; import type { ResolveConflictInput, UploadRecordingInput, - VerseAudioRecord, VerseAudioTakeRecord, - VerseAudioTakeWithUrl, VerseAudioWithUrl, } from './verse-audio.types'; import * as storageRepo from './storage-objects.repository'; import * as repo from './verse-audio.repository'; +import { + buildRecordingResponses, + fallbackBlobKey, + loadUnitResponse, +} from './verse-audio.responses'; import { ALLOWED_AUDIO_CONTENT_TYPES, VERSE_AUDIO_CONFLICT_STATUS } from './verse-audio.types'; -/** Migration 0025 placeholders — real bytes live at the pre-hash key shape. */ -const LEGACY_CONTENT_HASH_PREFIX = 'legacy-'; - function contentHashOf(data: Buffer): string { return createHash('sha256').update(data).digest('hex'); } -function toIso(date: Date): string { - return date.toISOString(); -} - -function isLegacyContentHash(contentHash: string): boolean { - return contentHash.startsWith(LEGACY_CONTENT_HASH_PREFIX); -} - -/** Pre-hash object key (`unit-{id}/text-{id}`) used before contentHash entered the path. */ -function legacyBlobName(projectUnitId: number, bibleTextId: number): string { - return `unit-${projectUnitId}/text-${bibleTextId}`; -} - -function fallbackBlobKey(projectUnitId: number, bibleTextId: number, contentHash: string): string { - return isLegacyContentHash(contentHash) - ? legacyBlobName(projectUnitId, bibleTextId) - : audioBlobName(projectUnitId, bibleTextId, contentHash); -} - -async function storageKeysById(ids: number[]): Promise> { - const map = new Map(); - if (ids.length === 0) { - return map; - } - - const result = await storageRepo.getByIds(ids); - if (!result.ok) { - return map; - } - for (const row of result.data) { - map.set(row.id, row.key); - } - return map; -} - -/** - * Modern takes use a deterministic hash-keyed blob name — no DB round trip. - * Legacy backfill rows still need storage_objects.key (old path without hash). - */ -function resolveDownloadUrl( - storageObjectId: number | null, - contentHash: string, - projectUnitId: number, - bibleTextId: number, - keysById: Map -): Promise { - if (isLegacyContentHash(contentHash) && storageObjectId !== null) { - const key = keysById.get(storageObjectId); - if (key) { - return generateAudioDownloadUrl(key); - } - } - return generateAudioDownloadUrl(fallbackBlobKey(projectUnitId, bibleTextId, contentHash)); -} - -function collectLegacyStorageObjectIds( - entries: Array<{ record: VerseAudioRecord; takes: VerseAudioTakeRecord[] }> -): number[] { - const ids = new Set(); - for (const { record, takes } of entries) { - for (const take of takes) { - if (isLegacyContentHash(take.contentHash) && take.storageObjectId !== null) { - ids.add(take.storageObjectId); - } - } - - const active = takes.find((t) => t.id === record.activeTakeId) ?? takes[takes.length - 1]; - const activeIsLegacy = active ? isLegacyContentHash(active.contentHash) : true; - if (activeIsLegacy && record.storageObjectId !== null) { - ids.add(record.storageObjectId); - } - } - return [...ids]; -} - -async function takeWithUrl( - take: VerseAudioTakeRecord, - projectUnitId: number, - bibleTextId: number, - keysById: Map -): Promise { - return { - id: take.id, - uploadedBy: take.uploadedBy, - contentType: take.contentType, - sizeBytes: take.sizeBytes, - durationSeconds: take.durationSeconds, - contentHash: take.contentHash, - downloadUrl: await resolveDownloadUrl( - take.storageObjectId, - take.contentHash, - projectUnitId, - bibleTextId, - keysById - ), - createdAt: toIso(take.createdAt), - updatedAt: toIso(take.updatedAt), - }; -} - -async function withTakesAndUrl( - record: VerseAudioRecord, - takes: VerseAudioTakeRecord[], - keysById: Map -): Promise { - const takeViews = await Promise.all( - takes.map((take) => takeWithUrl(take, record.projectUnitId, record.bibleTextId, keysById)) - ); - - const active = - takeViews.find((t) => t.id === record.activeTakeId) ?? takeViews[takeViews.length - 1]; - - const activeHash = active?.contentHash ?? `${LEGACY_CONTENT_HASH_PREFIX}missing`; - - return { - id: record.id, - projectUnitId: record.projectUnitId, - bibleTextId: record.bibleTextId, - uploadedBy: record.uploadedBy, - contentType: record.contentType, - sizeBytes: record.sizeBytes, - durationSeconds: record.durationSeconds, - versionToken: record.versionToken, - conflictStatus: record.conflictStatus, - activeTakeId: record.activeTakeId, - verseNumber: record.verseNumber, - downloadUrl: await resolveDownloadUrl( - record.storageObjectId, - activeHash, - record.projectUnitId, - record.bibleTextId, - keysById - ), - takes: takeViews, - createdAt: toIso(record.createdAt), - updatedAt: toIso(record.updatedAt), - }; -} - -async function buildRecordingResponses( - entries: Array<{ record: VerseAudioRecord; takes: VerseAudioTakeRecord[] }> -): Promise { - const keysById = await storageKeysById(collectLegacyStorageObjectIds(entries)); - return Promise.all(entries.map(({ record, takes }) => withTakesAndUrl(record, takes, keysById))); -} - -async function loadUnitResponse( - projectUnitId: number, - bibleTextId: number -): Promise> { - const recording = await repo.get(projectUnitId, bibleTextId); - if (!recording.ok) { - return recording; - } - - const takes = await repo.listTakesForRecording(recording.data.id); - if (!takes.ok) { - return takes; - } - - const [response] = await buildRecordingResponses([{ record: recording.data, takes: takes.data }]); - if (!response) { - return err(ErrorCode.INTERNAL_ERROR); - } - return ok(response); -} - async function storeTakeBytes( projectUnitId: number, bibleTextId: number, @@ -594,57 +425,4 @@ export async function deleteRecording( return ok(undefined); } -/** - * Deletes objects nothing references any more and stamps their rows. - * - * Runs in two independent phases. First it drops superseded takes on settled - * clean units: hash-keyed blobs are per-take, so without this a verse re-recorded - * ten times would keep ten objects forever. Conflicted units are left intact - * until resolve. That phase only deletes take rows — the objects they freed fall - * to the orphan pass on a later sweep, which is what keeps a concurrent - * re-upload of the same bytes from having its revived row stamped deleted. - * - * The orphan pass is the counterpart to the cascade: dropping a project unit - * cascades its recordings away, but Postgres cannot touch a bucket, so without - * this the audio would sit there forever. Runs on an interval from the server - * entrypoint. - */ -export async function reclaimOrphanedStorageObjects(): Promise> { - // Independent of the orphan pass below: a persistent failure here must not - // starve reclamation of objects left behind by deleted project units. - const pruned = await repo.pruneSupersededTakes(env.AUDIO_TAKE_RETENTION_MS); - if (!pruned.ok) { - logger.error({ - message: 'Superseded take prune failed; continuing to the orphan sweep', - context: { code: pruned.error.code }, - }); - } else if (pruned.data > 0) { - logger.info('Pruned superseded verse audio takes', { pruned: pruned.data }); - } - - let reclaimed = 0; - const orphans = await storageRepo.findOrphans(env.AUDIO_RECLAIM_GRACE_MS); - if (!orphans.ok) { - return orphans; - } - - for (const orphan of orphans.data) { - // The candidate list is only a hint. Lock and revalidate immediately before - // touching the bucket so an upload cannot gain a live reference between the - // database check and object deletion. - const result = await storageRepo.reclaimOrphanIfUnreferenced( - orphan.id, - env.AUDIO_RECLAIM_GRACE_MS, - async (locked) => deleteVerseAudio(locked.key) - ); - if (!result.ok || !result.data) { - continue; - } - reclaimed++; - } - - if (reclaimed > 0) { - logger.info('Reclaimed orphaned verse audio objects', { reclaimed }); - } - return ok(reclaimed); -} +export { reclaimOrphanedStorageObjects } from './verse-audio.reclaim.service'; diff --git a/src/domains/verse-audio/verse-audio.test-fixtures.ts b/src/domains/verse-audio/verse-audio.test-fixtures.ts new file mode 100644 index 00000000..3cc30f79 --- /dev/null +++ b/src/domains/verse-audio/verse-audio.test-fixtures.ts @@ -0,0 +1,51 @@ +import { Buffer } from 'node:buffer'; +import { createHash } from 'node:crypto'; + +import type { + UploadRecordingInput, + VerseAudioRecord, + VerseAudioTakeRecord, +} from './verse-audio.types'; + +import { VERSE_AUDIO_CONFLICT_STATUS } from './verse-audio.types'; + +export const hashOf = (data: Buffer) => createHash('sha256').update(data).digest('hex'); + +export const record: VerseAudioRecord = { + id: 1, + projectUnitId: 12, + bibleTextId: 3401, + uploadedBy: 7, + storageObjectId: 55, + contentType: 'audio/mp4', + sizeBytes: 4, + durationSeconds: 12.5, + versionToken: 1, + conflictStatus: VERSE_AUDIO_CONFLICT_STATUS.CLEAN, + activeTakeId: 10, + verseNumber: 3, + createdAt: new Date('2026-07-01T00:00:00Z'), + updatedAt: new Date('2026-07-01T00:00:00Z'), +}; + +export const take: VerseAudioTakeRecord = { + id: 10, + recordingId: 1, + uploadedBy: 7, + storageObjectId: 55, + contentType: 'audio/mp4', + sizeBytes: 4, + durationSeconds: 12.5, + contentHash: hashOf(Buffer.from('abcd')), + createdAt: new Date('2026-07-01T00:00:00Z'), + updatedAt: new Date('2026-07-01T00:00:00Z'), +}; + +export const uploadInput: UploadRecordingInput = { + projectUnitId: 12, + bibleTextId: 3401, + uploadedBy: 7, + contentType: 'audio/mp4', + data: Buffer.from('abcd'), + durationSeconds: 12.5, +}; diff --git a/src/domains/verse-audio/verse-audio.upload.service.test.ts b/src/domains/verse-audio/verse-audio.upload.service.test.ts new file mode 100644 index 00000000..ea4a014a --- /dev/null +++ b/src/domains/verse-audio/verse-audio.upload.service.test.ts @@ -0,0 +1,344 @@ +import { Buffer } from 'node:buffer'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { audioBlobName, uploadVerseAudio } from '@/lib/audio-storage'; +import { err, ErrorCode, ok } from '@/lib/types'; + +import * as storageRepo from './storage-objects.repository'; +import * as repo from './verse-audio.repository'; +import { uploadRecording } from './verse-audio.service'; +import { hashOf, record, take, uploadInput } from './verse-audio.test-fixtures'; + +vi.mock('@/lib/audio-storage', () => ({ + audioBlobName: vi.fn( + (unitId: number, textId: number, hash: string) => `unit-${unitId}/text-${textId}/${hash}` + ), + audioBucket: vi.fn(() => 'verse-audio'), + deleteVerseAudio: vi.fn(async () => {}), + generateAudioDownloadUrl: vi.fn(async (key: string) => `https://r2.example/${key}?sig=x`), + uploadVerseAudio: vi.fn(async () => {}), +})); + +vi.mock('./storage-objects.repository', () => ({ + claim: vi.fn(), + findOrphans: vi.fn(), + getById: vi.fn(), + getByIds: vi.fn(), + markDeleted: vi.fn(), + reclaimOrphanIfUnreferenced: vi.fn(), +})); + +vi.mock('./verse-audio.repository', () => ({ + createFirstRecordingWithTake: vi.fn(), + findTakeByContentHash: vi.fn(), + get: vi.fn(), + getTakeById: vi.fn(), + insertTake: vi.fn(), + listByChapter: vi.fn(), + listTakesByRecordingIds: vi.fn(), + listTakesForRecording: vi.fn(), + markConflictPreservingActive: vi.fn(), + pruneSupersededTakes: vi.fn(), + remove: vi.fn(), + updateRecordingStateIfVersion: vi.fn(), +})); + +vi.mock('@/lib/logger', () => ({ + logger: { debug: vi.fn(), error: vi.fn(), info: vi.fn(), warn: vi.fn() }, +})); + +describe('verse-audio service', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(storageRepo.claim).mockResolvedValue( + ok({ + bucket: 'verse-audio', + createdAt: new Date(), + deletedAt: null, + id: 55, + key: `unit-12/text-3401/${take.contentHash}`, + }) + ); + vi.mocked(storageRepo.getById).mockResolvedValue( + ok({ + bucket: 'verse-audio', + createdAt: new Date(), + deletedAt: null, + id: 55, + key: `unit-12/text-3401/${take.contentHash}`, + }) + ); + vi.mocked(storageRepo.getByIds).mockResolvedValue(ok([])); + vi.mocked(storageRepo.markDeleted).mockResolvedValue(ok(undefined)); + vi.mocked(uploadVerseAudio).mockResolvedValue(undefined); + vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([take])); + vi.mocked(repo.listTakesByRecordingIds).mockResolvedValue(ok([take])); + }); + + describe('uploadRecording', () => { + it('creates the first take cleanly when no unit exists', async () => { + vi.mocked(repo.get) + .mockResolvedValueOnce(err(ErrorCode.VERSE_AUDIO_NOT_FOUND)) + .mockResolvedValue(ok({ ...record, activeTakeId: 10, versionToken: 2 })); + vi.mocked(repo.createFirstRecordingWithTake).mockResolvedValue( + ok({ + outcome: 'created', + record: { ...record, activeTakeId: 10, versionToken: 2 }, + take, + }) + ); + + const result = await uploadRecording(uploadInput); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.versionToken).toBe(2); + expect(result.data.conflictStatus).toBe('clean'); + expect(result.data.takes).toHaveLength(1); + } + expect(repo.createFirstRecordingWithTake).toHaveBeenCalledWith( + expect.objectContaining({ + activeTakeId: null, + bibleTextId: 3401, + conflictStatus: 'clean', + contentType: 'audio/mp4', + durationSeconds: 12.5, + projectUnitId: 12, + sizeBytes: 4, + storageObjectId: 55, + uploadedBy: 7, + versionToken: 1, + }), + expect.objectContaining({ + contentHash: take.contentHash, + contentType: 'audio/mp4', + durationSeconds: 12.5, + sizeBytes: 4, + storageObjectId: 55, + uploadedBy: 7, + }) + ); + }); + + it('rejects unsupported content types without touching storage', async () => { + const result = await uploadRecording({ ...uploadInput, contentType: 'video/mp4' }); + + expect(result).toEqual(err(ErrorCode.UNSUPPORTED_AUDIO_TYPE)); + expect(uploadVerseAudio).not.toHaveBeenCalled(); + }); + + it('rejects empty files without touching storage', async () => { + const result = await uploadRecording({ ...uploadInput, data: Buffer.alloc(0) }); + + expect(result).toEqual(err(ErrorCode.EMPTY_AUDIO_FILE)); + expect(uploadVerseAudio).not.toHaveBeenCalled(); + }); + + it('promotes an existing non-active take when base is fresh (intentional revert)', async () => { + const prior = { ...take, id: 10 }; + const active = { + ...take, + contentHash: hashOf(Buffer.from('newer')), + id: 11, + storageObjectId: 56, + }; + const unit = { ...record, activeTakeId: 11, storageObjectId: 56, versionToken: 3 }; + + vi.mocked(repo.get) + .mockResolvedValueOnce(ok(unit)) + .mockResolvedValue(ok({ ...unit, activeTakeId: 10, storageObjectId: 55, versionToken: 4 })); + vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(prior)); + vi.mocked(repo.updateRecordingStateIfVersion).mockResolvedValue( + ok({ + applied: true, + record: { ...unit, activeTakeId: 10, storageObjectId: 55, versionToken: 4 }, + }) + ); + vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([prior, active])); + + const result = await uploadRecording({ + ...uploadInput, + baseVersionToken: 3, + data: Buffer.from('abcd'), + }); + + expect(repo.insertTake).not.toHaveBeenCalled(); + expect(uploadVerseAudio).not.toHaveBeenCalled(); + expect(repo.updateRecordingStateIfVersion).toHaveBeenCalledWith( + 1, + 3, + expect.objectContaining({ + activeTakeId: 10, + versionToken: 4, + }) + ); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.activeTakeId).toBe(10); + } + }); + + it('logs when a legacy client omits baseVersionToken', async () => { + const { logger } = await import('@/lib/logger'); + vi.mocked(repo.get).mockResolvedValue(ok(record)); + vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(take)); + + await uploadRecording(uploadInput); + + expect(logger.info).toHaveBeenCalledWith( + expect.objectContaining({ event: 'verse_audio_upload_legacy_no_token' }), + expect.stringContaining('legacy replace') + ); + }); + + it('replaces cleanly when baseVersionToken is omitted (legacy clients)', async () => { + const newData = Buffer.from('legacy-replace'); + const newHash = hashOf(newData); + const newTake = { ...take, contentHash: newHash, id: 11, storageObjectId: 56 }; + + vi.mocked(repo.get) + .mockResolvedValueOnce(ok(record)) + .mockResolvedValue(ok({ ...record, activeTakeId: 11, versionToken: 2 })); + vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(null)); + vi.mocked(storageRepo.claim).mockResolvedValue( + ok({ + bucket: 'verse-audio', + createdAt: new Date(), + deletedAt: null, + id: 56, + key: `unit-12/text-3401/${newHash}`, + }) + ); + vi.mocked(repo.insertTake).mockResolvedValue(ok(newTake)); + vi.mocked(repo.updateRecordingStateIfVersion).mockResolvedValue( + ok({ + applied: true, + record: { + ...record, + activeTakeId: 11, + sizeBytes: newData.length, + storageObjectId: 56, + versionToken: 2, + }, + }) + ); + vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([take, newTake])); + + const result = await uploadRecording({ + ...uploadInput, + data: newData, + }); + + expect(repo.updateRecordingStateIfVersion).toHaveBeenCalledWith( + 1, + 1, + expect.objectContaining({ + activeTakeId: 11, + versionToken: 2, + }) + ); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.conflictStatus).toBe('clean'); + } + }); + + it('updates cleanly when baseVersionToken matches', async () => { + const newData = Buffer.from('efgh'); + const newHash = hashOf(newData); + const newTake = { ...take, contentHash: newHash, id: 11, storageObjectId: 56 }; + + vi.mocked(repo.get) + .mockResolvedValueOnce(ok(record)) + .mockResolvedValue(ok({ ...record, activeTakeId: 11, versionToken: 2 })); + vi.mocked(repo.findTakeByContentHash).mockResolvedValue(ok(null)); + vi.mocked(storageRepo.claim).mockResolvedValue( + ok({ + bucket: 'verse-audio', + createdAt: new Date(), + deletedAt: null, + id: 56, + key: `unit-12/text-3401/${newHash}`, + }) + ); + vi.mocked(repo.insertTake).mockResolvedValue(ok(newTake)); + vi.mocked(repo.updateRecordingStateIfVersion).mockResolvedValue( + ok({ + applied: true, + record: { + ...record, + activeTakeId: 11, + sizeBytes: 4, + storageObjectId: 56, + versionToken: 2, + }, + }) + ); + vi.mocked(repo.listTakesForRecording).mockResolvedValue(ok([take, newTake])); + + const result = await uploadRecording({ + ...uploadInput, + baseVersionToken: 1, + data: newData, + }); + + expect(repo.updateRecordingStateIfVersion).toHaveBeenCalledWith( + 1, + 1, + expect.objectContaining({ + activeTakeId: 11, + versionToken: 2, + }) + ); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.conflictStatus).toBe('clean'); + expect(result.data.versionToken).toBe(2); + } + }); + + it('returns INTERNAL_ERROR and skips the DB when the blob upload throws', async () => { + vi.mocked(repo.get).mockResolvedValue(err(ErrorCode.VERSE_AUDIO_NOT_FOUND)); + vi.mocked(uploadVerseAudio).mockRejectedValue(new Error('r2 down')); + + const result = await uploadRecording(uploadInput); + + expect(result).toEqual(err(ErrorCode.INTERNAL_ERROR)); + expect(repo.createFirstRecordingWithTake).not.toHaveBeenCalled(); + }); + }); + + describe('storage object tracking', () => { + it('claims the storage row before writing the object on first upload', async () => { + const order: string[] = []; + vi.mocked(repo.get) + .mockResolvedValueOnce(err(ErrorCode.VERSE_AUDIO_NOT_FOUND)) + .mockResolvedValue(ok(record)); + vi.mocked(storageRepo.claim).mockImplementation(async () => { + order.push('claim'); + return ok({ + bucket: 'verse-audio', + createdAt: new Date(), + deletedAt: null, + id: 55, + key: `unit-12/text-3401/${take.contentHash}`, + }); + }); + vi.mocked(uploadVerseAudio).mockImplementation(async () => { + order.push('upload'); + }); + vi.mocked(repo.createFirstRecordingWithTake).mockResolvedValue( + ok({ outcome: 'created', record, take }) + ); + + await uploadRecording(uploadInput); + + expect(order).toEqual(['claim', 'upload']); + expect(storageRepo.claim).toHaveBeenCalledWith( + 'verse-audio', + `unit-12/text-3401/${take.contentHash}` + ); + expect(audioBlobName).toHaveBeenCalledWith(12, 3401, take.contentHash); + }); + }); +}); diff --git a/src/lib/services/aquifer/aquifer.client.ts b/src/lib/services/aquifer/aquifer.client.ts index 7c039909..13d32ac2 100644 --- a/src/lib/services/aquifer/aquifer.client.ts +++ b/src/lib/services/aquifer/aquifer.client.ts @@ -32,7 +32,6 @@ import { const DEFAULT_TIMEOUT_MS = 30_000; /** Hard cap on search pagination to avoid unbounded Aquifer fan-out. */ export const AQUIFER_MAX_SEARCH_PAGES = 20; -const MAX_SEARCH_PAGES = AQUIFER_MAX_SEARCH_PAGES; /** Cap on upstream body text echoed into logs. */ const MAX_LOGGED_BODY_CHARS = 300; /** Cap on zod issues echoed into logs. */ @@ -238,9 +237,12 @@ export async function getResource(contentId: number): Promise @@ -250,16 +252,13 @@ export async function searchAllResources( const limit = 100; let complete = false; - for (let pageIndex = 0; pageIndex < MAX_SEARCH_PAGES; pageIndex += 1) { + for (let pageIndex = 0; pageIndex < AQUIFER_MAX_SEARCH_PAGES; pageIndex += 1) { const page = await searchResources({ ...params, limit, offset }); if (!page.ok) return page; allItems.push(...page.data.items); - if ( - allItems.length >= page.data.totalItemCount || - page.data.items.length === 0 || - page.data.items.length < limit - ) { + // Empty page is subsumed by `< limit` (0 < 100); keep one short-page check. + if (allItems.length >= page.data.totalItemCount || page.data.items.length < limit) { complete = true; break; } diff --git a/src/workers/ingest-bible-text.worker.test.ts b/src/workers/ingest-bible-text.worker.test.ts index aac4e743..ed0e2bcb 100644 --- a/src/workers/ingest-bible-text.worker.test.ts +++ b/src/workers/ingest-bible-text.worker.test.ts @@ -241,9 +241,8 @@ describe('dblIngestTextWorker', () => { const { logger } = await import('../lib/logger'); const infoSpy = vi.spyOn(logger, 'info').mockImplementation(() => undefined as any); - const chapterAssignmentsService = await import( - '../domains/chapter-assignments/chapter-assignments.service' - ); + const chapterAssignmentsService = + await import('../domains/chapter-assignments/chapter-assignments.service'); vi.mocked(chapterAssignmentsService.createChapterAssignmentForProjectUnit).mockResolvedValue({ ok: true, data: [], @@ -265,9 +264,8 @@ describe('dblIngestTextWorker', () => { await registerDblIngestTextWorker(boss); const handler = work.mock.calls[0][2]; - const chapterAssignmentsService = await import( - '../domains/chapter-assignments/chapter-assignments.service' - ); + const chapterAssignmentsService = + await import('../domains/chapter-assignments/chapter-assignments.service'); vi.mocked(chapterAssignmentsService.createChapterAssignmentForProjectUnit).mockResolvedValue({ ok: true, data: [], @@ -356,9 +354,10 @@ describe('dblIngestTextWorker', () => { await expect(retryHandler(retryJob)).resolves.toBeUndefined(); expect(usfmImportService.materializePendingUsfmImportsForBible).toHaveBeenCalledTimes(3); - expect(usfmImportService.materializePendingUsfmImportsForBible).toHaveBeenLastCalledWith(1, [ - 7, - ]); + expect(usfmImportService.materializePendingUsfmImportsForBible).toHaveBeenLastCalledWith( + 1, + [7] + ); expect(mockDblClientInstance.getChapter).toHaveBeenCalledTimes(1); }); @@ -405,9 +404,8 @@ describe('dblIngestTextWorker', () => { const infoSpy = vi.spyOn(logger, 'info').mockImplementation(() => undefined as any); const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => undefined as any); - const chapterAssignmentsService = await import( - '../domains/chapter-assignments/chapter-assignments.service' - ); + const chapterAssignmentsService = + await import('../domains/chapter-assignments/chapter-assignments.service'); vi.mocked(chapterAssignmentsService.createChapterAssignmentForProjectUnit).mockResolvedValue({ ok: false, error: { message: 'insert failed', code: 'INTERNAL_ERROR' }, From 10d26f0c0e6a4b2f8ed91eac9625cc9f5b2f2809 Mon Sep 17 00:00:00 2001 From: mattrace-gloo Date: Thu, 22 Oct 2026 13:10:13 -0700 Subject: [PATCH 3/3] test(resources): assert images-range warn on incomplete Aquifer search Mirror the notes-range coverage for the images verse-range page-ceiling warn. --- .../translation-resources.range.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/domains/translation-resources/translation-resources.range.test.ts b/src/domains/translation-resources/translation-resources.range.test.ts index 71abc561..3c0b4fcb 100644 --- a/src/domains/translation-resources/translation-resources.range.test.ts +++ b/src/domains/translation-resources/translation-resources.range.test.ts @@ -279,5 +279,37 @@ describe('translation-resources verse-range routes', () => { expect(res.status).toBe(200); expect(await res.json()).toEqual({ items: [] }); }); + + it('warns when Aquifer images range search is incomplete and still returns collected items', async () => { + asAuthenticatedUser(); + asProjectMember(); + const hits = [imageSearchHit(55)]; + vi.mocked(aquiferClient.searchAllResources).mockResolvedValue(searchEnumeration(hits, false)); + vi.mocked(aquiferClient.getResource).mockResolvedValue( + ok({ + id: 55, + name: 'map', + localizedName: 'Map 55', + content: { url: 'https://cdn.example/55.jpg', size: 55 }, + grouping: { type: 'Images', name: 'Images', mediaType: 'Image' }, + }) + ); + + const res = await server.request(IMAGES_RANGE_PATH, { method: 'GET' }); + + expect(res.status).toBe(200); + expect((await res.json()).items).toHaveLength(1); + expect(logger.warn).toHaveBeenCalledWith({ + message: 'Aquifer images verse-range search hit page ceiling; returning partial items', + context: expect.objectContaining({ + resourceType: 'Images', + bookCode: 'MRK', + chapter: 1, + startVerse: 1, + endVerse: 8, + collectedCount: 1, + }), + }); + }); }); });