diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 3988729e..492fc566 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -295,12 +295,13 @@ jobs: - name: Install deps run: npm ci - # What ships to browsers: `dependencies` and the runtime they pull in - # (svelte, devalue). Build and test tooling (vite, vitest, tailwind, - # kit's dev server) is left out because none of it reaches a user, and - # its open advisories need major-version upgrades tracked separately. - - name: npm audit (shipped dependencies) - run: npm audit --omit=dev --audit-level=moderate + # The whole tree, build and test tooling included. Until #5021 this + # audited only what ships to browsers (`--omit=dev`), because vite, + # vitest, tailwind and kit carried advisories that needed major + # upgrades. Those upgrades landed and the full tree audits clean, so + # the tooling that builds the shipped bundle is held to the same bar. + - name: npm audit (all dependencies) + run: npm audit --audit-level=moderate - name: Type-check + svelte-check run: npm run check diff --git a/docs/security.md b/docs/security.md index 7452b8df..7ed8472a 100644 --- a/docs/security.md +++ b/docs/security.md @@ -94,7 +94,7 @@ Keystore, so a copy of the app's files doesn't yield a usable session. Nothing is published unless every check passes: the Go, integration, web and Android test suites, `govulncheck` against the toolchain that builds the -image, and `npm audit` on the packages that ship to the browser. See +image, and `npm audit` on every web dependency, build tooling included. See `.gitea/workflows/release.yml`. ## Reporting a problem diff --git a/web/package-lock.json b/web/package-lock.json index ff7b1992..e9b72a66 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -13,8 +13,8 @@ "lucide-svelte": "^1.0.1" }, "devDependencies": { - "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.8.0", + "@sveltejs/adapter-static": "^4.0.0", + "@sveltejs/kit": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", "@tailwindcss/postcss": "^4.3.3", "@testing-library/jest-dom": "^6.9.1", @@ -22,11 +22,11 @@ "@types/node": "^25.9.1", "jsdom": "^25.0.1", "postcss": "^8.5.15", - "svelte": "^5.1.9", - "svelte-check": "^4.4.8", + "svelte": "^5.57.1", + "svelte-check": "^4.7.5", "tailwindcss": "^4.3.3", - "typescript": "^5.6.3", - "vite": "^8.0.0", + "typescript": "^6.0.0", + "vite": "^8.0.12", "vitest": "^5.0.0" } }, @@ -987,46 +987,43 @@ } }, "node_modules/@sveltejs/adapter-static": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", - "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-4.0.0.tgz", + "integrity": "sha512-aF1J23R1ebKqUxQ4/qLr/Lxu63NFmLAtg8UZl3HFLxaoA1D/nhTVGcD9lTqx65iUbjNR2iOL0TgOxSX82x6Paw==", "dev": true, "license": "MIT", "peerDependencies": { - "@sveltejs/kit": "^2.0.0" + "@sveltejs/kit": "^3.0.0-next.0" } }, "node_modules/@sveltejs/kit": { - "version": "2.70.3", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.70.3.tgz", - "integrity": "sha512-UDvEYuZqAMbfB/oXIoqKvbKcb7YczK5zYrzmsGV1zRJk03jntwp8dXiYoIJotxAndsKvcPFtx9H1GRSKFdSHgg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-3.0.1.tgz", + "integrity": "sha512-8JMCCq7ZUJELi3hbhb0+c9Rs8qnbHW90vhOfM+t1FSF4fhIpG9/FOsH4IYgcITMhbdkSrrn+Kvd9G+q8A+UO6Q==", "dev": true, + "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@sveltejs/acorn-typescript": "^1.0.9", - "@types/cookie": "^0.6.0", - "acorn": "^8.16.0", - "cookie": "^0.6.0", - "devalue": "^5.8.1", + "@rolldown/pluginutils": "^1.0.1", + "@standard-schema/spec": "^1.1.0", + "cookie": "^2.0.1", + "devalue": "^5.9.4", "esm-env": "^1.2.2", - "kleur": "^4.1.5", - "magic-string": "^0.30.5", - "mrmime": "^2.0.0", - "set-cookie-parser": "^3.0.0", - "sirv": "^3.0.0" + "magic-string": "^1.2.3", + "mrmime": "^2.0.1", + "sirv": "^3.0.2" }, "bin": { "svelte-kit": "svelte-kit.js" }, "engines": { - "node": ">=18.13" + "node": ">=22.17" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.0", - "typescript": "^5.3.3 || ^6.0.0", - "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" + "@sveltejs/vite-plugin-svelte": "^7.0.0", + "svelte": "^5.57.1", + "typescript": "^6.0.0", + "vite": "^8.0.12" }, "peerDependenciesMeta": { "@opentelemetry/api": { @@ -1037,6 +1034,25 @@ } } }, + "node_modules/@sveltejs/kit/node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/@sveltejs/load-config": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.3.tgz", + "integrity": "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ==", + "dev": true, + "engines": { + "node": ">= 18.0.0" + } + }, "node_modules/@sveltejs/vite-plugin-svelte": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.3.1.tgz", @@ -1776,13 +1792,6 @@ "assertion-error": "^2.0.1" } }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -1991,13 +2000,17 @@ } }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=22" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/css.escape": { @@ -2587,16 +2600,6 @@ } } }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/lightningcss": { "version": "1.33.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", @@ -3188,13 +3191,6 @@ "node": ">=v12.22.7" } }, - "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==", - "dev": true, - "license": "MIT" - }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -3264,13 +3260,13 @@ } }, "node_modules/svelte-check": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.8.tgz", - "integrity": "sha512-67adfgBox5eNSNIvIIwgFizKGdcRrGpiMoNO2obHcYuLz7iTa8Xgm/NGU3ntMFnNm8K1grFOIG6HhMLX/vcN8w==", + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.6.tgz", + "integrity": "sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw==", "dev": true, - "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", + "@sveltejs/load-config": "^0.2.3", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", @@ -3284,7 +3280,7 @@ }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", - "typescript": ">=5.0.0" + "typescript": "^5.0.0 || ^6.0.0" } }, "node_modules/svelte/node_modules/@types/estree": { @@ -3409,9 +3405,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/web/package.json b/web/package.json index 57c49aed..c7ab0a98 100644 --- a/web/package.json +++ b/web/package.json @@ -12,7 +12,7 @@ "tokens": "node scripts/tokens-to-css.js" }, "devDependencies": { - "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/adapter-static": "^4.0.0", "@sveltejs/kit": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", "@tailwindcss/postcss": "^4.3.3", @@ -21,16 +21,19 @@ "@types/node": "^25.9.1", "jsdom": "^25.0.1", "postcss": "^8.5.15", - "svelte": "^5.1.9", - "svelte-check": "^4.4.8", + "svelte": "^5.57.1", + "svelte-check": "^4.7.5", "tailwindcss": "^4.3.3", - "typescript": "^5.6.3", - "vite": "^8.0.0", + "typescript": "^6.0.0", + "vite": "^8.0.12", "vitest": "^5.0.0" }, "dependencies": { "@neodrag/svelte": "^2.3.3", "@tanstack/svelte-query": "^5.90.2", "lucide-svelte": "^1.0.1" + }, + "imports": { + "#lib/*": "./src/lib/*" } } diff --git a/web/src/lib/api/client.test.ts b/web/src/lib/api/client.test.ts index b15e90e4..d65bedcf 100644 --- a/web/src/lib/api/client.test.ts +++ b/web/src/lib/api/client.test.ts @@ -83,7 +83,7 @@ describe('api.get/post/del', () => { describe('apiFetch 401 interceptor', () => { test('401 response triggers auth.logout({silent:true}) and still throws', async () => { const logoutSpy = vi.fn(); - vi.doMock('$lib/auth/store.svelte', () => ({ + vi.doMock('#lib/auth/store.svelte.js', () => ({ logout: logoutSpy, login: vi.fn(), bootstrap: vi.fn(), @@ -97,6 +97,6 @@ describe('apiFetch 401 interceptor', () => { status: 401 }); expect(logoutSpy).toHaveBeenCalledWith({ silent: true }); - vi.doUnmock('$lib/auth/store.svelte'); + vi.doUnmock('#lib/auth/store.svelte.js'); }); }); diff --git a/web/src/lib/api/client.ts b/web/src/lib/api/client.ts index b984d252..2e623394 100644 --- a/web/src/lib/api/client.ts +++ b/web/src/lib/api/client.ts @@ -44,7 +44,7 @@ export async function apiFetch(path: string, init?: RequestInit): Promise apiClientMock()); +vi.mock('#lib/api/client.js', () => apiClientMock()); -vi.mock('$lib/query/client', () => ({ +vi.mock('#lib/query/client.js', () => ({ queryClient: { clear: vi.fn() } })); -import { api } from '$lib/api/client'; -import { queryClient } from '$lib/query/client'; +import { api } from '#lib/api/client.js'; +import { queryClient } from '#lib/query/client.js'; import { bootstrap, login, logout, user } from './store.svelte'; beforeEach(() => { diff --git a/web/src/lib/auth/user.svelte.ts b/web/src/lib/auth/user.svelte.ts index 6918f11b..36b7b6e8 100644 --- a/web/src/lib/auth/user.svelte.ts +++ b/web/src/lib/auth/user.svelte.ts @@ -4,7 +4,7 @@ // component that needs the current user can import `user` from either // this module or auth/store — auth/store re-exports it for backward // compatibility. -import type { User } from '$lib/api/client'; +import type { User } from '#lib/api/client.js'; let _user = $state(null); diff --git a/web/src/lib/components/AcoustIDCard.svelte b/web/src/lib/components/AcoustIDCard.svelte index 25682242..3227fa5d 100644 --- a/web/src/lib/components/AcoustIDCard.svelte +++ b/web/src/lib/components/AcoustIDCard.svelte @@ -7,9 +7,9 @@ updateAcoustIDSettings, type AcoustIDOverview, type AcoustIDUnsettled - } from '$lib/api/admin'; - import { errMessage } from '$lib/api/errors'; - import { pushToast } from '$lib/stores/toast.svelte'; + } from '#lib/api/admin.js'; + import { errMessage } from '#lib/api/errors.js'; + import { pushToast } from '#lib/stores/toast.svelte.js'; // AcoustID recording-id lookup (M401): fills the MusicBrainz ids that // untagged tracks lack, so the similarity arms can see them. Off until the diff --git a/web/src/lib/components/AcoustIDCard.test.ts b/web/src/lib/components/AcoustIDCard.test.ts index ef4cf927..ae08ab8c 100644 --- a/web/src/lib/components/AcoustIDCard.test.ts +++ b/web/src/lib/components/AcoustIDCard.test.ts @@ -1,15 +1,15 @@ import { afterEach, describe, expect, test, vi } from "vitest"; import { render, screen, fireEvent, waitFor } from "@testing-library/svelte"; -import type { AcoustIDOverview } from "$lib/api/admin"; +import type { AcoustIDOverview } from "#lib/api/admin.js"; -vi.mock("$lib/api/admin", () => ({ +vi.mock("#lib/api/admin.js", () => ({ getAcoustID: vi.fn(), updateAcoustIDSettings: vi.fn(), runAcoustIDLookup: vi.fn(), listUnsettledAcoustID: vi.fn(), })); -vi.mock("$lib/stores/toast.svelte", () => ({ pushToast: vi.fn() })); +vi.mock("#lib/stores/toast.svelte.js", () => ({ pushToast: vi.fn() })); import AcoustIDCard from "./AcoustIDCard.svelte"; import { @@ -17,7 +17,7 @@ import { listUnsettledAcoustID, runAcoustIDLookup, updateAcoustIDSettings, -} from "$lib/api/admin"; +} from "#lib/api/admin.js"; const base: AcoustIDOverview = { settings: { enabled: true, min_score: 0.85, api_key_set: true }, diff --git a/web/src/lib/components/ActiveSessions.svelte b/web/src/lib/components/ActiveSessions.svelte index d095f564..f1aaa0b0 100644 --- a/web/src/lib/components/ActiveSessions.svelte +++ b/web/src/lib/components/ActiveSessions.svelte @@ -6,9 +6,9 @@ revokeSession, revokeOtherSessions, type ActiveSession - } from '$lib/api/me'; - import { errCode } from '$lib/api/errors'; - import { pushToast } from '$lib/stores/toast.svelte'; + } from '#lib/api/me.js'; + import { errCode } from '#lib/api/errors.js'; + import { pushToast } from '#lib/stores/toast.svelte.js'; // Self-contained: nothing else in the app reads this data, so it holds its // own state and reloads explicitly rather than joining the query cache. diff --git a/web/src/lib/components/ActiveSessions.test.ts b/web/src/lib/components/ActiveSessions.test.ts index 0cd226d3..d4158866 100644 --- a/web/src/lib/components/ActiveSessions.test.ts +++ b/web/src/lib/components/ActiveSessions.test.ts @@ -6,13 +6,13 @@ const listSessions = vi.fn(); const revokeSession = vi.fn(); const revokeOtherSessions = vi.fn(); -vi.mock('$lib/api/me', () => ({ +vi.mock('#lib/api/me.js', () => ({ listSessions: (...a: unknown[]) => listSessions(...a), revokeSession: (...a: unknown[]) => revokeSession(...a), revokeOtherSessions: (...a: unknown[]) => revokeOtherSessions(...a) })); -vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: vi.fn() })); +vi.mock('#lib/stores/toast.svelte.js', () => ({ pushToast: vi.fn() })); type Row = { id: string; diff --git a/web/src/lib/components/AddToPlaylistMenu.svelte b/web/src/lib/components/AddToPlaylistMenu.svelte index a172eee7..52bddbbe 100644 --- a/web/src/lib/components/AddToPlaylistMenu.svelte +++ b/web/src/lib/components/AddToPlaylistMenu.svelte @@ -1,12 +1,12 @@ {#if toast.value} diff --git a/web/src/lib/components/TrackMenu.svelte b/web/src/lib/components/TrackMenu.svelte index 74661f4f..ef7f0377 100644 --- a/web/src/lib/components/TrackMenu.svelte +++ b/web/src/lib/components/TrackMenu.svelte @@ -18,11 +18,11 @@ import AddToPlaylistMenu from './AddToPlaylistMenu.svelte'; import TrackMenuItem from './TrackMenuItem.svelte'; import TrackMenuDivider from './TrackMenuDivider.svelte'; - import { createLikedIdsQuery, likeEntity, unlikeEntity } from '$lib/api/likes'; - import { createMyQuarantineQuery, unflagTrack } from '$lib/api/quarantine'; - import { qk } from '$lib/api/queries'; - import { playNext, enqueueTrack, playRadio } from '$lib/player/store.svelte'; - import type { TrackRef } from '$lib/api/types'; + import { createLikedIdsQuery, likeEntity, unlikeEntity } from '#lib/api/likes.js'; + import { createMyQuarantineQuery, unflagTrack } from '#lib/api/quarantine.js'; + import { qk } from '#lib/api/queries.js'; + import { playNext, enqueueTrack, playRadio } from '#lib/player/store.svelte.js'; + import type { TrackRef } from '#lib/api/types.js'; let { track, diff --git a/web/src/lib/components/TrackMenu.test.ts b/web/src/lib/components/TrackMenu.test.ts index 31213cb8..bcbc28a0 100644 --- a/web/src/lib/components/TrackMenu.test.ts +++ b/web/src/lib/components/TrackMenu.test.ts @@ -13,7 +13,7 @@ const userState = vi.hoisted(() => ({ | null })); -vi.mock('$lib/auth/store.svelte', () => ({ +vi.mock('#lib/auth/store.svelte.js', () => ({ user: { get value() { return userState.current; } } })); @@ -21,20 +21,20 @@ vi.mock('$app/navigation', () => ({ goto: vi.fn() })); -vi.mock('$lib/api/likes', () => emptyLikesMock()); +vi.mock('#lib/api/likes.js', () => emptyLikesMock()); -vi.mock('$lib/api/quarantine', () => emptyQuarantineMock()); +vi.mock('#lib/api/quarantine.js', () => emptyQuarantineMock()); -vi.mock('$lib/api/playlists', () => emptyPlaylistsMock()); +vi.mock('#lib/api/playlists.js', () => emptyPlaylistsMock()); -vi.mock('$lib/player/store.svelte', () => ({ +vi.mock('#lib/player/store.svelte.js', () => ({ playNext: vi.fn(), enqueueTrack: vi.fn(), playRadio: vi.fn() })); import TrackMenu from './TrackMenu.svelte'; -import { playNext, enqueueTrack, playRadio } from '$lib/player/store.svelte'; +import { playNext, enqueueTrack, playRadio } from '#lib/player/store.svelte.js'; const track = makeTrack({ title: 'Roygbiv' }); diff --git a/web/src/lib/components/TrackRow.svelte b/web/src/lib/components/TrackRow.svelte index d290a5b1..9bf61400 100644 --- a/web/src/lib/components/TrackRow.svelte +++ b/web/src/lib/components/TrackRow.svelte @@ -1,13 +1,13 @@ diff --git a/web/src/routes/admin/+layout.ts b/web/src/routes/admin/+layout.ts index e3f246f6..2f10dd9f 100644 --- a/web/src/routes/admin/+layout.ts +++ b/web/src/routes/admin/+layout.ts @@ -1,5 +1,5 @@ import { redirect } from '@sveltejs/kit'; -import { user } from '$lib/auth/store.svelte'; +import { user } from '#lib/auth/store.svelte.js'; import type { LayoutLoad } from './$types'; // Hard route gate: runs before the layout (and any child page) renders. diff --git a/web/src/routes/admin/+page.svelte b/web/src/routes/admin/+page.svelte index 8c7ec2d3..eedd4efd 100644 --- a/web/src/routes/admin/+page.svelte +++ b/web/src/routes/admin/+page.svelte @@ -1,8 +1,8 @@