Compare commits

...

11 Commits

Author SHA1 Message Date
renovate[bot]
c7b4e8c540 chore(deps): update dependency ruff to v0.15.15 2026-06-02 19:41:18 +00:00
renovate[bot]
47c6d01617 chore(deps): update dependency vitest to v4 [security] (#7723)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-01 21:38:46 +00:00
renovate[bot]
653be9a604 chore(deps): update dependency pytest-asyncio to v1.4.0 (#7694)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-31 16:24:28 +00:00
Hayden
2d8b74282a fix: harden recipe content against stored XSS (chips, instructions, asset media) (#7719) 2026-05-31 16:14:16 +00:00
Hayden
48752bcd06 fix: support CSV/TXT upload and add validation for Plan to Eat import (#6360) (#7622) 2026-05-31 15:59:50 +00:00
Hayden
a46620d236 chore: drop unused python dependencies aniso8601 and appdirs (#7717) 2026-05-31 15:59:10 +00:00
Hayden
3bde6df958 chore: add 5-day dependency cooling period for supply-chain hardening (#7718) 2026-05-31 15:55:15 +00:00
Brian Choromanski
e1ddc06eff feat: Added version info to backup file (#7416)
Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2026-05-31 15:35:52 +00:00
Zachary Schaffter
262b531add feat: warn when deleting foods used in recipes (#7117)
Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2026-05-31 10:42:13 -05:00
miah
364af97060 dev: Improve support for front end unit tests (#7163) 2026-05-31 10:41:52 -05:00
Brian Choromanski
7b0d1fde64 feat: Enhanced PR Lint/Validation (#7329)
Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2026-05-31 10:41:43 -05:00
46 changed files with 1670 additions and 280 deletions

View File

@@ -44,6 +44,7 @@
8000, // used by mkdocs
9000,
9091, // used by docker production
51204, // used for test coverage report
24678 // used by nuxt when hot-reloading using polling
],
// Use 'onCreateCommand' to run commands at the end of container creation.

View File

@@ -20,6 +20,10 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
env:
# Install from the committed lockfile; never re-resolve (see pyproject
# [tool.uv] exclude-newer cooling window).
UV_FROZEN: "1"
steps:
- uses: actions/checkout@v6

View File

@@ -14,6 +14,10 @@ permissions:
jobs:
sync-locales:
runs-on: ubuntu-latest
env:
# Install from the committed lockfile; never re-resolve (see pyproject
# [tool.uv] exclude-newer cooling window).
UV_FROZEN: "1"
steps:
- name: Generate GitHub App Token
id: app-token

View File

@@ -3,7 +3,7 @@ name: Pull Request Linter
on:
workflow_call:
pull_request:
types: [edited] # This captures the PR title changing
types: [edited, reopened] # This captures the PR title/body changing
branches:
- mealie-next
@@ -41,3 +41,50 @@ jobs:
ignoreLabels: |
bot
ignore-semantic-pull-request
validate-template:
name: Validate PR template
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Check required PR template sections
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
if (pr.user.type === "Bot") {
console.log("Skipping template check for bot");
return;
}
const response = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: ".github/pull_request_template.md",
});
const template = Buffer.from(response.data.content, "base64").toString("utf8");
const lines = template.split("\n");
const requiredHeadings = [];
let lastHeading = null;
for (const line of lines) {
if (line.startsWith("## ")) {
lastHeading = line.trim();
} else if (line.trim() === "_(REQUIRED)_" && lastHeading) {
requiredHeadings.push(lastHeading);
lastHeading = null;
}
}
const body = pr.body || "";
const missing = requiredHeadings.filter(h => !body.includes(h));
if (missing.length > 0) {
core.setFailed(`Missing headings:\n${missing.join("\n")}`);
} else {
console.log("All required headings present");
}

View File

@@ -18,6 +18,8 @@ jobs:
name: "Lint PR"
if: github.event_name == 'pull_request'
uses: ./.github/workflows/pull-request-lint.yml
permissions:
pull-requests: write
backend-tests:
name: "Backend Server Tests"

View File

@@ -13,6 +13,10 @@ jobs:
env:
PRODUCTION: false
# Install from the committed lockfile; never re-resolve. The rolling
# `exclude-newer` cooling window (pyproject [tool.uv]) would otherwise make
# every uv command re-resolve and fail on in-window pins.
UV_FROZEN: "1"
strategy:
fail-fast: true

30
.vscode/test-block.code-snippets vendored Normal file
View File

@@ -0,0 +1,30 @@
{
"Test Block": {
"prefix": "mtest",
"body": [
"import { mount } from \"@vue/test-utils\";",
"import { describe, expect, test, vi } from \"vitest\";",
"import { makeWrapper } from \"~/tests/utils\";",
"",
"const wrapper = () => makeWrapper(() => {",
" return ${1:composable}();",
"});",
"",
"describe(\"${TM_FILENAME_BASE/(.*)\\..+$/$1/}\", () => {",
" describe(\"${2:method}\", () => {",
" test(\"It does the thing\", () => {",
" const { ${2:method} } = wrapper();",
" const result = ${2:method}();",
" expect(result).toBe(EXPECTED);",
" });",
" });",
"});",
"",
],
"description": "Insert a test block",
"scope": "typescript",
"include": [
"**/*.test.{ts,tsx,vue}"
]
}
}

View File

@@ -7,6 +7,10 @@ env:
DEFAULT_GROUP: Home
DEFAULT_HOUSEHOLD: Family
PRODUCTION: false
# Install from the committed lockfile; never re-resolve. Required because the
# rolling `exclude-newer` cooling window (pyproject [tool.uv]) would otherwise
# make every `uv run`/`uv sync` re-resolve and fail on in-window pins.
UV_FROZEN: "1"
API_PORT: 9000
API_DOCS: True
TOKEN_TIME: 256 # hours

View File

@@ -52,6 +52,11 @@ RUN apt-get update \
RUN pip install uv
# Install from the committed lockfile; never re-resolve. The rolling
# `exclude-newer` cooling window (pyproject [tool.uv]) would otherwise make
# `uv export` below re-resolve and fail on in-window pins.
ENV UV_FROZEN=1
WORKDIR /mealie
# copy project files here to ensure they will be cached.

View File

@@ -29,6 +29,7 @@
| --------------------------- | :-----: | ----------------------------------------------------------------------------------- |
| SECURITY_MAX_LOGIN_ATTEMPTS | 5 | Maximum times a user can provide an invalid password before their account is locked |
| SECURITY_USER_LOCKOUT_TIME | 24 | Time in hours for how long a users account is locked |
| ALLOWED_IFRAME_HOSTS | `""` | Comma-separated extra hostnames allowed as `<iframe>` sources in recipe content. Extends the built-in list of trusted video providers (YouTube, Vimeo). Subdomains are included automatically. Only `https` sources are permitted. Adding hosts here opts into rendering embeds from those origins to all viewers, including the public, so add only origins you trust. |
### Database

View File

@@ -0,0 +1,54 @@
<template>
<div>
<p>
To harden Mealie against malicious content, <code>&lt;iframe&gt;</code> embeds in recipe
instructions, notes, and descriptions are now restricted to a trusted set of hosts.
</p>
<div class="mb-2">
By default, embeds are allowed only from well-known video providers:
<ul class="ml-6">
<li>YouTube</li>
<li>Vimeo</li>
</ul>
</div>
<p>
Existing recipes that embed content from <strong>other</strong> hosts will no longer render
those embeds. The rest of the recipe is unaffected.
</p>
<div v-if="user?.admin">
<hr class="mt-2 mb-4">
<p>
As an admin, you can allow additional hosts with the <code>ALLOWED_IFRAME_HOSTS</code>
environment variable (comma-separated). It extends the built-in defaults, and only
<code>https</code> sources are permitted. See the configuration docs for details:
<br>
<v-btn
class="mt-2"
color="primary"
href="https://docs.mealie.io/documentation/getting-started/installation/backend-config/"
target="_blank"
>
Backend Configuration
</v-btn>
</p>
</div>
</div>
</template>
<script setup lang="ts">
import type { AnnouncementMeta } from "~/composables/use-announcements";
const { user } = useMealieAuth();
</script>
<script lang="ts">
export const meta: AnnouncementMeta = {
title: "Recipe embeds restricted to trusted hosts",
};
</script>
<style scoped lang="css">
p {
padding-bottom: 8px;
}
</style>

View File

@@ -59,9 +59,10 @@
>
<v-card-text>
{{ $t("general.confirm-delete-generic") }}
<p v-if="deleteTarget" class="mt-4 ml-4">
<p v-if="deleteTarget" class="mt-4 mb-0 font-weight-bold">
{{ deleteTarget.name || deleteTarget.title || deleteTarget.id }}
</p>
<slot name="delete-dialog-bottom" />
</v-card-text>
</BaseDialog>
@@ -88,6 +89,7 @@
</template>
</v-virtual-scroll>
</v-card>
<slot name="delete-dialog-bottom" />
</v-card-text>
</BaseDialog>
@@ -151,7 +153,7 @@ const createDialog = defineModel("createDialog", { type: Boolean, default: false
const editForm = defineModel<{ items: AutoFormItems; data: Record<string, any> }>("editForm", { required: true });
const editDialog = defineModel("editDialog", { type: Boolean, default: false });
defineProps({
const props = defineProps({
icon: {
type: String,
required: true,
@@ -185,6 +187,10 @@ defineProps({
type: String,
default: "name",
},
onDeleteDialogOpen: {
type: Function as PropType<(items: any[]) => Promise<void>>,
default: null,
},
});
// ============================================================
@@ -212,8 +218,11 @@ const editEventHandler = (item: any) => {
const deleteTarget = ref<any>(null);
const deleteDialog = ref(false);
function deleteEventHandler(item: any) {
async function deleteEventHandler(item: any) {
deleteTarget.value = item;
if (props.onDeleteDialogOpen) {
await props.onDeleteDialogOpen([item]);
}
deleteDialog.value = true;
}
@@ -222,8 +231,11 @@ function deleteEventHandler(item: any) {
const bulkDeleteTarget = ref<Array<any>>([]);
const bulkDeleteDialog = ref(false);
function bulkDeleteEventHandler(items: Array<any>) {
async function bulkDeleteEventHandler(items: Array<any>) {
bulkDeleteTarget.value = items;
if (props.onDeleteDialogOpen) {
await props.onDeleteDialogOpen(items);
}
bulkDeleteDialog.value = true;
console.log("Bulk Delete Event Handler", items);
}

View File

@@ -25,6 +25,7 @@
<script setup lang="ts">
import type { RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
import { truncateText as truncatePlainText } from "~/lib/sanitize/text";
export type UrlPrefixParam = "tags" | "categories" | "tools";
@@ -50,10 +51,7 @@ const props = withDefaults(defineProps<Props>(), {
defineEmits(["item-selected"]);
function truncateText(text: string, length = 20, clamp = "...") {
if (!props.truncate) return text;
const node = document.createElement("div");
node.innerHTML = text;
const content = node.textContent || "";
return content.length > length ? content.slice(0, length) + clamp : content;
return truncatePlainText(text, length, clamp);
}
</script>

View File

@@ -4,12 +4,8 @@
</template>
<script setup lang="ts">
import DOMPurify from "isomorphic-dompurify";
import { marked } from "marked";
enum DOMPurifyHook {
UponSanitizeAttribute = "uponSanitizeAttribute",
}
import { sanitizeMarkdownHtml } from "~/lib/sanitize/markdown";
const props = defineProps({
source: {
@@ -18,48 +14,11 @@ const props = defineProps({
},
});
const ALLOWED_STYLE_TAGS = [
"background-color", "color", "font-style", "font-weight", "text-decoration", "text-align",
];
function sanitizeMarkdown(rawHtml: string | null | undefined): string {
if (!rawHtml) {
return "";
}
DOMPurify.addHook(DOMPurifyHook.UponSanitizeAttribute, (node, data) => {
if (data.attrName === "style") {
const styles = data.attrValue.split(";").filter((style) => {
const [property] = style.split(":");
return ALLOWED_STYLE_TAGS.includes(property.trim().toLowerCase());
});
data.attrValue = styles.join(";");
}
});
const sanitized = DOMPurify.sanitize(rawHtml, {
ALLOWED_TAGS: [
"strong", "em", "b", "i", "u", "p", "code", "pre", "samp", "kbd", "var", "sub", "sup", "dfn", "cite",
"small", "address", "hr", "br", "id", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6",
"ul", "ol", "li", "dl", "dt", "dd", "abbr", "a", "img", "blockquote", "iframe",
"del", "ins", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "colgroup",
],
ALLOWED_ATTR: [
"href", "src", "alt", "height", "width", "class", "allow", "title", "allowfullscreen", "frameborder",
"scrolling", "cite", "datetime", "name", "abbr", "target", "border", "start", "style",
],
});
Object.values(DOMPurifyHook).forEach((hook) => {
DOMPurify.removeHook(hook);
});
return sanitized;
}
const { $appInfo } = useNuxtApp();
const value = computed(() => {
const rawHtml = marked.parse(props.source || "", { async: false, breaks: true });
return sanitizeMarkdown(rawHtml);
return sanitizeMarkdownHtml(rawHtml, $appInfo?.allowedIframeHosts ?? []);
});
</script>

View File

@@ -0,0 +1,83 @@
import type { IngredientFood, RecipeSummary, ShoppingListItemOut, ShoppingListMultiPurposeLabelOut, ShoppingListOut } from "~/lib/api/types/household";
export const MOCK_ITEM: ShoppingListItemOut = {
shoppingListId: "",
id: "",
groupId: "",
householdId: "",
display: "MOCK_ITEM",
updatedAt: "100",
position: 1,
checked: false,
createdAt: "100",
};
export const MOCK_RECIPE: RecipeSummary = {
id: "recipe-id",
name: "Recipe!",
};
export const MOCK_RECIPE2: RecipeSummary = {
...MOCK_RECIPE,
id: undefined,
name: "Recipe 2!",
};
export const MOCK_FOOD: IngredientFood = {
id: "1",
name: "food 1",
};
export const MOCK_FOOD2: IngredientFood = {
id: "2",
name: "food 2",
};
export const MOCK_LABEL: ShoppingListMultiPurposeLabelOut = {
shoppingListId: "",
labelId: "",
id: "",
label: {
name: "MOCK_LABEL",
groupId: "",
id: "",
},
};
export const MOCK_LABEL2: ShoppingListMultiPurposeLabelOut = {
shoppingListId: "",
labelId: "",
id: "",
label: {
name: "MOCK_LABEL2",
groupId: "",
id: "",
},
};
export const MOCK_SHOPPING_LIST: ShoppingListOut = {
groupId: "",
userId: "",
id: "",
householdId: "",
labelSettings: [
MOCK_LABEL,
MOCK_LABEL2,
],
listItems: [
MOCK_ITEM,
],
recipeReferences: [{
id: "",
shoppingListId: "",
recipeId: "",
recipeQuantity: 0,
recipe: MOCK_RECIPE,
}, {
id: "",
shoppingListId: "",
recipeId: "",
recipeQuantity: 0,
recipe: MOCK_RECIPE2,
}],
};

View File

@@ -0,0 +1,89 @@
import * as vueusecore from "@vueuse/core";
import { describe, expect, test, vi } from "vitest";
import type { ShoppingListItemOut } from "~/lib/api/types/household";
import { makeWrapper } from "~/tests/utils";
import { useShoppingListCopy } from "../use-shopping-list-copy";
import { MOCK_ITEM } from "./mocks";
vi.mock("@vueuse/core", { spy: true });
const mockCopy = vi.fn().mockImplementation(args => new Promise(resolve => resolve(args)));
vi.mocked(vueusecore.useClipboard).mockImplementation(() => {
return {
isSupported: computed(() => true),
copied: computed(() => true),
text: computed(() => ""),
copy: mockCopy,
};
});
const wrapper = () => makeWrapper(useShoppingListCopy);
const TEST_HEADER = "SPECIAL HEADER!";
const MOCK_LIST: { [key: string]: ShoppingListItemOut[] } = {
[TEST_HEADER]: [MOCK_ITEM],
[TEST_HEADER + "2"]: [MOCK_ITEM],
};
describe("Shopping list copy composable", () => {
describe("copyListItems", () => {
test("copies markdown lists correctly", () => {
const { copyListItems } = wrapper();
copyListItems(MOCK_LIST, "markdown");
const expected = [
"# SPECIAL HEADER!",
"- [ ] MOCK_ITEM",
"",
"# SPECIAL HEADER!2",
"- [ ] MOCK_ITEM",
].join("\n");
expect(mockCopy).toBeCalledWith(expected);
});
test("copies plain text lists correctly", () => {
const { copyListItems } = wrapper();
copyListItems(MOCK_LIST, "plain");
const expected = [
"[SPECIAL HEADER!]",
"MOCK_ITEM",
"",
"[SPECIAL HEADER!2]",
"MOCK_ITEM",
].join("\n");
expect(mockCopy).toBeCalledWith(expected);
});
});
describe("formatCopiedLabelHeading", () => {
test("copies markdown headers correctly", () => {
const { formatCopiedLabelHeading } = wrapper();
const header = formatCopiedLabelHeading("markdown", TEST_HEADER);
expect(header).toEqual(`# ${TEST_HEADER}`);
});
test("copies plain text headers correctly", () => {
const { formatCopiedLabelHeading } = wrapper();
const header = formatCopiedLabelHeading("plain", TEST_HEADER);
expect(header).toEqual(`[${TEST_HEADER}]`);
});
});
describe("formatCopiedListItem", () => {
test("copies markdown items correctly", () => {
const { formatCopiedListItem } = wrapper();
const header = formatCopiedListItem("markdown", MOCK_ITEM);
expect(header).toEqual(`- [ ] ${MOCK_ITEM.display}`);
});
test("copies plain text items correctly", () => {
const { formatCopiedListItem } = wrapper();
const header = formatCopiedListItem("plain", MOCK_ITEM);
expect(header).toEqual(MOCK_ITEM.display);
});
test("copies items without a display as empty", () => {
const { formatCopiedListItem } = wrapper();
const header = formatCopiedListItem("plain", { ...MOCK_ITEM, display: undefined });
expect(header).toEqual("");
});
});
});

View File

@@ -0,0 +1,175 @@
import { describe, expect, test } from "vitest";
import type { ShoppingListOut } from "~/lib/api/types/household";
import { makeWrapper } from "~/tests/utils";
import { useShoppingListSorting } from "../use-shopping-list-sorting";
import { MOCK_FOOD, MOCK_FOOD2, MOCK_ITEM, MOCK_LABEL, MOCK_LABEL2, MOCK_SHOPPING_LIST } from "./mocks";
const wrapper = () => makeWrapper(() => {
const { t } = useI18n();
return {
t,
...useShoppingListSorting(),
};
});
describe("use-shopping-list-sorting", () => {
describe("sortItems", () => {
const { sortItems } = wrapper();
test("sorts by position first", () => {
const result = sortItems(MOCK_ITEM, { ...MOCK_ITEM, position: 0 });
const result2 = sortItems({ ...MOCK_ITEM, position: 0 }, MOCK_ITEM);
expect(result).toBe(1);
expect(result2).toBe(-1);
});
test("sorts by createdAt next", () => {
const result = sortItems(MOCK_ITEM, { ...MOCK_ITEM, createdAt: "0" });
const result2 = sortItems({ ...MOCK_ITEM, createdAt: "0" }, MOCK_ITEM);
expect(result).toBe(1);
expect(result2).toBe(-1);
});
test("sorts similar items into the same spot", () => {
const result = sortItems(MOCK_ITEM, MOCK_ITEM);
expect(result).toBe(0);
});
test("handles nulls", () => {
const result = sortItems(MOCK_ITEM, { ...MOCK_ITEM, position: undefined });
const result2 = sortItems({ ...MOCK_ITEM, position: undefined }, MOCK_ITEM);
expect(result).toBe(1);
expect(result2).toBe(-1);
});
test("handles nulls", () => {
const result = sortItems(MOCK_ITEM, { ...MOCK_ITEM, createdAt: undefined });
const result2 = sortItems({ ...MOCK_ITEM, createdAt: undefined }, MOCK_ITEM);
expect(result).toBe(1);
expect(result2).toBe(-1);
});
});
describe("sortListItems", () => {
const { sortListItems } = wrapper();
test("sorts by position first", () => {
const sortedList = { ...MOCK_SHOPPING_LIST, listItems: [MOCK_ITEM, { ...MOCK_ITEM, position: 0 }, { ...MOCK_ITEM, createdAt: "0" }] };
sortListItems(sortedList);
expect(sortedList.listItems).toEqual([
{ ...MOCK_ITEM, position: 0 },
{ ...MOCK_ITEM, createdAt: "0" },
MOCK_ITEM,
]);
});
test("handles nulls", () => {
const sortedList = { ...MOCK_SHOPPING_LIST, listItems: undefined };
sortListItems(sortedList);
expect(sortedList.listItems).toEqual(undefined);
});
});
describe("updateItemsByLabel", () => {
const { updateItemsByLabel, t } = wrapper();
test("sorts by group", () => {
const sortedList = {
...MOCK_SHOPPING_LIST, listItems: [
MOCK_ITEM,
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
],
};
const result = updateItemsByLabel(sortedList);
expect(result).toEqual({
[t("shopping-list.no-label")]: [
MOCK_ITEM,
],
[MOCK_LABEL.label.name]: [
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
],
[MOCK_LABEL2.label.name]: [
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
],
});
});
test("ignores checked items", () => {
const sortedList = {
...MOCK_SHOPPING_LIST, listItems: [
MOCK_ITEM,
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1", checked: true },
],
};
const result = updateItemsByLabel(sortedList);
expect(result).toEqual({
[t("shopping-list.no-label")]: [
MOCK_ITEM,
],
[MOCK_LABEL.label.name]: [
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
],
[MOCK_LABEL2.label.name]: [
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
],
});
});
test("returns unordered labels if no ordering is specified", () => {
const sortedList = {
...MOCK_SHOPPING_LIST,
labelSettings: undefined,
listItems: [
MOCK_ITEM,
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1", checked: true },
],
};
const result = updateItemsByLabel(sortedList);
expect(result).toEqual({
[t("shopping-list.no-label")]: [
MOCK_ITEM,
],
[MOCK_LABEL2.label.name]: [
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
],
[MOCK_LABEL.label.name]: [
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
],
});
});
});
describe("groupAndSortListItemsByFood", () => {
const { groupAndSortListItemsByFood } = wrapper();
test("sorts by group", () => {
const sortedList = { ...MOCK_SHOPPING_LIST };
groupAndSortListItemsByFood(sortedList);
expect(sortedList.listItems).toEqual(MOCK_SHOPPING_LIST.listItems);
});
test("groups checked items together", () => {
const sortedList: ShoppingListOut = {
...MOCK_SHOPPING_LIST, listItems: [
{ ...MOCK_ITEM, checked: true, food: MOCK_FOOD },
{ ...MOCK_ITEM, checked: true, food: MOCK_FOOD2 },
],
};
groupAndSortListItemsByFood(sortedList);
expect(sortedList.listItems).toEqual([
{ ...MOCK_ITEM, checked: true, food: MOCK_FOOD },
{ ...MOCK_ITEM, checked: true, food: MOCK_FOOD2, position: 1 },
]);
});
test("populates position and created at if not present", () => {
const sortedList: ShoppingListOut = {
...MOCK_SHOPPING_LIST, listItems: [
{ ...MOCK_ITEM, food: MOCK_FOOD, position: undefined },
{ ...MOCK_ITEM, food: MOCK_FOOD2, createdAt: undefined },
],
};
groupAndSortListItemsByFood(sortedList);
expect(sortedList.listItems).toEqual([
{ ...MOCK_ITEM, food: MOCK_FOOD2, createdAt: undefined },
{ ...MOCK_ITEM, food: MOCK_FOOD, position: 1 },
]);
});
test("handles nulls", () => {
const sortedList: ShoppingListOut = { ...MOCK_SHOPPING_LIST, listItems: undefined };
groupAndSortListItemsByFood(sortedList);
expect(sortedList.listItems).toEqual(undefined);
});
});
});

View File

@@ -0,0 +1,63 @@
import { describe, expect, test } from "vitest";
import type { ShoppingListOut } from "~/lib/api/types/household";
import { makeWrapper } from "~/tests/utils";
import { useShoppingListState } from "../use-shopping-list-state";
import { MOCK_ITEM, MOCK_RECIPE, MOCK_RECIPE2, MOCK_SHOPPING_LIST } from "./mocks";
const wrapper = (list: ShoppingListOut = MOCK_SHOPPING_LIST) => makeWrapper(() => {
const { shoppingList, ...state } = useShoppingListState();
shoppingList.value = list;
return {
shoppingList,
...state,
};
});
describe("use-shopping-list-state", () => {
describe("checked items are sorted", () => {
const { sortCheckedItems } = wrapper();
test("by timestamp", () => {
const sorted = sortCheckedItems(MOCK_ITEM, { ...MOCK_ITEM, updatedAt: "200" });
const sorted2 = sortCheckedItems(MOCK_ITEM, { ...MOCK_ITEM, updatedAt: "0" });
expect(sorted).toBe(1);
expect(sorted2).toBe(-1);
});
test("by position if timestamps match", () => {
const sorted = sortCheckedItems(MOCK_ITEM, { ...MOCK_ITEM, position: 2 });
const sorted2 = sortCheckedItems(MOCK_ITEM, { ...MOCK_ITEM, position: 0 });
const sorted3 = sortCheckedItems({ ...MOCK_ITEM, position: undefined }, { ...MOCK_ITEM, position: undefined });
expect(sorted).toBe(1);
expect(sorted2).toBe(-1);
expect(sorted3).toBe(1);
});
});
describe("recipeMap", () => {
test("Updates to match shopping list recipe references", () => {
const { recipeMap } = wrapper();
expect(recipeMap).toEqual(new Map([
[MOCK_RECIPE.id, MOCK_RECIPE],
["", MOCK_RECIPE2],
]));
});
test("handles nulls", () => {
const { recipeMap } = wrapper({ ...MOCK_SHOPPING_LIST, recipeReferences: undefined });
expect(recipeMap).toEqual(new Map([]));
});
});
describe("checked and unchecked items", () => {
test("update appropriately", () => {
const mockCheckedItem = { ...MOCK_ITEM, checked: true };
const { listItems: { checked, unchecked } } = wrapper({
...MOCK_SHOPPING_LIST, listItems: [
MOCK_ITEM,
mockCheckedItem,
],
});
expect(unchecked[0]).toEqual(MOCK_ITEM);
expect(checked[0]).toEqual(mockCheckedItem);
});
});
});

View File

@@ -462,7 +462,7 @@
"mealie-text": "Mealie can import recipes from the Mealie application from a pre v1.0 release. Export your recipes from your old instance, and upload the zip file below. Note that only recipes can be imported from the export.",
"plantoeat": {
"title": "Plan to Eat",
"description-long": "Mealie can import recipies from Plan to Eat."
"description-long": "Mealie can import recipes from Plan to Eat. Upload a ZIP archive, CSV, or TXT file exported from Plan to Eat."
},
"myrecipebox": {
"title": "My Recipe Box",
@@ -1144,6 +1144,8 @@
},
"data-pages": {
"foods": {
"delete-affects-recipes": "Warning: this food is used in {count} recipe(s). Deleting it will leave an empty ingredient in the recipe(s).",
"delete-affects-recipes-more": "View all {count} recipes",
"merge-dialog-text": "Combining the selected foods will merge the source food and target food into a single food. The source food will be deleted and all of the references to the source food will be updated to point to the target food.",
"merge-food-example": "Merging {food1} into {food2}",
"seed-dialog-text": "Seed the database with foods based on your local language. This will create ~2700 common foods that can be used to organize your database. Foods are translated via a community effort.",

View File

@@ -48,6 +48,7 @@ export interface AppInfo {
oidcRedirect: boolean;
oidcProviderName: string;
tokenTime: number;
allowedIframeHosts?: string[];
}
export interface AppStartupInfo {
isFirstLogin: boolean;

View File

@@ -0,0 +1,74 @@
import { describe, expect, test } from "vitest";
import { sanitizeMarkdownHtml } from "./markdown";
describe("sanitizeMarkdownHtml", () => {
test("returns empty string for nullish input", () => {
expect(sanitizeMarkdownHtml(null)).toEqual("");
expect(sanitizeMarkdownHtml(undefined)).toEqual("");
expect(sanitizeMarkdownHtml("")).toEqual("");
});
test("keeps allowed formatting tags", () => {
const html = sanitizeMarkdownHtml("<p>Mix <strong>flour</strong> and <em>water</em></p>");
expect(html).toContain("<strong>flour</strong>");
expect(html).toContain("<em>water</em>");
});
test("strips script tags and event handlers", () => {
const html = sanitizeMarkdownHtml("<p onclick=\"alert(1)\">hi</p><script>alert(1)</script>");
expect(html).not.toContain("script");
expect(html).not.toContain("onclick");
expect(html).not.toContain("alert");
});
test("strips img onerror payloads", () => {
const html = sanitizeMarkdownHtml("<img src=x onerror=alert(1)>");
expect(html).not.toContain("onerror");
});
// Form controls must never render in user content.
test("strips form, input, and button elements", () => {
const html = sanitizeMarkdownHtml("<form action=/x><input name=p><button>go</button></form>");
expect(html).not.toContain("<form");
expect(html).not.toContain("<input");
expect(html).not.toContain("<button");
});
test("strips iframes when no allowed hosts are configured", () => {
const html = sanitizeMarkdownHtml("<iframe src=\"https://evil.example/x\"></iframe>", []);
expect(html).not.toContain("<iframe");
});
test("strips iframes whose src host is not allowlisted", () => {
const html = sanitizeMarkdownHtml(
"<iframe src=\"https://evil.example/x\"></iframe>",
["youtube.com"],
);
expect(html).not.toContain("<iframe");
});
test("strips non-https iframes even for an allowlisted host", () => {
const html = sanitizeMarkdownHtml(
"<iframe src=\"http://www.youtube.com/embed/abc\"></iframe>",
["youtube.com"],
);
expect(html).not.toContain("<iframe");
});
test("keeps iframes from an allowlisted host (incl. subdomains)", () => {
const html = sanitizeMarkdownHtml(
"<iframe src=\"https://www.youtube.com/embed/abc\"></iframe>",
["youtube.com"],
);
expect(html).toContain("<iframe");
expect(html).toContain("https://www.youtube.com/embed/abc");
});
test("does not allow a lookalike host to pass the suffix check", () => {
const html = sanitizeMarkdownHtml(
"<iframe src=\"https://notyoutube.com/embed/abc\"></iframe>",
["youtube.com"],
);
expect(html).not.toContain("<iframe");
});
});

View File

@@ -0,0 +1,98 @@
import DOMPurify from "isomorphic-dompurify";
enum DOMPurifyHook {
UponSanitizeAttribute = "uponSanitizeAttribute",
AfterSanitizeAttributes = "afterSanitizeAttributes",
}
const ALLOWED_STYLE_PROPERTIES = [
"background-color", "color", "font-style", "font-weight", "text-decoration", "text-align",
];
const BASE_ALLOWED_TAGS = [
"strong", "em", "b", "i", "u", "p", "code", "pre", "samp", "kbd", "var", "sub", "sup", "dfn", "cite",
"small", "address", "hr", "br", "id", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6",
"ul", "ol", "li", "dl", "dt", "dd", "abbr", "a", "img", "blockquote",
"del", "ins", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "colgroup",
];
const BASE_ALLOWED_ATTR = [
"href", "src", "alt", "height", "width", "class", "title",
"cite", "datetime", "name", "abbr", "target", "border", "start", "style",
];
// Attributes only meaningful on an <iframe>; added to the allowlist solely when iframe embeds
// are enabled via a configured host allowlist.
const IFRAME_ALLOWED_ATTR = ["allow", "allowfullscreen", "frameborder", "scrolling"];
/**
* Returns true if an iframe `src` points at one of the allowed hosts. Only https URLs are
* accepted, and a configured host matches the URL's hostname exactly or as a parent domain
* (e.g. "youtube.com" matches "www.youtube.com").
*/
function isAllowedIframeSrc(src: string, allowedHosts: string[]): boolean {
let url: URL;
try {
url = new URL(src);
}
catch {
return false;
}
if (url.protocol !== "https:") {
return false;
}
const hostname = url.hostname.toLowerCase();
return allowedHosts.some((host) => {
const allowed = host.toLowerCase();
return hostname === allowed || hostname.endsWith(`.${allowed}`);
});
}
/**
* Sanitizes pre-rendered HTML (from markdown) for display in user content such as recipe
* instructions, notes, and descriptions.
*
* Only the tags in `BASE_ALLOWED_TAGS` and attributes in `BASE_ALLOWED_ATTR` survive; everything
* else (scripts, event handlers, form controls, ...) is dropped. `style` attributes are filtered
* down to the properties in `ALLOWED_STYLE_PROPERTIES`. `<iframe>` is only kept when
* `allowedIframeHosts` is non-empty, and even then any iframe whose `src` is not an https URL on
* the host allowlist is removed.
*/
export function sanitizeMarkdownHtml(rawHtml: string | null | undefined, allowedIframeHosts: string[] = []): string {
if (!rawHtml) {
return "";
}
const allowIframe = allowedIframeHosts.length > 0;
DOMPurify.addHook(DOMPurifyHook.UponSanitizeAttribute, (_node, data) => {
if (data.attrName === "style") {
const styles = data.attrValue.split(";").filter((style) => {
const [property] = style.split(":");
return ALLOWED_STYLE_PROPERTIES.includes(property.trim().toLowerCase());
});
data.attrValue = styles.join(";");
}
});
if (allowIframe) {
DOMPurify.addHook(DOMPurifyHook.AfterSanitizeAttributes, (node) => {
if (node.nodeName === "IFRAME" && !isAllowedIframeSrc(node.getAttribute("src") || "", allowedIframeHosts)) {
node.parentNode?.removeChild(node);
}
});
}
const sanitized = DOMPurify.sanitize(rawHtml, {
ALLOWED_TAGS: allowIframe ? [...BASE_ALLOWED_TAGS, "iframe"] : BASE_ALLOWED_TAGS,
ALLOWED_ATTR: allowIframe ? [...BASE_ALLOWED_ATTR, ...IFRAME_ALLOWED_ATTR] : BASE_ALLOWED_ATTR,
});
Object.values(DOMPurifyHook).forEach((hook) => {
DOMPurify.removeHook(hook);
});
return sanitized;
}

View File

@@ -0,0 +1,35 @@
import { describe, expect, test, vi } from "vitest";
import { truncateText } from "./text";
describe("truncateText", () => {
test("returns short text unchanged", () => {
expect(truncateText("Dinner")).toEqual("Dinner");
});
test("truncates long text with clamp", () => {
expect(truncateText("a".repeat(25))).toEqual(`${"a".repeat(20)}...`);
});
test("respects custom length and clamp", () => {
expect(truncateText("abcdef", 3, "~")).toEqual("abc~");
});
test("does not clamp text exactly at the length boundary", () => {
expect(truncateText("abcde", 5)).toEqual("abcde");
expect(truncateText("abcdef", 5)).toEqual("abcde...");
});
// Markup in the input must be treated as plain text and never parsed into the live document.
test("does not parse or execute HTML payloads", () => {
const createElement = vi.spyOn(document, "createElement");
const payload = "<img src=x onerror=alert(1)>";
const result = truncateText(payload);
// The payload is returned verbatim (truncated only by length), proving it is treated as text.
expect(result).toEqual(`${payload.slice(0, 20)}...`);
// No DOM element is constructed, so no <img> can fire its onerror handler.
expect(createElement).not.toHaveBeenCalled();
createElement.mockRestore();
});
});

View File

@@ -0,0 +1,9 @@
/**
* Truncates plain text to `length` characters, appending `clamp` when truncated.
*
* The input is treated strictly as text and is never parsed as HTML, so markup in the input is
* returned verbatim rather than interpreted.
*/
export function truncateText(text: string, length = 20, clamp = "..."): string {
return text.length > length ? text.slice(0, length) + clamp : text;
}

View File

@@ -69,11 +69,7 @@
</template>
</v-autocomplete>
<v-alert
v-if="foods && foods.length > 0"
type="error"
class="mb-0 text-body-2"
>
<v-alert v-if="foods && foods.length > 0" type="error" class="mb-0 text-body-2">
{{ $t("data-pages.foods.seed-dialog-warning") }}
</v-alert>
</v-card-text>
@@ -112,11 +108,7 @@
:label="$t('data-pages.foods.food-label')"
/>
<v-card variant="outlined">
<v-virtual-scroll
height="400"
item-height="25"
:items="bulkAssignTarget"
>
<v-virtual-scroll height="400" item-height="25" :items="bulkAssignTarget">
<template #default="{ item }">
<v-list-item class="pb-2">
<v-list-item-title>{{ item.name }}</v-list-item-title>
@@ -141,6 +133,7 @@
]"
:create-form="createForm"
:edit-form="editForm"
:on-delete-dialog-open="onDeleteDialogOpen"
@create-one="handleCreate"
@edit-one="handleEdit"
@delete-one="foodStore.actions.deleteOne"
@@ -151,15 +144,12 @@
<template #icon>
{{ $globals.icons.externalLink }}
</template>
{{ $t('data-pages.combine') }}
{{ $t("data-pages.combine") }}
</BaseButton>
</template>
<template #[`item.label`]="{ item }">
<MultiPurposeLabel
v-if="item.label"
:label="item.label"
>
<MultiPurposeLabel v-if="item.label" :label="item.label">
{{ item.label.name }}
</MultiPurposeLabel>
</template>
@@ -171,7 +161,7 @@
</template>
<template #[`item.createdAt`]="{ item }">
{{ item.createdAt ? $d(new Date(item.createdAt)) : '' }}
{{ item.createdAt ? $d(new Date(item.createdAt)) : "" }}
</template>
<template #table-button-bottom>
@@ -179,18 +169,33 @@
<template #icon>
{{ $globals.icons.database }}
</template>
{{ $t('data-pages.seed') }}
{{ $t("data-pages.seed") }}
</BaseButton>
</template>
<template #edit-dialog-custom-action>
<BaseButton
edit
@click="aliasManagerDialog = true"
>
{{ $t('data-pages.manage-aliases') }}
<BaseButton edit @click="aliasManagerDialog = true">
{{ $t("data-pages.manage-aliases") }}
</BaseButton>
</template>
<template #delete-dialog-bottom>
<v-alert v-if="affectedRecipes.length > 0" type="warning" density="compact" class="mt-4 mb-0">
{{ $t("data-pages.foods.delete-affects-recipes", { count: affectedRecipesTotal }) }}
<ul class="mt-1 pl-5 mb-0">
<li v-for="recipe in affectedRecipes.slice(0, 5)" :key="recipe.slug">
<NuxtLink :to="recipe.url" class="text-white">{{ recipe.name }}</NuxtLink>
</li>
</ul>
<NuxtLink
v-if="affectedRecipesTotal > 5"
:to="affectedRecipesMoreLink"
class="text-white d-inline-block mt-1"
>
{{ $t("data-pages.foods.delete-affects-recipes-more", { count: affectedRecipesTotal }) }}
</NuxtLink>
</v-alert>
</template>
</GroupDataPage>
</div>
</template>
@@ -218,6 +223,7 @@ interface CreateIngredientFoodWithOnHand extends CreateIngredientFood {
interface IngredientFoodWithOnHand extends IngredientFood {
onHand: boolean;
}
const userApi = useUserApi();
const i18n = useI18n();
const auth = useMealieAuth();
@@ -274,11 +280,14 @@ const tableHeaders: TableHeaders[] = [
];
const userHousehold = computed(() => auth.user.value?.householdSlug || "");
const userGroup = computed(() => auth.user.value?.groupSlug || "");
const foodStore = useFoodStore();
const foods = computed(() => foodStore.store.value.map((food) => {
const onHand = food.householdsWithIngredientFood?.includes(userHousehold.value) || false;
return { ...food, onHand } as IngredientFoodWithOnHand;
}));
const foods = computed(() =>
foodStore.store.value.map((food) => {
const onHand = food.householdsWithIngredientFood?.includes(userHousehold.value) || false;
return { ...food, onHand } as IngredientFoodWithOnHand;
}),
);
// ============================================================
// Labels
@@ -383,6 +392,9 @@ async function handleBulkAction(event: string, items: IngredientFoodWithOnHand[]
if (event === "delete-selected") {
const ids = items.map(item => item.id);
await foodStore.actions.deleteMany(ids);
affectedRecipes.value = [];
affectedRecipesTotal.value = 0;
affectedRecipesMoreLink.value = "";
}
else if (event === "assign-selected") {
bulkAssignEventHandler(items);
@@ -401,6 +413,26 @@ function updateFoodAlias(newAliases: IngredientFoodAlias[]) {
aliasManagerDialog.value = false;
}
// ============================================================
// Delete Foods
// fetch affected recipes before confirming deletion
const affectedRecipes = ref<{ name: string; slug: string; url: string }[]>([]);
const affectedRecipesTotal = ref(0);
const affectedRecipesMoreLink = ref("");
async function onDeleteDialogOpen(items: IngredientFoodWithOnHand[]) {
const ids = items.map(item => item.id);
const { data } = await userApi.recipes.search({ foods: ids, perPage: 5 });
affectedRecipes.value = (data?.items ?? []).map(r => ({
name: r.name ?? "",
slug: r.slug ?? "",
url: `/g/${userGroup.value}/r/${r.slug}`,
}));
affectedRecipesTotal.value = data?.total ?? 0;
affectedRecipesMoreLink.value = `/g/${userGroup.value}?${ids.map(id => `foods=${id}`).join("&")}`;
}
// ============================================================
// Merge Foods

View File

@@ -337,16 +337,8 @@ const _content: Record<string, MigrationContent> = {
},
[MIGRATIONS.plantoeat]: {
text: i18n.t("migration.plantoeat.description-long"),
acceptedFileType: ".zip",
tree: [
{
icon: $globals.icons.zip,
title: "plantoeat-recipes-508318_10-13-2023.zip",
children: [
{ title: "plantoeat-recipes-508318_10-13-2023.csv", icon: $globals.icons.codeJson },
],
},
],
acceptedFileType: ".zip,.csv,.txt",
tree: false,
},
[MIGRATIONS.recipekeeper]: {
text: i18n.t("migration.recipekeeper.description-long"),

View File

@@ -0,0 +1,18 @@
import { config } from "@vue/test-utils";
import { createI18n } from "vue-i18n";
function loadEnLocales() {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require("../lang/messages/en-US.json") as Record<string, string>;
}
const i18n = createI18n({
locale: "en-US",
messages: {
"en-US": loadEnLocales(),
},
});
config.global.plugins = [...(config.global.plugins ?? []), i18n];
export { i18n };

View File

@@ -1,3 +1,4 @@
import { mount } from "@vue/test-utils";
import { createI18n } from "vue-i18n";
function loadEnLocales() {
@@ -14,3 +15,12 @@ export function stubI18n() {
});
return i18n.global;
}
export const makeWrapper = <T>(setup: () => T) => {
const Wrapper = {
template: "<div />",
setup,
};
const { vm } = mount(Wrapper);
return vm as unknown as ReturnType<typeof Wrapper.setup>;
};

View File

@@ -12,6 +12,8 @@
"lint:log": "yarn lint:js --debug",
"test": "vitest",
"test:ci": "vitest --watch=false",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui",
"cleanup": "nuxt cleanup"
},
"lint-staged": {
@@ -48,6 +50,9 @@
"@types/node": "^25.5.2",
"@types/sortablejs": "^1.15.8",
"@vitejs/plugin-vue": "^6.0.7",
"@vitest/coverage-v8": "3.2.4",
"@vitest/ui": "3.2.4",
"@vue/test-utils": "^2.4.6",
"eslint": "^9.22.0",
"eslint-config-prettier": "^10.0.2",
"eslint-plugin-format": "^1.0.1",
@@ -57,8 +62,9 @@
"prettier": "^3.5.2",
"sass-embedded": "^1.85.1",
"typescript": "^5.3",
"unplugin-auto-import": "^21.0.0",
"vite-plugin-commonjs": "^0.10.4",
"vitest": "^3.0.7"
"vitest": "^4.0.0"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"resolutions": {

View File

@@ -1,11 +1,26 @@
import path from "path";
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
export default {
plugins: [vue()],
plugins: [
vue(),
AutoImport({
imports: ["vue", "@vueuse/core", "vue-i18n"],
dts: false,
}),
],
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./app/tests/setup.ts"],
coverage: {
provider: "v8",
include: ["app/{lib,components,composables,layouts,pages}/**/*.{ts,tsx,vue}"],
exclude: ["**/*.test.*", "node_modules/**", "dist/**", "coverage/**", "**/__tests__/**"],
reporter: ["html", "text-summary"],
all: true,
},
},
resolve: {
alias: {

View File

@@ -2,6 +2,14 @@
# yarn lockfile v1
"@ampproject/remapping@^2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4"
integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==
dependencies:
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24"
"@antfu/install-pkg@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@antfu/install-pkg/-/install-pkg-1.1.0.tgz#78fa036be1a6081b5a77a5cf59f50c7752b6ba26"
@@ -281,7 +289,7 @@
"@babel/template" "^7.29.7"
"@babel/types" "^7.29.7"
"@babel/parser@^7.24.6", "@babel/parser@^7.25.3", "@babel/parser@^7.27.0", "@babel/parser@^7.28.4", "@babel/parser@^7.28.5", "@babel/parser@^7.29.3", "@babel/parser@^7.29.7":
"@babel/parser@^7.24.6", "@babel/parser@^7.25.3", "@babel/parser@^7.25.4", "@babel/parser@^7.27.0", "@babel/parser@^7.28.4", "@babel/parser@^7.28.5", "@babel/parser@^7.29.3", "@babel/parser@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.7.tgz#837b87387cbf5ec5530cb634b3c622f68edb9334"
integrity sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==
@@ -904,7 +912,7 @@
"@babel/types" "^7.29.7"
debug "^4.3.1"
"@babel/types@^7.26.8", "@babel/types@^7.28.4", "@babel/types@^7.29.0", "@babel/types@^7.29.7", "@babel/types@^7.4.4":
"@babel/types@^7.25.4", "@babel/types@^7.26.8", "@babel/types@^7.28.4", "@babel/types@^7.29.0", "@babel/types@^7.29.7", "@babel/types@^7.4.4":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.7.tgz#8005e31d82712ee7adaef6e23c63b71a62770a92"
integrity sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==
@@ -920,6 +928,11 @@
"@babel/helper-string-parser" "^8.0.0-rc.6"
"@babel/helper-validator-identifier" "^8.0.0-rc.6"
"@bcoe/v8-coverage@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz#bbe12dca5b4ef983a0d0af4b07b9bc90ea0ababa"
integrity sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==
"@bomb.sh/tab@^0.0.15":
version "0.0.15"
resolved "https://registry.yarnpkg.com/@bomb.sh/tab/-/tab-0.0.15.tgz#678fd24b3b3ab7e9e426cf541b3ee2cf8fb661ec"
@@ -1852,6 +1865,11 @@
dependencies:
minipass "^7.0.4"
"@istanbuljs/schema@^0.1.2":
version "0.1.6"
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.6.tgz#8dc9afa2ac1506cb1a58f89940f1c124446c8df3"
integrity sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==
"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5":
version "0.3.13"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f"
@@ -1886,7 +1904,7 @@
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28", "@jridgewell/trace-mapping@^0.3.31":
"@jridgewell/trace-mapping@^0.3.23", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28", "@jridgewell/trace-mapping@^0.3.31":
version "0.3.31"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0"
integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==
@@ -2377,6 +2395,11 @@
vue-i18n "^10.0.7"
vue-router "^4.5.1"
"@one-ini/wasm@0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@one-ini/wasm/-/wasm-0.1.1.tgz#6013659736c9dbfccc96e8a9c2b3de317df39323"
integrity sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==
"@oxc-minify/binding-android-arm-eabi@0.131.0":
version "0.131.0"
resolved "https://registry.yarnpkg.com/@oxc-minify/binding-android-arm-eabi/-/binding-android-arm-eabi-0.131.0.tgz#031cab3588c2b31d124699d9de3a71d4022e9ee1"
@@ -2664,6 +2687,11 @@
dependencies:
"@oxc-project/types" "^0.60.0"
"@oxc-project/types@=0.133.0":
version "0.133.0"
resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.133.0.tgz#2e282ef9e1d26e06b68ccd14b73f310a3b2cf7f8"
integrity sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==
"@oxc-project/types@^0.131.0":
version "0.131.0"
resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.131.0.tgz#be912efd221a600fdc0f7e127ef1248b9b7beb0a"
@@ -3022,7 +3050,86 @@
resolved "https://registry.yarnpkg.com/@replit/codemirror-indentation-markers/-/codemirror-indentation-markers-6.5.3.tgz#1cfe5c557c45dd7f2988cbee278f17607010591b"
integrity sha512-hL5Sfvw3C1vgg7GolLe/uxX5T3tmgOA3ZzqlMv47zjU1ON51pzNWiVbS22oh6crYhtVhv8b3gdXwoYp++2ilHw==
"@rolldown/pluginutils@^1.0.0-rc.2", "@rolldown/pluginutils@^1.0.1":
"@rolldown/binding-android-arm64@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz#54ce8f8382213f4a314a0c2f7ba83f81ffeae592"
integrity sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==
"@rolldown/binding-darwin-arm64@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz#388fca1566c14c00c4b446fc3928630e7f0d95fc"
integrity sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==
"@rolldown/binding-darwin-x64@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz#53f57de1f599ecf1db13823cfc88c18fb80954ad"
integrity sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==
"@rolldown/binding-freebsd-x64@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz#6f3fdda1b7aeaac9d268a526804b4fb96e4e35f1"
integrity sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==
"@rolldown/binding-linux-arm-gnueabihf@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz#d87a454bf585cc9676849377e91d6e375297326f"
integrity sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==
"@rolldown/binding-linux-arm64-gnu@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz#419fd6bf612cf348f10528cbcd94ebab9607d8d1"
integrity sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==
"@rolldown/binding-linux-arm64-musl@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz#fcc6918696bb76844877e1e4930a18fd0d374069"
integrity sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==
"@rolldown/binding-linux-ppc64-gnu@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz#32aecb7c8dae5d4f2a8cde57a058ec86991542f8"
integrity sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==
"@rolldown/binding-linux-s390x-gnu@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz#bed9346ea81e6bb8b93cf11f5d88b77db890b763"
integrity sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==
"@rolldown/binding-linux-x64-gnu@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz#64c2d26f75dffd9b5a1f97557a00ae77250c8cb7"
integrity sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==
"@rolldown/binding-linux-x64-musl@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz#5a45132e8a47659eeaaf3b540c2954a97c860ff3"
integrity sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==
"@rolldown/binding-openharmony-arm64@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz#290513068c55e849dc8457a32afee1d7b0acb309"
integrity sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==
"@rolldown/binding-wasm32-wasi@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz#3d9972dbf1a953d3c7afaa4a0f20ef2b2e39f31b"
integrity sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==
dependencies:
"@emnapi/core" "1.10.0"
"@emnapi/runtime" "1.10.0"
"@napi-rs/wasm-runtime" "^1.1.4"
"@rolldown/binding-win32-arm64-msvc@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz#a004ab607a16d6f03bcb555728ff888af75773ad"
integrity sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==
"@rolldown/binding-win32-x64-msvc@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz#e2a25b34691a1cc8a1209d7de709063026dd0cdb"
integrity sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==
"@rolldown/pluginutils@^1.0.0", "@rolldown/pluginutils@^1.0.0-rc.2", "@rolldown/pluginutils@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz#e3fcee093fbb5ce765e1ad088ff4de2889f6f9be"
integrity sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==
@@ -3277,6 +3384,11 @@
resolved "https://registry.yarnpkg.com/@sphinxxxx/color-conversion/-/color-conversion-2.2.2.tgz#03ecc29279e3c0c832f6185a5bfa3497858ac8ca"
integrity sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw==
"@standard-schema/spec@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8"
integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==
"@stylistic/eslint-plugin@^5.4.0", "@stylistic/eslint-plugin@^5.9.0":
version "5.10.0"
resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz#471bbd9f7a27ceaac4a217e7f5b3890855e5640c"
@@ -3647,57 +3759,95 @@
dependencies:
"@rolldown/pluginutils" "^1.0.1"
"@vitest/expect@3.2.4":
"@vitest/coverage-v8@3.2.4":
version "3.2.4"
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-3.2.4.tgz#8362124cd811a5ee11c5768207b9df53d34f2433"
integrity sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==
resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz#a2d8d040288c1956a1c7d0a0e2cdcfc7a3319f13"
integrity sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==
dependencies:
"@types/chai" "^5.2.2"
"@vitest/spy" "3.2.4"
"@vitest/utils" "3.2.4"
chai "^5.2.0"
"@ampproject/remapping" "^2.3.0"
"@bcoe/v8-coverage" "^1.0.2"
ast-v8-to-istanbul "^0.3.3"
debug "^4.4.1"
istanbul-lib-coverage "^3.2.2"
istanbul-lib-report "^3.0.1"
istanbul-lib-source-maps "^5.0.6"
istanbul-reports "^3.1.7"
magic-string "^0.30.17"
magicast "^0.3.5"
std-env "^3.9.0"
test-exclude "^7.0.1"
tinyrainbow "^2.0.0"
"@vitest/mocker@3.2.4":
version "3.2.4"
resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-3.2.4.tgz#4471c4efbd62db0d4fa203e65cc6b058a85cabd3"
integrity sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==
"@vitest/expect@4.1.8":
version "4.1.8"
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-4.1.8.tgz#45154f1f8559f55c5281eb0dcb1ac37b581a87d8"
integrity sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==
dependencies:
"@vitest/spy" "3.2.4"
estree-walker "^3.0.3"
magic-string "^0.30.17"
"@standard-schema/spec" "^1.1.0"
"@types/chai" "^5.2.2"
"@vitest/spy" "4.1.8"
"@vitest/utils" "4.1.8"
chai "^6.2.2"
tinyrainbow "^3.1.0"
"@vitest/pretty-format@3.2.4", "@vitest/pretty-format@^3.2.4":
"@vitest/mocker@4.1.8":
version "4.1.8"
resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-4.1.8.tgz#d006bfc5894a1af51e74deddef2535d6bd436b16"
integrity sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==
dependencies:
"@vitest/spy" "4.1.8"
estree-walker "^3.0.3"
magic-string "^0.30.21"
"@vitest/pretty-format@3.2.4":
version "3.2.4"
resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-3.2.4.tgz#3c102f79e82b204a26c7a5921bf47d534919d3b4"
integrity sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==
dependencies:
tinyrainbow "^2.0.0"
"@vitest/runner@3.2.4":
"@vitest/pretty-format@4.1.8":
version "4.1.8"
resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.1.8.tgz#d9d2e248b900d7ad9556c4374fcdf1871c615193"
integrity sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==
dependencies:
tinyrainbow "^3.1.0"
"@vitest/runner@4.1.8":
version "4.1.8"
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-4.1.8.tgz#4631808f3996359b74ccc3ca262990e14c295d50"
integrity sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==
dependencies:
"@vitest/utils" "4.1.8"
pathe "^2.0.3"
"@vitest/snapshot@4.1.8":
version "4.1.8"
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-4.1.8.tgz#37470135d64ea11bb2a839b1c6b7f5de7018f6ee"
integrity sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==
dependencies:
"@vitest/pretty-format" "4.1.8"
"@vitest/utils" "4.1.8"
magic-string "^0.30.21"
pathe "^2.0.3"
"@vitest/spy@4.1.8":
version "4.1.8"
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-4.1.8.tgz#3abfe9301d25c39f808dcaa9f10fec0dd370e564"
integrity sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==
"@vitest/ui@3.2.4":
version "3.2.4"
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-3.2.4.tgz#5ce0274f24a971f6500f6fc166d53d8382430766"
integrity sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==
resolved "https://registry.yarnpkg.com/@vitest/ui/-/ui-3.2.4.tgz#df8080537c1dcfeae353b2d3cb3301d9acafe04a"
integrity sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==
dependencies:
"@vitest/utils" "3.2.4"
fflate "^0.8.2"
flatted "^3.3.3"
pathe "^2.0.3"
strip-literal "^3.0.0"
"@vitest/snapshot@3.2.4":
version "3.2.4"
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-3.2.4.tgz#40a8bc0346ac0aee923c0eefc2dc005d90bc987c"
integrity sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==
dependencies:
"@vitest/pretty-format" "3.2.4"
magic-string "^0.30.17"
pathe "^2.0.3"
"@vitest/spy@3.2.4":
version "3.2.4"
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-3.2.4.tgz#cc18f26f40f3f028da6620046881f4e4518c2599"
integrity sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==
dependencies:
tinyspy "^4.0.3"
sirv "^3.0.1"
tinyglobby "^0.2.14"
tinyrainbow "^2.0.0"
"@vitest/utils@3.2.4":
version "3.2.4"
@@ -3708,6 +3858,15 @@
loupe "^3.1.4"
tinyrainbow "^2.0.0"
"@vitest/utils@4.1.8":
version "4.1.8"
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.1.8.tgz#099ea5255cec08735410cf707edaba2c158c5ad9"
integrity sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==
dependencies:
"@vitest/pretty-format" "4.1.8"
convert-source-map "^2.0.0"
tinyrainbow "^3.1.0"
"@vue-macros/common@^1.16.1":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@vue-macros/common/-/common-1.16.1.tgz#dac7ebc57ded4d6fb19d7f9a83d2973971d9fa65"
@@ -3877,6 +4036,14 @@
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.35.tgz#192eb3d720c40715db79313454c4937432a4e86d"
integrity sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==
"@vue/test-utils@^2.4.6":
version "2.4.10"
resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.4.10.tgz#f3b006e03918e66b5df1f2a6f7f5200663b525d3"
integrity sha512-SmoZ5EA1kYiAFs9NkYdiFFQF+cSnUwnvlYEbY+DogWQZUiqOm/Y29eSbc5T6yi75SgSF9863SBeXniIEoPajCA==
dependencies:
js-beautify "^1.14.9"
vue-component-type-helpers "^3.0.0"
"@vuetify/loader-shared@^2.1.2":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@vuetify/loader-shared/-/loader-shared-2.1.2.tgz#faf27cb8c40ba5a45b930b9a2785e35525e9c96f"
@@ -3911,6 +4078,11 @@
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-14.3.0.tgz#a3e7e6391f9ed7f363cbb28c32c4a278efaacbd0"
integrity sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==
abbrev@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-2.0.0.tgz#cf59829b8b4f03f89dda2771cb7f3653828c89bf"
integrity sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==
abbrev@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-3.0.1.tgz#8ac8b3b5024d31464fe2a5feeea9f4536bf44025"
@@ -4095,6 +4267,15 @@ ast-kit@^2.1.2, ast-kit@^2.1.3:
"@babel/parser" "^7.28.5"
pathe "^2.0.3"
ast-v8-to-istanbul@^0.3.3:
version "0.3.12"
resolved "https://registry.yarnpkg.com/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz#8eb1b7c86ef8499859be761b17ffd91406c0c36f"
integrity sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==
dependencies:
"@jridgewell/trace-mapping" "^0.3.31"
estree-walker "^3.0.3"
js-tokens "^10.0.0"
ast-walker-scope@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/ast-walker-scope/-/ast-walker-scope-0.6.2.tgz#b827e8949c129802f76fe0f142e95fd7efda57dc"
@@ -4453,16 +4634,10 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001782, caniuse-lite@^1.0.30001787:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz#238887ddf5fcfc8c36d872394d0a78a517312a72"
integrity sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==
chai@^5.2.0:
version "5.3.3"
resolved "https://registry.yarnpkg.com/chai/-/chai-5.3.3.tgz#dd3da955e270916a4bd3f625f4b919996ada7e06"
integrity sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==
dependencies:
assertion-error "^2.0.1"
check-error "^2.1.1"
deep-eql "^5.0.1"
loupe "^3.1.0"
pathval "^2.0.0"
chai@^6.2.2:
version "6.2.2"
resolved "https://registry.yarnpkg.com/chai/-/chai-6.2.2.tgz#ae41b52c9aca87734505362717f3255facda360e"
integrity sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==
chalk@^4.0.0:
version "4.1.2"
@@ -4482,11 +4657,6 @@ change-case@^5.4.4:
resolved "https://registry.yarnpkg.com/change-case/-/change-case-5.4.4.tgz#0d52b507d8fb8f204343432381d1a6d7bff97a02"
integrity sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==
check-error@^2.1.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.3.tgz#2427361117b70cca8dc89680ead32b157019caf5"
integrity sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==
chokidar@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30"
@@ -4608,6 +4778,11 @@ combined-stream@^1.0.8:
dependencies:
delayed-stream "~1.0.0"
commander@^10.0.0:
version "10.0.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
commander@^11.1.0:
version "11.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906"
@@ -4674,6 +4849,14 @@ confbox@^0.2.4:
resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.2.4.tgz#592e7be71f882a4a874e3c88f0ac1ef6f7da1ce5"
integrity sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==
config-chain@^1.1.13:
version "1.1.13"
resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4"
integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==
dependencies:
ini "^1.3.4"
proto-list "~1.2.1"
consola@^3.2.3, consola@^3.4.2:
version "3.4.2"
resolved "https://registry.yarnpkg.com/consola/-/consola-3.4.2.tgz#5af110145397bb67afdab77013fdc34cae590ea7"
@@ -4926,11 +5109,6 @@ decimal.js@^10.6.0:
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a"
integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==
deep-eql@^5.0.1:
version "5.0.2"
resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341"
integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==
deep-is@^0.1.3:
version "0.1.4"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
@@ -5100,6 +5278,16 @@ easy-bem@^1.0.2:
resolved "https://registry.yarnpkg.com/easy-bem/-/easy-bem-1.1.1.tgz#1bfcc10425498090bcfddc0f9c000aba91399e03"
integrity sha512-GJRqdiy2h+EXy6a8E6R+ubmqUM08BK0FWNq41k24fup6045biQ8NXxoXimiwegMQvFFV3t1emADdGNL1TlS61A==
editorconfig@^1.0.4:
version "1.0.7"
resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-1.0.7.tgz#8d6e178aeb507c206d65e1804c1d7510d110d434"
integrity sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==
dependencies:
"@one-ini/wasm" "0.1.1"
commander "^10.0.0"
minimatch "^9.0.1"
semver "^7.5.3"
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -5237,7 +5425,7 @@ es-errors@^1.3.0:
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
es-module-lexer@^1.5.4, es-module-lexer@^1.7.0:
es-module-lexer@^1.5.4:
version "1.7.0"
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a"
integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==
@@ -5796,7 +5984,7 @@ execa@^8.0.1:
signal-exit "^4.1.0"
strip-final-newline "^3.0.0"
expect-type@^1.2.1:
expect-type@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.3.0.tgz#0d58ed361877a31bbc4dd6cf71bbfef7faf6bd68"
integrity sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==
@@ -5883,6 +6071,11 @@ fdir@^6.2.0, fdir@^6.5.0:
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350"
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
fflate@^0.8.2:
version "0.8.3"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.3.tgz#bc27d8eb30343d4d512abb03480202ce65d825fc"
integrity sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==
file-entry-cache@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f"
@@ -5938,7 +6131,7 @@ flat-cache@^4.0.0:
flatted "^3.2.9"
keyv "^4.5.4"
flatted@^3.2.9:
flatted@^3.2.9, flatted@^3.3.3:
version "3.4.2"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726"
integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==
@@ -6154,7 +6347,7 @@ glob-parent@^6.0.2:
dependencies:
is-glob "^4.0.3"
glob@^10.0.0:
glob@^10.0.0, glob@^10.4.1, glob@^10.4.2:
version "10.5.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c"
integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==
@@ -6333,6 +6526,11 @@ html-entities@^2.6.0:
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.6.0.tgz#7c64f1ea3b36818ccae3d3fb48b6974208e984f8"
integrity sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==
html-escaper@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
http-errors@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b"
@@ -6461,6 +6659,11 @@ ini@4.1.1:
resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.1.tgz#d95b3d843b1e906e56d6747d5447904ff50ce7a1"
integrity sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==
ini@^1.3.4:
version "1.3.8"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
internal-slot@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961"
@@ -6804,6 +7007,37 @@ isomorphic-dompurify@^3.4.0:
dompurify "^3.4.5"
jsdom "^29.1.1"
istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756"
integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==
istanbul-lib-report@^3.0.0, istanbul-lib-report@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d"
integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==
dependencies:
istanbul-lib-coverage "^3.0.0"
make-dir "^4.0.0"
supports-color "^7.1.0"
istanbul-lib-source-maps@^5.0.6:
version "5.0.6"
resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz#acaef948df7747c8eb5fbf1265cb980f6353a441"
integrity sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==
dependencies:
"@jridgewell/trace-mapping" "^0.3.23"
debug "^4.1.1"
istanbul-lib-coverage "^3.0.0"
istanbul-reports@^3.1.7:
version "3.2.0"
resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz#cb4535162b5784aa623cee21a7252cf2c807ac93"
integrity sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==
dependencies:
html-escaper "^2.0.0"
istanbul-lib-report "^3.0.0"
jackspeak@^3.1.2:
version "3.4.3"
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a"
@@ -6839,6 +7073,27 @@ jmespath@^0.16.0:
resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076"
integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==
js-beautify@^1.14.9:
version "1.15.4"
resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.15.4.tgz#f579f977ed4c930cef73af8f98f3f0a608acd51e"
integrity sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==
dependencies:
config-chain "^1.1.13"
editorconfig "^1.0.4"
glob "^10.4.2"
js-cookie "^3.0.5"
nopt "^7.2.1"
js-cookie@^3.0.5:
version "3.0.8"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.8.tgz#444e6f4b27a5d844594fef61c9d6bca5f0787688"
integrity sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==
js-tokens@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-10.0.0.tgz#dffe7599b4a8bb7fe30aff8d0235234dffb79831"
integrity sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==
js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -7032,6 +7287,80 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
lightningcss-android-arm64@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz#f033885116dfefd9c6f54787523e3514b61e1968"
integrity sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==
lightningcss-darwin-arm64@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz#50b71871b01c8199584b649e292547faea7af9b5"
integrity sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==
lightningcss-darwin-x64@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz#35f3e97332d130b9ca181e11b568ded6aebc6d5e"
integrity sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==
lightningcss-freebsd-x64@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz#9777a76472b64ed6ff94342ad64c7bafd794a575"
integrity sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==
lightningcss-linux-arm-gnueabihf@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz#13ae652e1ab73b9135d7b7da172f666c410ad53d"
integrity sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==
lightningcss-linux-arm64-gnu@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz#417858795a94592f680123a1b1f9da8a0e1ef335"
integrity sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==
lightningcss-linux-arm64-musl@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz#6be36692e810b718040802fd809623cffe732133"
integrity sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==
lightningcss-linux-x64-gnu@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz#0b7803af4eb21cfd38dd39fe2abbb53c7dd091f6"
integrity sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==
lightningcss-linux-x64-musl@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz#88dc8ba865ddddb1ac5ef04b0f161804418c163b"
integrity sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==
lightningcss-win32-arm64-msvc@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz#4f30ba3fa5e925f5b79f945e8cc0d176c3b1ab38"
integrity sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==
lightningcss-win32-x64-msvc@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz#141aa5605645064928902bb4af045fa7d9f4220a"
integrity sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==
lightningcss@^1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.32.0.tgz#b85aae96486dcb1bf49a7c8571221273f4f1e4a9"
integrity sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==
dependencies:
detect-libc "^2.0.3"
optionalDependencies:
lightningcss-android-arm64 "1.32.0"
lightningcss-darwin-arm64 "1.32.0"
lightningcss-darwin-x64 "1.32.0"
lightningcss-freebsd-x64 "1.32.0"
lightningcss-linux-arm-gnueabihf "1.32.0"
lightningcss-linux-arm64-gnu "1.32.0"
lightningcss-linux-arm64-musl "1.32.0"
lightningcss-linux-x64-gnu "1.32.0"
lightningcss-linux-x64-musl "1.32.0"
lightningcss-win32-arm64-msvc "1.32.0"
lightningcss-win32-x64-msvc "1.32.0"
lilconfig@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4"
@@ -7168,7 +7497,7 @@ log-update@^6.1.0:
strip-ansi "^7.1.0"
wrap-ansi "^9.0.0"
loupe@^3.1.0, loupe@^3.1.4:
loupe@^3.1.4:
version "3.2.1"
resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.2.1.tgz#0095cf56dc5b7a9a7c08ff5b1a8796ec8ad17e76"
integrity sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==
@@ -7234,6 +7563,15 @@ magic-string@^0.30.11, magic-string@^0.30.12, magic-string@^0.30.17, magic-strin
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.5"
magicast@^0.3.5:
version "0.3.5"
resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.3.5.tgz#8301c3c7d66704a0771eb1bad74274f0ec036739"
integrity sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==
dependencies:
"@babel/parser" "^7.25.4"
"@babel/types" "^7.25.4"
source-map-js "^1.2.0"
magicast@^0.5.2:
version "0.5.3"
resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.5.3.tgz#1800f6e76dd8b0dbe7257438a2c336aefabbd905"
@@ -7243,6 +7581,13 @@ magicast@^0.5.2:
"@babel/types" "^7.29.0"
source-map-js "^1.2.1"
make-dir@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e"
integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==
dependencies:
semver "^7.5.3"
marked@^15.0.12:
version "15.0.12"
resolved "https://registry.yarnpkg.com/marked/-/marked-15.0.12.tgz#30722c7346e12d0a2d0207ab9b0c4f0102d86c4e"
@@ -7346,7 +7691,7 @@ minimatch@^5.0.1, minimatch@^5.1.0:
dependencies:
brace-expansion "^2.0.1"
minimatch@^9.0.4:
minimatch@^9.0.1, minimatch@^9.0.4:
version "9.0.9"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e"
integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==
@@ -7538,6 +7883,13 @@ node-releases@^2.0.36:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.46.tgz#d188a129a83f5e03a101aacb58f260f2ee8faaa1"
integrity sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==
nopt@^7.2.1:
version "7.2.1"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-7.2.1.tgz#1cac0eab9b8e97c9093338446eddd40b2c8ca1e7"
integrity sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==
dependencies:
abbrev "^2.0.0"
nopt@^8.0.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-8.1.0.tgz#b11d38caf0f8643ce885818518064127f602eae3"
@@ -8018,11 +8370,6 @@ pathe@^2.0.1, pathe@^2.0.2, pathe@^2.0.3:
resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716"
integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
pathval@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.1.tgz#8855c5a2899af072d6ac05d11e46045ad0dc605d"
integrity sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==
perfect-debounce@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a"
@@ -8368,6 +8715,11 @@ proper-lockfile@^4.1.2:
retry "^0.12.0"
signal-exit "^3.0.2"
proto-list@~1.2.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==
proxy-from-env@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz#a7487568adad577cfaaa7e88c49cab3ab3081aba"
@@ -8604,6 +8956,30 @@ rfdc@^1.4.1:
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca"
integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==
rolldown@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.3.tgz#db88a3008fb0e28230a00423727ce75ba32121ac"
integrity sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==
dependencies:
"@oxc-project/types" "=0.133.0"
"@rolldown/pluginutils" "^1.0.0"
optionalDependencies:
"@rolldown/binding-android-arm64" "1.0.3"
"@rolldown/binding-darwin-arm64" "1.0.3"
"@rolldown/binding-darwin-x64" "1.0.3"
"@rolldown/binding-freebsd-x64" "1.0.3"
"@rolldown/binding-linux-arm-gnueabihf" "1.0.3"
"@rolldown/binding-linux-arm64-gnu" "1.0.3"
"@rolldown/binding-linux-arm64-musl" "1.0.3"
"@rolldown/binding-linux-ppc64-gnu" "1.0.3"
"@rolldown/binding-linux-s390x-gnu" "1.0.3"
"@rolldown/binding-linux-x64-gnu" "1.0.3"
"@rolldown/binding-linux-x64-musl" "1.0.3"
"@rolldown/binding-openharmony-arm64" "1.0.3"
"@rolldown/binding-wasm32-wasi" "1.0.3"
"@rolldown/binding-win32-arm64-msvc" "1.0.3"
"@rolldown/binding-win32-x64-msvc" "1.0.3"
rollup-plugin-visualizer@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/rollup-plugin-visualizer/-/rollup-plugin-visualizer-7.0.1.tgz#291c10ff4a956d9b2483f8b4147b2bf0aacd3a6e"
@@ -9086,7 +9462,7 @@ smob@^1.0.0:
resolved "https://registry.yarnpkg.com/smob/-/smob-1.6.2.tgz#190b94c25530c631a7ccc63de0d4c0087222d21d"
integrity sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.1:
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.0, source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
@@ -9169,7 +9545,7 @@ std-env@^3.9.0:
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.10.0.tgz#d810b27e3a073047b2b5e40034881f5ea6f9c83b"
integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==
std-env@^4.0.0, std-env@^4.1.0:
std-env@^4.0.0, std-env@^4.0.0-rc.1, std-env@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/std-env/-/std-env-4.1.0.tgz#45899abc590d86d682e87f0acd1033a75084cd3f"
integrity sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==
@@ -9347,7 +9723,7 @@ strip-json-comments@^3.1.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
strip-literal@^3.0.0, strip-literal@^3.1.0:
strip-literal@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-3.1.0.tgz#222b243dd2d49c0bcd0de8906adbd84177196032"
integrity sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==
@@ -9513,6 +9889,15 @@ terser@^5.17.4:
commander "^2.20.0"
source-map-support "~0.5.20"
test-exclude@^7.0.1:
version "7.0.2"
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-7.0.2.tgz#482392077630bc57d5630c13abe908bb910dfc65"
integrity sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==
dependencies:
"@istanbuljs/schema" "^0.1.2"
glob "^10.4.1"
minimatch "^10.2.2"
text-decoder@^1.1.0:
version "1.2.7"
resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.7.tgz#5d073a9a74b9c0a9d28dfadcab96b604af57d8ba"
@@ -9540,16 +9925,16 @@ tinyclip@^0.1.12:
resolved "https://registry.yarnpkg.com/tinyclip/-/tinyclip-0.1.13.tgz#aafa818c5378f65fd375b9d3981eadd6c784b919"
integrity sha512-8OqlXQ35euK9+e7L68u8UwcODxkHoIkjbGsgXuARKNyQ5G6xt8nw1YPeMbxMLgCPFkToU+UEK5j05t2t8edKpQ==
tinyexec@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2"
integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==
tinyexec@^1.0.1, tinyexec@^1.1.1, tinyexec@^1.1.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.2.2.tgz#b66edf362dcad61174bc9ce4f3ee279e2448a721"
integrity sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==
tinyexec@^1.0.2:
version "1.2.4"
resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.2.4.tgz#ae45bb2edebda94c70f4ea897e0f1243e470db71"
integrity sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==
tinyglobby@^0.2.10, tinyglobby@^0.2.13, tinyglobby@^0.2.14, tinyglobby@^0.2.15, tinyglobby@^0.2.16:
version "0.2.16"
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6"
@@ -9558,25 +9943,28 @@ tinyglobby@^0.2.10, tinyglobby@^0.2.13, tinyglobby@^0.2.14, tinyglobby@^0.2.15,
fdir "^6.5.0"
picomatch "^4.0.4"
tinyglobby@^0.2.17:
version "0.2.17"
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631"
integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==
dependencies:
fdir "^6.5.0"
picomatch "^4.0.4"
tinypool@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-2.1.0.tgz#303a671d6ef68d03c9512cdc9a47c86b8a85f20c"
integrity sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==
tinypool@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.1.1.tgz#059f2d042bd37567fbc017d3d426bdd2a2612591"
integrity sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==
tinyrainbow@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-2.0.0.tgz#9509b2162436315e80e3eee0fcce4474d2444294"
integrity sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==
tinyspy@^4.0.3:
version "4.0.4"
resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-4.0.4.tgz#d77a002fb53a88aa1429b419c1c92492e0c81f78"
integrity sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==
tinyrainbow@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-3.1.0.tgz#1d8a623893f95cf0a2ddb9e5d11150e191409421"
integrity sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==
tldts-core@^7.4.0:
version "7.4.0"
@@ -9921,6 +10309,18 @@ universalify@^2.0.0:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
unplugin-auto-import@^21.0.0:
version "21.0.0"
resolved "https://registry.yarnpkg.com/unplugin-auto-import/-/unplugin-auto-import-21.0.0.tgz#d1ac3bf95f80fb4182ec8f3d65d3e3aad38a63ac"
integrity sha512-vWuC8SwqJmxZFYwPojhOhOXDb5xFhNNcEVb9K/RFkyk/3VnfaOjzitWN7v+8DEKpMjSsY2AEGXNgt6I0yQrhRQ==
dependencies:
local-pkg "^1.1.2"
magic-string "^0.30.21"
picomatch "^4.0.3"
unimport "^5.6.0"
unplugin "^2.3.11"
unplugin-utils "^0.3.1"
unplugin-utils@^0.2.3:
version "0.2.5"
resolved "https://registry.yarnpkg.com/unplugin-utils/-/unplugin-utils-0.2.5.tgz#d2fe44566ffffd7f216579bbb01184f6702e379b"
@@ -10161,17 +10561,6 @@ vite-hot-client@^2.1.0:
resolved "https://registry.yarnpkg.com/vite-hot-client/-/vite-hot-client-2.2.0.tgz#284fa1afa63cc8781d079515884eb49d2890ac2f"
integrity sha512-76Zs9zrHbH7M7wqeyooGQKdX+yg0pQ0xuQ1PbFp4z5a0Lzn2e5IPFoCswnmqZ4GiwqB4Jo3WcDAMO9jARTJl8w==
vite-node@3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.2.4.tgz#f3676d94c4af1e76898c162c92728bca65f7bb07"
integrity sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==
dependencies:
cac "^6.7.14"
debug "^4.4.1"
es-module-lexer "^1.7.0"
pathe "^2.0.3"
vite "^5.0.0 || ^6.0.0 || ^7.0.0-0"
vite-node@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-5.3.0.tgz#fb9f73c2d5bd1f95b3beaad6455e04ad899c6336"
@@ -10263,7 +10652,20 @@ vite-plugin-vuetify@^2.1.3:
debug "^4.3.3"
upath "^2.0.1"
"vite@^5.0.0 || ^6.0.0 || ^7.0.0-0", vite@^7.3.1, vite@^7.3.3:
"vite@^6.0.0 || ^7.0.0 || ^8.0.0":
version "8.0.16"
resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.16.tgz#ae073866c06563d6634a90169a496e11bd84f1a6"
integrity sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==
dependencies:
lightningcss "^1.32.0"
picomatch "^4.0.4"
postcss "^8.5.15"
rolldown "1.0.3"
tinyglobby "^0.2.17"
optionalDependencies:
fsevents "~2.3.3"
vite@^7.3.1, vite@^7.3.3:
version "7.3.3"
resolved "https://registry.yarnpkg.com/vite/-/vite-7.3.3.tgz#d7e07a52b5873fb86f902a3f4b3d17410337450f"
integrity sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==
@@ -10277,33 +10679,30 @@ vite-plugin-vuetify@^2.1.3:
optionalDependencies:
fsevents "~2.3.3"
vitest@^3.0.7:
version "3.2.4"
resolved "https://registry.yarnpkg.com/vitest/-/vitest-3.2.4.tgz#0637b903ad79d1539a25bc34c0ed54b5c67702ea"
integrity sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==
vitest@^4.0.0:
version "4.1.8"
resolved "https://registry.yarnpkg.com/vitest/-/vitest-4.1.8.tgz#9fed17277bf7350497e54338898a7afd46dfd509"
integrity sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==
dependencies:
"@types/chai" "^5.2.2"
"@vitest/expect" "3.2.4"
"@vitest/mocker" "3.2.4"
"@vitest/pretty-format" "^3.2.4"
"@vitest/runner" "3.2.4"
"@vitest/snapshot" "3.2.4"
"@vitest/spy" "3.2.4"
"@vitest/utils" "3.2.4"
chai "^5.2.0"
debug "^4.4.1"
expect-type "^1.2.1"
magic-string "^0.30.17"
"@vitest/expect" "4.1.8"
"@vitest/mocker" "4.1.8"
"@vitest/pretty-format" "4.1.8"
"@vitest/runner" "4.1.8"
"@vitest/snapshot" "4.1.8"
"@vitest/spy" "4.1.8"
"@vitest/utils" "4.1.8"
es-module-lexer "^2.0.0"
expect-type "^1.3.0"
magic-string "^0.30.21"
obug "^2.1.1"
pathe "^2.0.3"
picomatch "^4.0.2"
std-env "^3.9.0"
picomatch "^4.0.3"
std-env "^4.0.0-rc.1"
tinybench "^2.9.0"
tinyexec "^0.3.2"
tinyglobby "^0.2.14"
tinypool "^1.1.1"
tinyrainbow "^2.0.0"
vite "^5.0.0 || ^6.0.0 || ^7.0.0-0"
vite-node "3.2.4"
tinyexec "^1.0.2"
tinyglobby "^0.2.15"
tinyrainbow "^3.1.0"
vite "^6.0.0 || ^7.0.0 || ^8.0.0"
why-is-node-running "^2.3.0"
vscode-uri@^3.1.0:
@@ -10327,6 +10726,11 @@ vue-bundle-renderer@^2.2.0:
dependencies:
ufo "^1.6.1"
vue-component-type-helpers@^3.0.0:
version "3.3.3"
resolved "https://registry.yarnpkg.com/vue-component-type-helpers/-/vue-component-type-helpers-3.3.3.tgz#9f31a7610348de6fa151c0166600f7ed5808beb3"
integrity sha512-x4nsFpy5Pe8fqPzp/5vkTPeTTDBpAx4WVtV47Ejt0+2FQrq4pRRsJs7JmYRqMFzTu/LW+pCWEjQ3YVCkPV7f9g==
vue-demi@^0.14.10:
version "0.14.10"
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"

View File

@@ -179,7 +179,7 @@ def validate_file_token(token: str | None = None) -> Path:
@contextmanager
def get_temporary_zip_path(auto_unlink=True) -> Generator[Path, None, None]:
app_dirs.TEMP_DIR.mkdir(exist_ok=True, parents=True)
temp_path = app_dirs.TEMP_DIR.joinpath("my_zip_archive.zip")
temp_path = app_dirs.TEMP_DIR / f"{uuid4().hex}.zip"
try:
yield temp_path
finally:

View File

@@ -33,6 +33,16 @@ class FeatureDetails(NamedTuple):
return s
DEFAULT_ALLOWED_IFRAME_HOSTS = [
"youtube.com",
"youtube-nocookie.com",
"vimeo.com",
"player.vimeo.com",
]
"""Secure-by-default hostnames permitted as `<iframe>` sources in user content. Limited to
well-known video providers. Subdomains of these hosts are also allowed (e.g. `www.youtube.com`)."""
MaskedNoneString = Annotated[
str | None,
PlainSerializer(lambda x: None if x is None else "*****", return_type=str | None),
@@ -150,6 +160,19 @@ class AppSettings(AppLoggingSettings):
ALLOW_SIGNUP: bool = False
ALLOW_PASSWORD_LOGIN: bool = True
ALLOWED_IFRAME_HOSTS: str = ""
"""Comma-separated list of additional hostnames allowed as `<iframe>` sources in user content
(recipe instructions, notes, descriptions). Extends `DEFAULT_ALLOWED_IFRAME_HOSTS`. Subdomains of
a listed host are also allowed. Adding hosts is opt-in to riskier behavior; the defaults are
limited to well-known video providers."""
@property
def allowed_iframe_hosts(self) -> list[str]:
"""The full set of hostnames permitted as `<iframe>` sources, secure defaults plus any
admin-configured additions via `ALLOWED_IFRAME_HOSTS`."""
extra = [host.strip().lower() for host in self.ALLOWED_IFRAME_HOSTS.split(",") if host.strip()]
return list(dict.fromkeys(DEFAULT_ALLOWED_IFRAME_HOSTS + extra))
DAILY_SCHEDULE_TIME: str = "23:45"
"""Local server time, in HH:MM format. See `DAILY_SCHEDULE_TIME_UTC` for the parsed UTC equivalent"""

View File

@@ -43,6 +43,7 @@ def get_app_info(session: Session = Depends(generate_session)):
oidc_provider_name=settings.OIDC_PROVIDER_NAME,
allow_password_login=settings.ALLOW_PASSWORD_LOGIN,
token_time=settings.TOKEN_TIME,
allowed_iframe_hosts=settings.allowed_iframe_hosts,
)

View File

@@ -58,6 +58,13 @@ async def get_recipe_asset(recipe_id: UUID4, file_name: str):
raise HTTPException(status.HTTP_400_BAD_REQUEST)
if file.exists():
return FileResponse(file, filename=file.name, content_disposition_type="attachment")
# Force download and disable MIME sniffing so uploaded assets cannot be
# served as active content in Mealie's origin.
return FileResponse(
file,
filename=file.name,
content_disposition_type="attachment",
headers={"X-Content-Type-Options": "nosniff"},
)
else:
raise HTTPException(status.HTTP_404_NOT_FOUND)

View File

@@ -22,6 +22,7 @@ class AppInfo(MealieModel):
oidc_redirect: bool
oidc_provider_name: str
token_time: int
allowed_iframe_hosts: list[str] = []
class AppTheme(MealieModel):

View File

@@ -6,6 +6,7 @@ from pathlib import Path
from zipfile import ZipFile
from mealie.core.config import get_app_settings
from mealie.core.settings.static import APP_VERSION
from mealie.services._base_service import BaseService
from mealie.services.backups_v2.alchemy_exporter import AlchemyExporter
from mealie.services.backups_v2.backup_file import BackupFile
@@ -43,8 +44,15 @@ class BackupV2(BaseService):
def backup(self) -> Path:
# sourcery skip: merge-nested-ifs, reintroduce-else, remove-redundant-continue
timestamp = datetime.datetime.now(datetime.UTC).strftime("%Y.%m.%d.%H.%M.%S")
short_hash = self.settings.GIT_COMMIT_HASH[:7]
if APP_VERSION == "develop":
backup_name = f"mealie_dev-{short_hash}_{timestamp}.zip"
elif APP_VERSION == "nightly":
backup_name = f"mealie_nightly-{short_hash}_{timestamp}.zip"
else:
backup_name = f"mealie_{APP_VERSION}_{timestamp}.zip"
backup_name = f"mealie_{timestamp}.zip"
backup_file = self.directories.BACKUP_DIR / backup_name
database_json = self.db_exporter.dump()

View File

@@ -7,6 +7,7 @@ from pathlib import Path
from slugify import slugify
from mealie.pkgs.cache import cache_key
from mealie.schema.reports.reports import ReportEntryCreate
from mealie.services.scraper import cleaner
from ._migration_base import BaseMigrator
@@ -15,15 +16,23 @@ from .utils.migration_helpers import scrape_image, split_by_comma
def plantoeat_recipes(file: Path):
"""Yields all recipes inside the export file as dict"""
with tempfile.TemporaryDirectory() as tmpdir:
with zipfile.ZipFile(file) as zip_file:
zip_file.extractall(tmpdir)
"""Yields all recipes inside the export file as dict.
for name in Path(tmpdir).glob("**/[!.]*.csv"):
with open(name, newline="") as csvfile:
reader = csv.DictReader(csvfile)
yield from reader
Accepts a ZIP archive containing a CSV, or a raw CSV/TXT file.
"""
if zipfile.is_zipfile(file):
with tempfile.TemporaryDirectory() as tmpdir:
with zipfile.ZipFile(file) as zip_file:
zip_file.extractall(tmpdir)
for name in Path(tmpdir).glob("**/[!.]*.csv"):
with open(name, newline="") as csvfile:
reader = csv.DictReader(csvfile)
yield from reader
else:
with open(file, newline="", encoding="utf-8", errors="ignore") as csvfile:
reader = csv.DictReader(csvfile)
yield from reader
def get_value_as_string_or_none(dictionary: dict, key: str):
@@ -112,7 +121,32 @@ class PlanToEatMigrator(BaseMigrator):
return recipe_dict
def _validate_archive(self) -> bool:
"""Returns False and appends a failure report entry if the file is not a ZIP, CSV, or TXT."""
if zipfile.is_zipfile(self.archive):
return True
try:
with open(self.archive, encoding="utf-8", errors="strict") as f:
f.read(512)
return True
except UnicodeDecodeError:
pass
self.report_entries.append(
ReportEntryCreate(
report_id=self.report_id,
success=False,
message="Unsupported file format. Please upload a ZIP archive, CSV file, or TXT file.",
exception="",
)
)
return False
def _migrate(self) -> None:
if not self._validate_archive():
return
recipe_image_urls = {}
recipes = []

View File

@@ -12,8 +12,6 @@ dependencies = [
"SQLAlchemy==2.0.50",
"aiofiles==25.1.0",
"alembic==1.18.4",
"aniso8601==10.0.1",
"appdirs==1.4.4",
"apprise==1.10.0",
"bcrypt==5.0.0",
"extruct==0.18.0",
@@ -71,9 +69,9 @@ dev = [
"pre-commit==4.6.0",
"pylint==4.0.5",
"pytest==9.0.3",
"pytest-asyncio==1.3.0",
"pytest-asyncio==1.4.0",
"rich==15.0.0",
"ruff==0.15.14",
"ruff==0.15.15",
"types-PyYAML==6.0.12.20260518",
"types-python-dateutil==2.9.0.20260518",
"types-python-slugify==8.0.2.20240310",
@@ -178,3 +176,7 @@ max-complexity = 24 # Default is 10.
[tool.uv]
add-bounds = "exact"
# Cooling period: ignore package releases newer than 5 days to mitigate
# supply-chain attacks (compromised releases are usually caught and yanked
# within days). Evaluated at resolve time as a rolling window.
exclude-newer = "5 days"

View File

@@ -12,6 +12,8 @@
"extends": [
"config:recommended"
],
"minimumReleaseAge": "5 days",
"internalChecksFilter": "strict",
"addLabels": [
"dependencies"
],
@@ -51,8 +53,7 @@
],
"automerge": true,
"automergeType": "pr",
"automergeStrategy": "squash",
"minimumReleaseAge": "5 days"
"automergeStrategy": "squash"
},
{
"description": "Auto-merge Docker digest and patch updates",
@@ -66,8 +67,7 @@
],
"automerge": true,
"automergeType": "pr",
"automergeStrategy": "squash",
"minimumReleaseAge": "5 days"
"automergeStrategy": "squash"
}
]
}

View File

@@ -45,6 +45,8 @@ migrations_tandoor = CWD / "migrations/tandoor.zip"
migrations_plantoeat = CWD / "migrations/plantoeat.zip"
migrations_plantoeat_csv = CWD / "migrations/plantoeat.csv"
migrations_myrecipebox = CWD / "migrations/myrecipebox.csv"
migrations_recipekeeper = CWD / "migrations/recipekeeper.zip"

View File

@@ -0,0 +1,13 @@
Title,Course,Cuisine,Main Ingredient,Description,Source,Url,Url Host,Prep Time,Cook Time,Total Time,Servings,Yield,Ingredients,Directions,Tags,Rating,Public Url,Photo Url,Private,Nutritional Score (generic),Calories,Fat,Saturated Fat,Cholesterol,Sodium,Sugar,Carbohydrate,Fiber,Protein,Cost,Created At,Updated At
Test Recipe,Main Course,American,Beans,"This is a description.
Here is new line.",Manually entered source,https://eatwithclarity.com/sushi-bowl-with-sesame-tofu/,,75,75,150,7,1 loaf,", Heading
2 itm Test, note
, Heading2
3 pkg Two, note2
","Directions.
Will go here.","Allergen-Friendly, Cheap, Test",3,https://app.plantoeat.com/recipes/38843883,https://plantoeat.s3.amazonaws.com/recipes/29516709/470292506c8d9b71582487a7879ab7b197d06490-large.jpg?1628205591,yes,,13,16,17,18,19,22,20,21,23,,2023-10-13 20:29:29,2023-10-13 20:32:48
Test Recipe2,,,,,,,,,,,,,"2 itm Test, note
3 pkg Two, note2
","Directions.
Will go here.",,,,,,,,,,,,,,,,,2023-10-13 20:29:29,2023-10-13 20:32:48
1 Title Course Cuisine Main Ingredient Description Source Url Url Host Prep Time Cook Time Total Time Servings Yield Ingredients Directions Tags Rating Public Url Photo Url Private Nutritional Score (generic) Calories Fat Saturated Fat Cholesterol Sodium Sugar Carbohydrate Fiber Protein Cost Created At Updated At
2 Test Recipe Main Course American Beans This is a description. Here is new line. Manually entered source https://eatwithclarity.com/sushi-bowl-with-sesame-tofu/ 75 75 150 7 1 loaf , Heading 2 itm Test, note , Heading2 3 pkg Two, note2 Directions. Will go here. Allergen-Friendly, Cheap, Test 3 https://app.plantoeat.com/recipes/38843883 https://plantoeat.s3.amazonaws.com/recipes/29516709/470292506c8d9b71582487a7879ab7b197d06490-large.jpg?1628205591 yes 13 16 17 18 19 22 20 21 23 2023-10-13 20:29:29 2023-10-13 20:32:48
3 Test Recipe2 2 itm Test, note 3 pkg Two, note2 Directions. Will go here. 2023-10-13 20:29:29 2023-10-13 20:32:48

View File

@@ -94,6 +94,15 @@ test_cases = [
"transFatContent",
},
),
MigrationTestData(
typ=SupportedMigrations.plantoeat,
archive=test_data.migrations_plantoeat_csv,
search_slug="test-recipe",
nutrition_filter={
"unsaturatedFatContent",
"transFatContent",
},
),
MigrationTestData(
typ=SupportedMigrations.myrecipebox,
archive=test_data.migrations_myrecipebox,
@@ -124,6 +133,7 @@ test_ids = [
"mealie_alpha_archive",
"tandoor_archive",
"plantoeat_archive",
"plantoeat_csv",
"myrecipebox_csv",
"recipekeeper_archive",
"cookn_archive",
@@ -190,6 +200,30 @@ def test_recipe_migration(api_client: TestClient, unique_user_fn_scoped: TestUse
# TODO: validate other types of content
def test_plantoeat_rejects_invalid_file_type(api_client: TestClient, unique_user: TestUser) -> None:
# Simulate uploading a binary file (e.g. PDF) that is neither ZIP nor CSV/TXT
binary_content = bytes(range(256)) * 4 # arbitrary binary data that is not valid UTF-8
payload = {"migration_type": SupportedMigrations.plantoeat.value}
file_payload = {"archive": binary_content}
response = api_client.post(
api_routes.groups_migrations,
data=payload,
files=file_payload,
headers=unique_user.token,
)
assert response.status_code == 200
report_id = response.json()["id"]
response = api_client.get(api_routes.groups_reports_item_id(report_id), headers=unique_user.token)
assert response.status_code == 200
report = response.json()
assert report["entries"]
assert not report["entries"][0]["success"]
assert "ZIP" in report["entries"][0]["message"] or "CSV" in report["entries"][0]["message"]
def test_bad_mealie_alpha_data_is_ignored(api_client: TestClient, unique_user: TestUser):
with TemporaryDirectory() as tmpdir:
with ZipFile(test_data.migrations_mealie) as zf:

View File

@@ -104,6 +104,33 @@ def test_recipe_asset_dangerous_extension_blocked(
assert response.status_code == 400, f"expected 400 for extension={ext}, got {response.status_code}"
def test_recipe_asset_served_as_attachment(
api_client: TestClient, unique_user: TestUser, recipe_ingredient_only: Recipe
):
"""Assets must be served as downloads with MIME sniffing disabled so uploaded files cannot
execute as active content in Mealie's origin."""
recipe = recipe_ingredient_only
payload = {"name": random_string(10), "icon": "mdi-file", "extension": "txt"}
file_payload = {"file": b"<script>alert(1)</script>"}
response = api_client.post(
f"/api/recipes/{recipe.slug}/assets",
data=payload,
files=file_payload,
headers=unique_user.token,
)
assert response.status_code == 200
recipe_response = api_client.get(f"/api/recipes/{recipe.slug}", headers=unique_user.token).json()
recipe_id = recipe_response["id"]
file_name = recipe_response["assets"][0]["fileName"]
media_response = api_client.get(f"/api/media/recipes/{recipe_id}/assets/{file_name}")
assert media_response.status_code == 200
assert "attachment" in media_response.headers["content-disposition"].lower()
assert media_response.headers["x-content-type-options"] == "nosniff"
def test_recipe_image_upload(api_client: TestClient, unique_user: TestUser, recipe_ingredient_only: Recipe):
data_payload = {"extension": "jpg"}
file_payload = {"image": data.images_test_image_1.read_bytes()}

View File

@@ -27,6 +27,31 @@ def test_non_default_settings(monkeypatch):
assert app_settings.DOCS_URL is None
def test_allowed_iframe_hosts_defaults(monkeypatch):
monkeypatch.delenv("ALLOWED_IFRAME_HOSTS", raising=False)
get_app_settings.cache_clear()
app_settings = get_app_settings()
# Secure defaults are always present and never empty (empty would disable iframe embeds).
assert "youtube.com" in app_settings.allowed_iframe_hosts
assert "vimeo.com" in app_settings.allowed_iframe_hosts
def test_allowed_iframe_hosts_extends_defaults(monkeypatch):
monkeypatch.setenv("ALLOWED_IFRAME_HOSTS", " Example.com , trusted.tld ,, ")
get_app_settings.cache_clear()
app_settings = get_app_settings()
hosts = app_settings.allowed_iframe_hosts
# Configured hosts are normalized, blanks dropped, and defaults retained.
assert "example.com" in hosts
assert "trusted.tld" in hosts
assert "youtube.com" in hosts
assert "" not in hosts
# No duplicates.
assert len(hosts) == len(set(hosts))
def test_default_connection_args(monkeypatch):
monkeypatch.setenv("DB_ENGINE", "sqlite")
get_app_settings.cache_clear()

74
uv.lock generated
View File

@@ -2,6 +2,10 @@ version = 1
revision = 3
requires-python = "==3.12.*"
[options]
exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
exclude-newer-span = "P5D"
[[package]]
name = "aiofiles"
version = "25.1.0"
@@ -25,15 +29,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" },
]
[[package]]
name = "aniso8601"
version = "10.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/52179c4e3f1978d3d9a285f98c706642522750ef343e9738286130423730/aniso8601-10.0.1.tar.gz", hash = "sha256:25488f8663dd1528ae1f54f94ac1ea51ae25b4d531539b8bc707fed184d16845", size = 47190, upload-time = "2025-04-18T17:29:42.995Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/59/75/e0e10dc7ed1408c28e03a6cb2d7a407f99320eb953f229d008a7a6d05546/aniso8601-10.0.1-py2.py3-none-any.whl", hash = "sha256:eb19717fd4e0db6de1aab06f12450ab92144246b257423fe020af5748c0cb89e", size = 52848, upload-time = "2025-04-18T17:29:41.492Z" },
]
[[package]]
name = "annotated-doc"
version = "0.0.4"
@@ -65,15 +60,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
]
[[package]]
name = "appdirs"
version = "1.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470, upload-time = "2020-05-11T07:59:51.037Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566, upload-time = "2020-05-11T07:59:49.499Z" },
]
[[package]]
name = "apprise"
version = "1.10.0"
@@ -902,8 +888,6 @@ source = { editable = "." }
dependencies = [
{ name = "aiofiles" },
{ name = "alembic" },
{ name = "aniso8601" },
{ name = "appdirs" },
{ name = "apprise" },
{ name = "authlib" },
{ name = "bcrypt" },
@@ -978,8 +962,6 @@ docs = [
requires-dist = [
{ name = "aiofiles", specifier = "==25.1.0" },
{ name = "alembic", specifier = "==1.18.4" },
{ name = "aniso8601", specifier = "==10.0.1" },
{ name = "appdirs", specifier = "==1.4.4" },
{ name = "apprise", specifier = "==1.10.0" },
{ name = "authlib", specifier = "==1.7.2" },
{ name = "bcrypt", specifier = "==5.0.0" },
@@ -1034,9 +1016,9 @@ dev = [
{ name = "pydantic-to-typescript2", specifier = "==1.0.6" },
{ name = "pylint", specifier = "==4.0.5" },
{ name = "pytest", specifier = "==9.0.3" },
{ name = "pytest-asyncio", specifier = "==1.3.0" },
{ name = "pytest-asyncio", specifier = "==1.4.0" },
{ name = "rich", specifier = "==15.0.0" },
{ name = "ruff", specifier = "==0.15.14" },
{ name = "ruff", specifier = "==0.15.15" },
{ name = "types-python-dateutil", specifier = "==2.9.0.20260518" },
{ name = "types-python-slugify", specifier = "==8.0.2.20240310" },
{ name = "types-pyyaml", specifier = "==6.0.12.20260518" },
@@ -1601,15 +1583,15 @@ wheels = [
[[package]]
name = "pytest-asyncio"
version = "1.3.0"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
{ url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" },
]
[[package]]
@@ -1819,27 +1801,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.15.14"
version = "0.15.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/8a/8bce2894573e9dae6ff4d77fe34ad727d79b9e6238ad288c5638990d90f6/ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f", size = 4700910, upload-time = "2026-05-21T14:34:55.177Z" }
sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" },
{ url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" },
{ url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" },
{ url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" },
{ url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" },
{ url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" },
{ url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" },
{ url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" },
{ url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" },
{ url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" },
{ url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" },
{ url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" },
{ url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" },
{ url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" },
{ url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" },
{ url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" },
{ url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" },
{ url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" },
{ url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" },
{ url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" },
{ url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" },
{ url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" },
{ url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" },
{ url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" },
{ url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" },
{ url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" },
{ url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" },
{ url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" },
{ url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" },
{ url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" },
{ url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" },
{ url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" },
{ url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" },
{ url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
]
[[package]]