Merge remote-tracking branch 'upstream/main' into config-export-feature
This commit is contained in:
parent
34b9e4f6fb
commit
512bee535a
91 changed files with 4213 additions and 499 deletions
1
.env.test
Normal file
1
.env.test
Normal file
|
|
@ -0,0 +1 @@
|
|||
DATABASE_URL=:memory:
|
||||
15
.github/actions/install-dependencies/action.yml
vendored
Normal file
15
.github/actions/install-dependencies/action.yml
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
name: Install dependencies
|
||||
|
||||
description: Install dependencies
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
name: Install Bun
|
||||
with:
|
||||
bun-version: "1.3.5"
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: bun install --frozen-lockfile
|
||||
18
.github/dependabot.yml
vendored
Normal file
18
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "bun"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
rebase-strategy: 'auto'
|
||||
groups:
|
||||
minor-patch:
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
rebase-strategy: 'auto'
|
||||
37
.github/workflows/ci.yml
vendored
Normal file
37
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
name: Checks
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install dependencies
|
||||
uses: "./.github/actions/install-dependencies"
|
||||
|
||||
- name: Run type checks
|
||||
shell: bash
|
||||
run: bun run tsc
|
||||
|
||||
- name: Run tests
|
||||
shell: bash
|
||||
run: bun run test --ci --coverage
|
||||
|
||||
- name: Build project
|
||||
shell: bash
|
||||
run: bun run build
|
||||
47
.github/workflows/release.yml
vendored
47
.github/workflows/release.yml
vendored
|
|
@ -7,6 +7,11 @@ on:
|
|||
- "v*.*.*-beta.*"
|
||||
- "v*.*.*-alpha.*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
determine-release-type:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -32,16 +37,23 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: cloud
|
||||
endpoint: "meienberger/runtipi-builder"
|
||||
install: true
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
|
|
@ -50,6 +62,31 @@ jobs:
|
|||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
target: production
|
||||
platforms: linux/amd64
|
||||
push: false
|
||||
load: true
|
||||
tags: local/zerobyte:ci
|
||||
build-args: |
|
||||
APP_VERSION=${{ needs.determine-release-type.outputs.tagname }}
|
||||
|
||||
- name: Scan new image for vulnerabilities
|
||||
uses: anchore/scan-action@v7
|
||||
id: scan
|
||||
with:
|
||||
image: local/zerobyte:ci
|
||||
fail-build: true
|
||||
severity-cutoff: critical
|
||||
|
||||
- name: upload Anchore scan report
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
with:
|
||||
sarif_file: ${{ steps.scan.outputs.sarif }}
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
|
|
@ -63,7 +100,7 @@ jobs:
|
|||
flavor: |
|
||||
latest=${{ needs.determine-release-type.outputs.release_type == 'release' }}
|
||||
|
||||
- name: Build and push images
|
||||
- name: Push images to GitHub Container Registry
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
|
|
@ -74,8 +111,6 @@ jobs:
|
|||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
APP_VERSION=${{ needs.determine-release-type.outputs.tagname }}
|
||||
cache-from: type=registry,ref=ghcr.io/nicotsx/zerobyte:buildcache
|
||||
cache-to: type=registry,ref=ghcr.io/nicotsx/zerobyte:buildcache,mode=max
|
||||
|
||||
publish-release:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
10
AGENTS.md
10
AGENTS.md
|
|
@ -36,6 +36,16 @@ This is a unified application with the following structure:
|
|||
bun run tsc
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
bun run test
|
||||
|
||||
# Run a specific test file
|
||||
bunx dotenv-cli -e .env.test -- bun test --preload ./app/test/setup.ts path/to/test.ts
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
|
|
|
|||
18
Dockerfile
18
Dockerfile
|
|
@ -1,8 +1,9 @@
|
|||
ARG BUN_VERSION="1.3.3"
|
||||
ARG BUN_VERSION="1.3.5"
|
||||
|
||||
FROM oven/bun:${BUN_VERSION}-alpine AS base
|
||||
|
||||
RUN apk add --no-cache davfs2=1.6.1-r2 openssh-client fuse3
|
||||
RUN apk upgrade --no-cache && \
|
||||
apk add --no-cache davfs2=1.6.1-r2 openssh-client fuse3
|
||||
|
||||
|
||||
# ------------------------------
|
||||
|
|
@ -14,7 +15,8 @@ WORKDIR /deps
|
|||
|
||||
ARG TARGETARCH
|
||||
ARG RESTIC_VERSION="0.18.1"
|
||||
ARG SHOUTRRR_VERSION="0.12.1"
|
||||
ARG RCLONE_VERSION="1.72.1"
|
||||
ARG SHOUTRRR_VERSION="0.13.1"
|
||||
ENV TARGETARCH=${TARGETARCH}
|
||||
|
||||
RUN apk add --no-cache curl bzip2 unzip tar
|
||||
|
|
@ -22,18 +24,18 @@ RUN apk add --no-cache curl bzip2 unzip tar
|
|||
RUN echo "Building for ${TARGETARCH}"
|
||||
RUN if [ "${TARGETARCH}" = "arm64" ]; then \
|
||||
curl -L -o restic.bz2 "https://github.com/restic/restic/releases/download/v$RESTIC_VERSION/restic_$RESTIC_VERSION"_linux_arm64.bz2; \
|
||||
curl -O https://downloads.rclone.org/rclone-current-linux-arm64.zip; \
|
||||
unzip rclone-current-linux-arm64.zip; \
|
||||
curl -L -o rclone.zip "https://github.com/rclone/rclone/releases/download/v$RCLONE_VERSION/rclone-v$RCLONE_VERSION-linux-arm64.zip"; \
|
||||
unzip rclone.zip; \
|
||||
curl -L -o shoutrrr.tar.gz "https://github.com/nicholas-fedor/shoutrrr/releases/download/v$SHOUTRRR_VERSION/shoutrrr_linux_arm64v8_${SHOUTRRR_VERSION}.tar.gz"; \
|
||||
elif [ "${TARGETARCH}" = "amd64" ]; then \
|
||||
curl -L -o restic.bz2 "https://github.com/restic/restic/releases/download/v$RESTIC_VERSION/restic_$RESTIC_VERSION"_linux_amd64.bz2; \
|
||||
curl -O https://downloads.rclone.org/rclone-current-linux-amd64.zip; \
|
||||
unzip rclone-current-linux-amd64.zip; \
|
||||
curl -L -o rclone.zip "https://github.com/rclone/rclone/releases/download/v$RCLONE_VERSION/rclone-v$RCLONE_VERSION-linux-amd64.zip"; \
|
||||
unzip rclone.zip; \
|
||||
curl -L -o shoutrrr.tar.gz "https://github.com/nicholas-fedor/shoutrrr/releases/download/v$SHOUTRRR_VERSION/shoutrrr_linux_amd64_${SHOUTRRR_VERSION}.tar.gz"; \
|
||||
fi
|
||||
|
||||
RUN bzip2 -d restic.bz2 && chmod +x restic
|
||||
RUN mv rclone-*-linux-*/rclone /deps/rclone && chmod +x /deps/rclone
|
||||
RUN mv rclone-v*-linux-*/rclone /deps/rclone && chmod +x /deps/rclone
|
||||
RUN tar -xzf shoutrrr.tar.gz && chmod +x shoutrrr
|
||||
|
||||
# ------------------------------
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -56,6 +56,9 @@ services:
|
|||
- /var/lib/zerobyte:/var/lib/zerobyte
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> It is highly discouraged to run Zerobyte on a server that is accessible from the internet (VPS or home server with port forwarding) If you do, make sure to change the port mapping to "127.0.0.1:4096:4096" and use a secure tunnel (SSH tunnel, Cloudflare Tunnel, etc.) with authentication.
|
||||
|
||||
> [!WARNING]
|
||||
> Do not try to point `/var/lib/zerobyte` on a network share. You will face permission issues and strong performance degradation.
|
||||
|
||||
|
|
@ -95,6 +98,10 @@ services:
|
|||
|
||||
If you need remote mount capabilities, keep the original configuration with `cap_add: SYS_ADMIN` and `devices: /dev/fuse:/dev/fuse`.
|
||||
|
||||
## Examples
|
||||
|
||||
See [examples/README.md](examples/README.md) for runnable, copy/paste-friendly examples.
|
||||
|
||||
## Adding your first volume
|
||||
|
||||
Zerobyte supports multiple volume backends including NFS, SMB, WebDAV, and local directories. A volume represents the source data you want to back up and monitor.
|
||||
|
|
@ -155,22 +162,27 @@ Zerobyte can use [rclone](https://rclone.org/) to support 40+ cloud storage prov
|
|||
**Setup instructions:**
|
||||
|
||||
1. **Install rclone on your host system** (if not already installed):
|
||||
|
||||
```bash
|
||||
curl https://rclone.org/install.sh | sudo bash
|
||||
```
|
||||
|
||||
2. **Configure your cloud storage remote** using rclone's interactive config:
|
||||
|
||||
```bash
|
||||
rclone config
|
||||
```
|
||||
|
||||
Follow the prompts to set up your cloud storage provider. For OAuth providers (Google Drive, Dropbox, etc.), rclone will guide you through the authentication flow.
|
||||
|
||||
3. **Verify your remote is configured**:
|
||||
|
||||
```bash
|
||||
rclone listremotes
|
||||
```
|
||||
|
||||
4. **Mount the rclone config into the Zerobyte container** by updating your `docker-compose.yml`:
|
||||
|
||||
```diff
|
||||
services:
|
||||
zerobyte:
|
||||
|
|
@ -192,6 +204,7 @@ Zerobyte can use [rclone](https://rclone.org/) to support 40+ cloud storage prov
|
|||
```
|
||||
|
||||
5. **Restart the Zerobyte container**:
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
|
|
@ -209,6 +222,7 @@ For a complete list of supported providers, see the [rclone documentation](https
|
|||
Once you have added a volume and created a repository, you can create your first backup job. A backup job defines the schedule and parameters for backing up a specific volume to a designated repository.
|
||||
|
||||
When creating a backup job, you can specify the following settings:
|
||||
|
||||
- **Schedule**: Define how often the backup should run (e.g., daily, weekly)
|
||||
- **Retention Policy**: Set rules for how long backups should be retained (e.g., keep daily backups for 7 days, weekly backups for 4 weeks)
|
||||
- **Paths**: Specify which files or directories to include in the backup
|
||||
|
|
|
|||
|
|
@ -169,6 +169,8 @@ export const createSseClient = <TData = unknown>({
|
|||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += value;
|
||||
// Normalize line endings: CRLF -> LF, then CR -> LF
|
||||
buffer = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
|
||||
const chunks = buffer.split('\n\n');
|
||||
buffer = chunks.pop() ?? '';
|
||||
|
|
|
|||
|
|
@ -1305,6 +1305,7 @@ export type ListBackupSchedulesResponses = {
|
|||
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
|
||||
name: string;
|
||||
nextBackupAt: number | null;
|
||||
oneFileSystem: boolean;
|
||||
repository: {
|
||||
compressionMode: 'auto' | 'max' | 'off' | null;
|
||||
config: {
|
||||
|
|
@ -1453,6 +1454,7 @@ export type CreateBackupScheduleData = {
|
|||
excludeIfPresent?: Array<string>;
|
||||
excludePatterns?: Array<string>;
|
||||
includePatterns?: Array<string>;
|
||||
oneFileSystem?: boolean;
|
||||
retentionPolicy?: {
|
||||
keepDaily?: number;
|
||||
keepHourly?: number;
|
||||
|
|
@ -1486,6 +1488,7 @@ export type CreateBackupScheduleResponses = {
|
|||
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
|
||||
name: string;
|
||||
nextBackupAt: number | null;
|
||||
oneFileSystem: boolean;
|
||||
repositoryId: string;
|
||||
retentionPolicy: {
|
||||
keepDaily?: number;
|
||||
|
|
@ -1549,6 +1552,7 @@ export type GetBackupScheduleResponses = {
|
|||
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
|
||||
name: string;
|
||||
nextBackupAt: number | null;
|
||||
oneFileSystem: boolean;
|
||||
repository: {
|
||||
compressionMode: 'auto' | 'max' | 'off' | null;
|
||||
config: {
|
||||
|
|
@ -1696,6 +1700,7 @@ export type UpdateBackupScheduleData = {
|
|||
excludePatterns?: Array<string>;
|
||||
includePatterns?: Array<string>;
|
||||
name?: string;
|
||||
oneFileSystem?: boolean;
|
||||
retentionPolicy?: {
|
||||
keepDaily?: number;
|
||||
keepHourly?: number;
|
||||
|
|
@ -1731,6 +1736,7 @@ export type UpdateBackupScheduleResponses = {
|
|||
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
|
||||
name: string;
|
||||
nextBackupAt: number | null;
|
||||
oneFileSystem: boolean;
|
||||
repositoryId: string;
|
||||
retentionPolicy: {
|
||||
keepDaily?: number;
|
||||
|
|
@ -1774,6 +1780,7 @@ export type GetBackupScheduleForVolumeResponses = {
|
|||
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
|
||||
name: string;
|
||||
nextBackupAt: number | null;
|
||||
oneFileSystem: boolean;
|
||||
repository: {
|
||||
compressionMode: 'auto' | 'max' | 'off' | null;
|
||||
config: {
|
||||
|
|
|
|||
|
|
@ -46,10 +46,15 @@ const items = [
|
|||
|
||||
export function AppSidebar() {
|
||||
const { state } = useSidebar();
|
||||
const displayVersion = APP_VERSION.startsWith("v") || APP_VERSION === "dev" ? APP_VERSION : `v${APP_VERSION}`;
|
||||
const releaseUrl =
|
||||
APP_VERSION === "dev"
|
||||
? "https://github.com/nicotsx/zerobyte"
|
||||
: `https://github.com/nicotsx/zerobyte/releases/tag/${displayVersion}`;
|
||||
|
||||
return (
|
||||
<Sidebar variant="inset" collapsible="icon" className="p-0">
|
||||
<SidebarHeader className="bg-card-header border-b border-border/50 hidden md:flex h-[65px] flex-row items-center p-4">
|
||||
<SidebarHeader className="bg-card-header border-b border-border/50 hidden md:flex h-16.25 flex-row items-center p-4">
|
||||
<Link to="/volumes" className="flex items-center gap-3 font-semibold pl-2">
|
||||
<img src="/images/zerobyte.png" alt="Zerobyte Logo" className={cn("h-8 w-8 shrink-0 object-contain -ml-2")} />
|
||||
<span
|
||||
|
|
@ -93,13 +98,16 @@ export function AppSidebar() {
|
|||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarFooter className="p-4 border-r border-t border-border/50">
|
||||
<div
|
||||
className={cn("text-xs text-muted-foreground transition-all duration-200", {
|
||||
<a
|
||||
href={releaseUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={cn("text-xs text-muted-foreground transition-all duration-200 hover:text-foreground", {
|
||||
"opacity-0 w-0 overflow-hidden": state === "collapsed",
|
||||
})}
|
||||
>
|
||||
{APP_VERSION}
|
||||
</div>
|
||||
{displayVersion}
|
||||
</a>
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { arktypeResolver } from "@hookform/resolvers/arktype";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { type } from "arktype";
|
||||
import { useCallback, useState } from "react";
|
||||
|
|
@ -6,6 +7,7 @@ import { useForm } from "react-hook-form";
|
|||
import { listRepositoriesOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import { RepositoryIcon } from "~/client/components/repository-icon";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||
import { Checkbox } from "~/client/components/ui/checkbox";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
|
|
@ -17,6 +19,7 @@ import {
|
|||
} from "~/client/components/ui/form";
|
||||
import { Input } from "~/client/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||
import { Button } from "~/client/components/ui/button";
|
||||
import { Textarea } from "~/client/components/ui/textarea";
|
||||
import { VolumeFileBrowser } from "~/client/components/volume-file-browser";
|
||||
import type { BackupSchedule, Volume } from "~/client/lib/types";
|
||||
|
|
@ -32,12 +35,14 @@ const internalFormSchema = type({
|
|||
frequency: "string",
|
||||
dailyTime: "string?",
|
||||
weeklyDay: "string?",
|
||||
monthlyDays: "string[]?",
|
||||
keepLast: "number?",
|
||||
keepHourly: "number?",
|
||||
keepDaily: "number?",
|
||||
keepWeekly: "number?",
|
||||
keepMonthly: "number?",
|
||||
keepYearly: "number?",
|
||||
oneFileSystem: "boolean?",
|
||||
});
|
||||
const cleanSchema = type.pipe((d) => internalFormSchema(deepClean(d)));
|
||||
|
||||
|
|
@ -76,15 +81,16 @@ const backupScheduleToFormValues = (schedule?: BackupSchedule): InternalFormValu
|
|||
}
|
||||
|
||||
const parts = schedule.cronExpression.split(" ");
|
||||
const [minutePart, hourPart, , , dayOfWeekPart] = parts;
|
||||
const [minutePart, hourPart, dayOfMonthPart, , dayOfWeekPart] = parts;
|
||||
|
||||
const isHourly = hourPart === "*";
|
||||
const isDaily = !isHourly && dayOfWeekPart === "*";
|
||||
const frequency = isHourly ? "hourly" : isDaily ? "daily" : "weekly";
|
||||
const isMonthly = !isHourly && dayOfMonthPart !== "*" && dayOfWeekPart === "*";
|
||||
const isDaily = !isHourly && dayOfMonthPart === "*" && dayOfWeekPart === "*";
|
||||
|
||||
const frequency = isHourly ? "hourly" : isMonthly ? "monthly" : isDaily ? "daily" : "weekly";
|
||||
const dailyTime = isHourly ? undefined : `${hourPart.padStart(2, "0")}:${minutePart.padStart(2, "0")}`;
|
||||
|
||||
const weeklyDay = frequency === "weekly" ? dayOfWeekPart : undefined;
|
||||
const monthlyDays = isMonthly ? dayOfMonthPart.split(",") : undefined;
|
||||
|
||||
const patterns = schedule.includePatterns || [];
|
||||
const isGlobPattern = (p: string) => /[*?[\]]/.test(p);
|
||||
|
|
@ -95,12 +101,14 @@ const backupScheduleToFormValues = (schedule?: BackupSchedule): InternalFormValu
|
|||
name: schedule.name,
|
||||
repositoryId: schedule.repositoryId,
|
||||
frequency,
|
||||
monthlyDays,
|
||||
dailyTime,
|
||||
weeklyDay,
|
||||
includePatterns: fileBrowserPaths.length > 0 ? fileBrowserPaths : undefined,
|
||||
includePatternsText: textPatterns.length > 0 ? textPatterns.join("\n") : undefined,
|
||||
excludePatternsText: schedule.excludePatterns?.join("\n") || undefined,
|
||||
excludeIfPresentText: schedule.excludeIfPresent?.join("\n") || undefined,
|
||||
oneFileSystem: schedule.oneFileSystem ?? false,
|
||||
...schedule.retentionPolicy,
|
||||
};
|
||||
};
|
||||
|
|
@ -246,6 +254,7 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
|||
<SelectItem value="hourly">Hourly</SelectItem>
|
||||
<SelectItem value="daily">Daily</SelectItem>
|
||||
<SelectItem value="weekly">Weekly</SelectItem>
|
||||
<SelectItem value="monthly">Specific days</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
|
@ -299,6 +308,42 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
|||
)}
|
||||
/>
|
||||
)}
|
||||
{frequency === "monthly" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="monthlyDays"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2">
|
||||
<FormLabel>Days of the month</FormLabel>
|
||||
<FormControl>
|
||||
<div className="grid grid-cols-7 gap-4 w-max">
|
||||
{Array.from({ length: 31 }, (_, i) => {
|
||||
const day = (i + 1).toString();
|
||||
const isSelected = field.value?.includes(day);
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
key={day}
|
||||
variant={isSelected ? "primary" : "secondary"}
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
const current = field.value || [];
|
||||
const next = isSelected ? current.filter((d) => d !== day) : [...current, day];
|
||||
field.onChange(next);
|
||||
}}
|
||||
>
|
||||
{day}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormDescription>Select one or more days when the backup should run.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
|
@ -318,7 +363,7 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
|||
onSelectionChange={handleSelectionChange}
|
||||
withCheckboxes={true}
|
||||
foldersOnly={false}
|
||||
className="flex-1 border rounded-md bg-card p-2 min-h-[300px] max-h-[400px] overflow-auto"
|
||||
className="flex-1 border rounded-md bg-card p-2 min-h-75 max-h-100 overflow-auto"
|
||||
/>
|
||||
{selectedPaths.size > 0 && (
|
||||
<div className="mt-4">
|
||||
|
|
@ -342,7 +387,7 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
|||
<Textarea
|
||||
{...field}
|
||||
placeholder="/data/** /config/*.json *.db"
|
||||
className="font-mono text-sm min-h-[100px]"
|
||||
className="font-mono text-sm min-h-25"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
|
|
@ -375,7 +420,7 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
|||
<Textarea
|
||||
{...field}
|
||||
placeholder="*.tmp node_modules/** .cache/ *.log"
|
||||
className="font-mono text-sm min-h-[120px]"
|
||||
className="font-mono text-sm min-h-30"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
|
|
@ -416,6 +461,24 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="oneFileSystem"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4 mt-6">
|
||||
<FormControl>
|
||||
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>Stay on one file system</FormLabel>
|
||||
<FormDescription>
|
||||
Prevent Restic from crossing file system boundaries. This is useful to avoid backing up network
|
||||
mounts or other partitions that might be mounted inside your backup source.
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
|
@ -629,6 +692,10 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="text-xs uppercase text-muted-foreground">One file system</p>
|
||||
<p className="font-medium">{formValues.oneFileSystem ? "Enabled" : "Disabled"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs uppercase text-muted-foreground">Retention</p>
|
||||
<p className="font-medium">
|
||||
|
|
|
|||
|
|
@ -181,7 +181,8 @@ export const ScheduleSummary = (props: Props) => {
|
|||
<div className="md:col-span-2 lg:col-span-4">
|
||||
<p className="text-xs uppercase text-muted-foreground">Warning Details</p>
|
||||
<p className="font-mono text-sm text-yellow-600 whitespace-pre-wrap break-all">
|
||||
Last backup completed with warnings. Check your container logs for more details.
|
||||
{schedule.lastBackupError ??
|
||||
"Last backup completed with warnings. Check your container logs for more details."}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -141,7 +141,12 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
|||
const handleSubmit = (formValues: BackupScheduleFormValues) => {
|
||||
if (!schedule) return;
|
||||
|
||||
const cronExpression = getCronExpression(formValues.frequency, formValues.dailyTime, formValues.weeklyDay);
|
||||
const cronExpression = getCronExpression(
|
||||
formValues.frequency,
|
||||
formValues.dailyTime,
|
||||
formValues.weeklyDay,
|
||||
formValues.monthlyDays,
|
||||
);
|
||||
|
||||
const retentionPolicy: Record<string, number> = {};
|
||||
if (formValues.keepLast) retentionPolicy.keepLast = formValues.keepLast;
|
||||
|
|
@ -162,6 +167,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
|||
includePatterns: formValues.includePatterns,
|
||||
excludePatterns: formValues.excludePatterns,
|
||||
excludeIfPresent: formValues.excludeIfPresent,
|
||||
oneFileSystem: formValues.oneFileSystem,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -177,6 +183,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
|||
includePatterns: schedule.includePatterns || [],
|
||||
excludePatterns: schedule.excludePatterns || [],
|
||||
excludeIfPresent: schedule.excludeIfPresent || [],
|
||||
oneFileSystem: schedule.oneFileSystem,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -71,7 +71,12 @@ export default function CreateBackup({ loaderData }: Route.ComponentProps) {
|
|||
const handleSubmit = (formValues: BackupScheduleFormValues) => {
|
||||
if (!selectedVolumeId) return;
|
||||
|
||||
const cronExpression = getCronExpression(formValues.frequency, formValues.dailyTime, formValues.weeklyDay);
|
||||
const cronExpression = getCronExpression(
|
||||
formValues.frequency,
|
||||
formValues.dailyTime,
|
||||
formValues.weeklyDay,
|
||||
formValues.monthlyDays,
|
||||
);
|
||||
|
||||
const retentionPolicy: Record<string, number> = {};
|
||||
if (formValues.keepLast) retentionPolicy.keepLast = formValues.keepLast;
|
||||
|
|
@ -92,6 +97,7 @@ export default function CreateBackup({ loaderData }: Route.ComponentProps) {
|
|||
includePatterns: formValues.includePatterns,
|
||||
excludePatterns: formValues.excludePatterns,
|
||||
excludeIfPresent: formValues.excludeIfPresent,
|
||||
oneFileSystem: formValues.oneFileSystem,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@ import { Input } from "~/client/components/ui/input";
|
|||
import { SecretInput } from "~/client/components/ui/secret-input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||
import { Checkbox } from "~/client/components/ui/checkbox";
|
||||
import { notificationConfigSchema } from "~/schemas/notifications";
|
||||
import { notificationConfigSchemaBase } from "~/schemas/notifications";
|
||||
|
||||
export const formSchema = type({
|
||||
name: "2<=string<=32",
|
||||
}).and(notificationConfigSchema);
|
||||
}).and(notificationConfigSchemaBase);
|
||||
const cleanSchema = type.pipe((d) => formSchema(deepClean(d)));
|
||||
|
||||
export type NotificationFormValues = typeof formSchema.inferIn;
|
||||
|
|
|
|||
|
|
@ -172,7 +172,15 @@ export default function NotificationDetailsPage({ loaderData }: Route.ComponentP
|
|||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<CreateNotificationForm mode="update" formId={formId} onSubmit={handleSubmit} initialValues={data.config} />
|
||||
<CreateNotificationForm
|
||||
mode="update"
|
||||
formId={formId}
|
||||
onSubmit={handleSubmit}
|
||||
initialValues={{
|
||||
...data.config,
|
||||
name: data.name,
|
||||
}}
|
||||
/>
|
||||
<div className="flex justify-end gap-2 pt-4 border-t">
|
||||
<Button type="submit" form={formId} loading={updateDestination.isPending}>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { SecretInput } from "../../../components/ui/secret-input";
|
|||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../components/ui/select";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../../../components/ui/tooltip";
|
||||
import { useSystemInfo } from "~/client/hooks/use-system-info";
|
||||
import { COMPRESSION_MODES, repositoryConfigSchema } from "~/schemas/restic";
|
||||
import { COMPRESSION_MODES, repositoryConfigSchemaBase } from "~/schemas/restic";
|
||||
import { Checkbox } from "../../../components/ui/checkbox";
|
||||
import {
|
||||
LocalRepositoryForm,
|
||||
|
|
@ -36,7 +36,7 @@ import {
|
|||
export const formSchema = type({
|
||||
name: "2<=string<=32",
|
||||
compressionMode: type.valueOf(COMPRESSION_MODES).optional(),
|
||||
}).and(repositoryConfigSchema);
|
||||
}).and(repositoryConfigSchemaBase);
|
||||
const cleanSchema = type.pipe((d) => formSchema(deepClean(d)));
|
||||
|
||||
export type RepositoryFormValues = typeof formSchema.inferIn;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import {
|
|||
} from "../../../components/ui/form";
|
||||
import { Input } from "../../../components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../components/ui/select";
|
||||
import { volumeConfigSchema } from "~/schemas/volumes";
|
||||
import { volumeConfigSchemaBase } from "~/schemas/volumes";
|
||||
import { testConnectionMutation } from "../../../api-client/@tanstack/react-query.gen";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../../../components/ui/tooltip";
|
||||
import { useSystemInfo } from "~/client/hooks/use-system-info";
|
||||
|
|
@ -26,7 +26,7 @@ import { DirectoryForm, NFSForm, SMBForm, WebDAVForm, RcloneForm } from "./volum
|
|||
|
||||
export const formSchema = type({
|
||||
name: "2<=string<=32",
|
||||
}).and(volumeConfigSchema);
|
||||
}).and(volumeConfigSchemaBase);
|
||||
const cleanSchema = type.pipe((d) => formSchema(deepClean(d)));
|
||||
|
||||
export type FormValues = typeof formSchema.inferIn;
|
||||
|
|
|
|||
2
app/drizzle/0023_special_thor.sql
Normal file
2
app/drizzle/0023_special_thor.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE `backup_schedules_table` ADD `one_file_system` integer DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
UPDATE `backup_schedules_table` SET `one_file_system` = true;
|
||||
1
app/drizzle/0024_schedules-one-fs.sql
Normal file
1
app/drizzle/0024_schedules-one-fs.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
UPDATE `backup_schedules_table` SET `one_file_system` = 1;
|
||||
839
app/drizzle/meta/0023_snapshot.json
Normal file
839
app/drizzle/meta/0023_snapshot.json
Normal file
|
|
@ -0,0 +1,839 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "3e3841ca-67a8-493a-a061-9c2a780878ed",
|
||||
"prevId": "11c24867-3186-4578-b8dd-cee4c48a28d1",
|
||||
"tables": {
|
||||
"app_metadata": {
|
||||
"name": "app_metadata",
|
||||
"columns": {
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"backup_schedule_mirrors_table": {
|
||||
"name": "backup_schedule_mirrors_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"schedule_id": {
|
||||
"name": "schedule_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"repository_id": {
|
||||
"name": "repository_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"last_copy_at": {
|
||||
"name": "last_copy_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_copy_status": {
|
||||
"name": "last_copy_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_copy_error": {
|
||||
"name": "last_copy_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"backup_schedule_mirrors_table_schedule_id_repository_id_unique": {
|
||||
"name": "backup_schedule_mirrors_table_schedule_id_repository_id_unique",
|
||||
"columns": [
|
||||
"schedule_id",
|
||||
"repository_id"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk": {
|
||||
"name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk",
|
||||
"tableFrom": "backup_schedule_mirrors_table",
|
||||
"tableTo": "backup_schedules_table",
|
||||
"columnsFrom": [
|
||||
"schedule_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"backup_schedule_mirrors_table_repository_id_repositories_table_id_fk": {
|
||||
"name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk",
|
||||
"tableFrom": "backup_schedule_mirrors_table",
|
||||
"tableTo": "repositories_table",
|
||||
"columnsFrom": [
|
||||
"repository_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"backup_schedule_notifications_table": {
|
||||
"name": "backup_schedule_notifications_table",
|
||||
"columns": {
|
||||
"schedule_id": {
|
||||
"name": "schedule_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"destination_id": {
|
||||
"name": "destination_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"notify_on_start": {
|
||||
"name": "notify_on_start",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"notify_on_success": {
|
||||
"name": "notify_on_success",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"notify_on_warning": {
|
||||
"name": "notify_on_warning",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"notify_on_failure": {
|
||||
"name": "notify_on_failure",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk": {
|
||||
"name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk",
|
||||
"tableFrom": "backup_schedule_notifications_table",
|
||||
"tableTo": "backup_schedules_table",
|
||||
"columnsFrom": [
|
||||
"schedule_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk": {
|
||||
"name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk",
|
||||
"tableFrom": "backup_schedule_notifications_table",
|
||||
"tableTo": "notification_destinations_table",
|
||||
"columnsFrom": [
|
||||
"destination_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"backup_schedule_notifications_table_schedule_id_destination_id_pk": {
|
||||
"columns": [
|
||||
"schedule_id",
|
||||
"destination_id"
|
||||
],
|
||||
"name": "backup_schedule_notifications_table_schedule_id_destination_id_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"backup_schedules_table": {
|
||||
"name": "backup_schedules_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"volume_id": {
|
||||
"name": "volume_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"repository_id": {
|
||||
"name": "repository_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"cron_expression": {
|
||||
"name": "cron_expression",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"retention_policy": {
|
||||
"name": "retention_policy",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"exclude_patterns": {
|
||||
"name": "exclude_patterns",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"exclude_if_present": {
|
||||
"name": "exclude_if_present",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"include_patterns": {
|
||||
"name": "include_patterns",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"last_backup_at": {
|
||||
"name": "last_backup_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_backup_status": {
|
||||
"name": "last_backup_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_backup_error": {
|
||||
"name": "last_backup_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"next_backup_at": {
|
||||
"name": "next_backup_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"one_file_system": {
|
||||
"name": "one_file_system",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"sort_order": {
|
||||
"name": "sort_order",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"backup_schedules_table_name_unique": {
|
||||
"name": "backup_schedules_table_name_unique",
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"backup_schedules_table_volume_id_volumes_table_id_fk": {
|
||||
"name": "backup_schedules_table_volume_id_volumes_table_id_fk",
|
||||
"tableFrom": "backup_schedules_table",
|
||||
"tableTo": "volumes_table",
|
||||
"columnsFrom": [
|
||||
"volume_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"backup_schedules_table_repository_id_repositories_table_id_fk": {
|
||||
"name": "backup_schedules_table_repository_id_repositories_table_id_fk",
|
||||
"tableFrom": "backup_schedules_table",
|
||||
"tableTo": "repositories_table",
|
||||
"columnsFrom": [
|
||||
"repository_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"notification_destinations_table": {
|
||||
"name": "notification_destinations_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"config": {
|
||||
"name": "config",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"notification_destinations_table_name_unique": {
|
||||
"name": "notification_destinations_table_name_unique",
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"repositories_table": {
|
||||
"name": "repositories_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"short_id": {
|
||||
"name": "short_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"config": {
|
||||
"name": "config",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"compression_mode": {
|
||||
"name": "compression_mode",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'auto'"
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'unknown'"
|
||||
},
|
||||
"last_checked": {
|
||||
"name": "last_checked",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_error": {
|
||||
"name": "last_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"repositories_table_short_id_unique": {
|
||||
"name": "repositories_table_short_id_unique",
|
||||
"columns": [
|
||||
"short_id"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"repositories_table_name_unique": {
|
||||
"name": "repositories_table_name_unique",
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"sessions_table": {
|
||||
"name": "sessions_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"sessions_table_user_id_users_table_id_fk": {
|
||||
"name": "sessions_table_user_id_users_table_id_fk",
|
||||
"tableFrom": "sessions_table",
|
||||
"tableTo": "users_table",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users_table": {
|
||||
"name": "users_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password_hash": {
|
||||
"name": "password_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"has_downloaded_restic_password": {
|
||||
"name": "has_downloaded_restic_password",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_table_username_unique": {
|
||||
"name": "users_table_username_unique",
|
||||
"columns": [
|
||||
"username"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"volumes_table": {
|
||||
"name": "volumes_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"short_id": {
|
||||
"name": "short_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'unmounted'"
|
||||
},
|
||||
"last_error": {
|
||||
"name": "last_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_health_check": {
|
||||
"name": "last_health_check",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"config": {
|
||||
"name": "config",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"auto_remount": {
|
||||
"name": "auto_remount",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"volumes_table_short_id_unique": {
|
||||
"name": "volumes_table_short_id_unique",
|
||||
"columns": [
|
||||
"short_id"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"volumes_table_name_unique": {
|
||||
"name": "volumes_table_name_unique",
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
839
app/drizzle/meta/0024_snapshot.json
Normal file
839
app/drizzle/meta/0024_snapshot.json
Normal file
|
|
@ -0,0 +1,839 @@
|
|||
{
|
||||
"id": "f19cb32f-2280-42dd-a86a-aba7c0409d9f",
|
||||
"prevId": "3e3841ca-67a8-493a-a061-9c2a780878ed",
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"tables": {
|
||||
"app_metadata": {
|
||||
"name": "app_metadata",
|
||||
"columns": {
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"backup_schedule_mirrors_table": {
|
||||
"name": "backup_schedule_mirrors_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"schedule_id": {
|
||||
"name": "schedule_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"repository_id": {
|
||||
"name": "repository_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"last_copy_at": {
|
||||
"name": "last_copy_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_copy_status": {
|
||||
"name": "last_copy_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_copy_error": {
|
||||
"name": "last_copy_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"backup_schedule_mirrors_table_schedule_id_repository_id_unique": {
|
||||
"name": "backup_schedule_mirrors_table_schedule_id_repository_id_unique",
|
||||
"columns": [
|
||||
"schedule_id",
|
||||
"repository_id"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk": {
|
||||
"name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk",
|
||||
"tableFrom": "backup_schedule_mirrors_table",
|
||||
"columnsFrom": [
|
||||
"schedule_id"
|
||||
],
|
||||
"tableTo": "backup_schedules_table",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "no action",
|
||||
"onDelete": "cascade"
|
||||
},
|
||||
"backup_schedule_mirrors_table_repository_id_repositories_table_id_fk": {
|
||||
"name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk",
|
||||
"tableFrom": "backup_schedule_mirrors_table",
|
||||
"columnsFrom": [
|
||||
"repository_id"
|
||||
],
|
||||
"tableTo": "repositories_table",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "no action",
|
||||
"onDelete": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"backup_schedule_notifications_table": {
|
||||
"name": "backup_schedule_notifications_table",
|
||||
"columns": {
|
||||
"schedule_id": {
|
||||
"name": "schedule_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"destination_id": {
|
||||
"name": "destination_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"notify_on_start": {
|
||||
"name": "notify_on_start",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"notify_on_success": {
|
||||
"name": "notify_on_success",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"notify_on_warning": {
|
||||
"name": "notify_on_warning",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"notify_on_failure": {
|
||||
"name": "notify_on_failure",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk": {
|
||||
"name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk",
|
||||
"tableFrom": "backup_schedule_notifications_table",
|
||||
"columnsFrom": [
|
||||
"schedule_id"
|
||||
],
|
||||
"tableTo": "backup_schedules_table",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "no action",
|
||||
"onDelete": "cascade"
|
||||
},
|
||||
"backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk": {
|
||||
"name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk",
|
||||
"tableFrom": "backup_schedule_notifications_table",
|
||||
"columnsFrom": [
|
||||
"destination_id"
|
||||
],
|
||||
"tableTo": "notification_destinations_table",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "no action",
|
||||
"onDelete": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"backup_schedule_notifications_table_schedule_id_destination_id_pk": {
|
||||
"columns": [
|
||||
"schedule_id",
|
||||
"destination_id"
|
||||
],
|
||||
"name": "backup_schedule_notifications_table_schedule_id_destination_id_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"backup_schedules_table": {
|
||||
"name": "backup_schedules_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"volume_id": {
|
||||
"name": "volume_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"repository_id": {
|
||||
"name": "repository_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"cron_expression": {
|
||||
"name": "cron_expression",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"retention_policy": {
|
||||
"name": "retention_policy",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"exclude_patterns": {
|
||||
"name": "exclude_patterns",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"exclude_if_present": {
|
||||
"name": "exclude_if_present",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"include_patterns": {
|
||||
"name": "include_patterns",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"last_backup_at": {
|
||||
"name": "last_backup_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_backup_status": {
|
||||
"name": "last_backup_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_backup_error": {
|
||||
"name": "last_backup_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"next_backup_at": {
|
||||
"name": "next_backup_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"one_file_system": {
|
||||
"name": "one_file_system",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"sort_order": {
|
||||
"name": "sort_order",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"backup_schedules_table_name_unique": {
|
||||
"name": "backup_schedules_table_name_unique",
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"backup_schedules_table_volume_id_volumes_table_id_fk": {
|
||||
"name": "backup_schedules_table_volume_id_volumes_table_id_fk",
|
||||
"tableFrom": "backup_schedules_table",
|
||||
"columnsFrom": [
|
||||
"volume_id"
|
||||
],
|
||||
"tableTo": "volumes_table",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "no action",
|
||||
"onDelete": "cascade"
|
||||
},
|
||||
"backup_schedules_table_repository_id_repositories_table_id_fk": {
|
||||
"name": "backup_schedules_table_repository_id_repositories_table_id_fk",
|
||||
"tableFrom": "backup_schedules_table",
|
||||
"columnsFrom": [
|
||||
"repository_id"
|
||||
],
|
||||
"tableTo": "repositories_table",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "no action",
|
||||
"onDelete": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"notification_destinations_table": {
|
||||
"name": "notification_destinations_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"config": {
|
||||
"name": "config",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"notification_destinations_table_name_unique": {
|
||||
"name": "notification_destinations_table_name_unique",
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"repositories_table": {
|
||||
"name": "repositories_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"short_id": {
|
||||
"name": "short_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"config": {
|
||||
"name": "config",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"compression_mode": {
|
||||
"name": "compression_mode",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'auto'"
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'unknown'"
|
||||
},
|
||||
"last_checked": {
|
||||
"name": "last_checked",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_error": {
|
||||
"name": "last_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"repositories_table_short_id_unique": {
|
||||
"name": "repositories_table_short_id_unique",
|
||||
"columns": [
|
||||
"short_id"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"repositories_table_name_unique": {
|
||||
"name": "repositories_table_name_unique",
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"sessions_table": {
|
||||
"name": "sessions_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"sessions_table_user_id_users_table_id_fk": {
|
||||
"name": "sessions_table_user_id_users_table_id_fk",
|
||||
"tableFrom": "sessions_table",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"tableTo": "users_table",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "no action",
|
||||
"onDelete": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users_table": {
|
||||
"name": "users_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password_hash": {
|
||||
"name": "password_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"has_downloaded_restic_password": {
|
||||
"name": "has_downloaded_restic_password",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_table_username_unique": {
|
||||
"name": "users_table_username_unique",
|
||||
"columns": [
|
||||
"username"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"volumes_table": {
|
||||
"name": "volumes_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"short_id": {
|
||||
"name": "short_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'unmounted'"
|
||||
},
|
||||
"last_error": {
|
||||
"name": "last_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_health_check": {
|
||||
"name": "last_health_check",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"config": {
|
||||
"name": "config",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"auto_remount": {
|
||||
"name": "auto_remount",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"volumes_table_short_id_unique": {
|
||||
"name": "volumes_table_short_id_unique",
|
||||
"columns": [
|
||||
"short_id"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"volumes_table_name_unique": {
|
||||
"name": "volumes_table_name_unique",
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -162,6 +162,20 @@
|
|||
"when": 1765794552191,
|
||||
"tag": "0022_woozy_shen",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 23,
|
||||
"version": "6",
|
||||
"when": 1766320570509,
|
||||
"tag": "0023_special_thor",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 24,
|
||||
"version": "6",
|
||||
"when": 1766325504548,
|
||||
"tag": "0024_schedules-one-fs",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -76,7 +76,7 @@ export const customNotificationConfigSchema = type({
|
|||
shoutrrrUrl: "string",
|
||||
});
|
||||
|
||||
export const notificationConfigSchema = emailNotificationConfigSchema
|
||||
export const notificationConfigSchemaBase = emailNotificationConfigSchema
|
||||
.or(slackNotificationConfigSchema)
|
||||
.or(discordNotificationConfigSchema)
|
||||
.or(gotifyNotificationConfigSchema)
|
||||
|
|
@ -85,6 +85,8 @@ export const notificationConfigSchema = emailNotificationConfigSchema
|
|||
.or(telegramNotificationConfigSchema)
|
||||
.or(customNotificationConfigSchema);
|
||||
|
||||
export const notificationConfigSchema = notificationConfigSchemaBase.onUndeclaredKey("delete");
|
||||
|
||||
export type NotificationConfig = typeof notificationConfigSchema.infer;
|
||||
|
||||
export const NOTIFICATION_EVENTS = {
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export const sftpRepositoryConfigSchema = type({
|
|||
privateKey: "string",
|
||||
}).and(baseRepositoryConfigSchema);
|
||||
|
||||
export const repositoryConfigSchema = s3RepositoryConfigSchema
|
||||
export const repositoryConfigSchemaBase = s3RepositoryConfigSchema
|
||||
.or(r2RepositoryConfigSchema)
|
||||
.or(localRepositoryConfigSchema)
|
||||
.or(gcsRepositoryConfigSchema)
|
||||
|
|
@ -88,6 +88,8 @@ export const repositoryConfigSchema = s3RepositoryConfigSchema
|
|||
.or(restRepositoryConfigSchema)
|
||||
.or(sftpRepositoryConfigSchema);
|
||||
|
||||
export const repositoryConfigSchema = repositoryConfigSchemaBase.onUndeclaredKey("delete");
|
||||
|
||||
export type RepositoryConfig = typeof repositoryConfigSchema.infer;
|
||||
|
||||
export const COMPRESSION_MODES = {
|
||||
|
|
|
|||
|
|
@ -55,7 +55,13 @@ export const rcloneConfigSchema = type({
|
|||
readOnly: "boolean?",
|
||||
});
|
||||
|
||||
export const volumeConfigSchema = nfsConfigSchema.or(smbConfigSchema).or(webdavConfigSchema).or(directoryConfigSchema).or(rcloneConfigSchema);
|
||||
export const volumeConfigSchemaBase = nfsConfigSchema
|
||||
.or(smbConfigSchema)
|
||||
.or(webdavConfigSchema)
|
||||
.or(directoryConfigSchema)
|
||||
.or(rcloneConfigSchema);
|
||||
|
||||
export const volumeConfigSchema = volumeConfigSchemaBase.onUndeclaredKey("delete");
|
||||
|
||||
export type BackendConfig = typeof volumeConfigSchema.infer;
|
||||
|
||||
|
|
|
|||
90
app/server/app.ts
Normal file
90
app/server/app.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { Scalar } from "@scalar/hono-api-reference";
|
||||
import { Hono } from "hono";
|
||||
import { logger as honoLogger } from "hono/logger";
|
||||
import { secureHeaders } from "hono/secure-headers";
|
||||
import { rateLimiter } from "hono-rate-limiter";
|
||||
import { openAPIRouteHandler } from "hono-openapi";
|
||||
import { authController } from "./modules/auth/auth.controller";
|
||||
import { requireAuth } from "./modules/auth/auth.middleware";
|
||||
import { repositoriesController } from "./modules/repositories/repositories.controller";
|
||||
import { systemController } from "./modules/system/system.controller";
|
||||
import { volumeController } from "./modules/volumes/volume.controller";
|
||||
import { backupScheduleController } from "./modules/backups/backups.controller";
|
||||
import { eventsController } from "./modules/events/events.controller";
|
||||
import { notificationsController } from "./modules/notifications/notifications.controller";
|
||||
import { configExportController } from "./modules/lifecycle/config-export.controller";
|
||||
import { handleServiceError } from "./utils/errors";
|
||||
import { logger } from "./utils/logger";
|
||||
import { config } from "./core/config";
|
||||
|
||||
export const generalDescriptor = (app: Hono) =>
|
||||
openAPIRouteHandler(app, {
|
||||
documentation: {
|
||||
info: {
|
||||
title: "Zerobyte API",
|
||||
version: "1.0.0",
|
||||
description: "API for managing volumes",
|
||||
},
|
||||
servers: [{ url: `http://${config.serverIp}:4096`, description: "Development Server" }],
|
||||
},
|
||||
});
|
||||
|
||||
export const scalarDescriptor = Scalar({
|
||||
title: "Zerobyte API Docs",
|
||||
pageTitle: "Zerobyte API Docs",
|
||||
url: "/api/v1/openapi.json",
|
||||
});
|
||||
|
||||
export const createApp = () => {
|
||||
const app = new Hono();
|
||||
|
||||
if (config.environment !== "test") {
|
||||
app.use(honoLogger());
|
||||
}
|
||||
|
||||
app
|
||||
.use(secureHeaders())
|
||||
.use(
|
||||
rateLimiter({
|
||||
windowMs: 60 * 5 * 1000,
|
||||
limit: 1000,
|
||||
keyGenerator: (c) => c.req.header("x-forwarded-for") ?? "",
|
||||
skip: () => {
|
||||
return config.__prod__ === false;
|
||||
},
|
||||
}),
|
||||
)
|
||||
.get("healthcheck", (c) => c.json({ status: "ok" }))
|
||||
.route("/api/v1/auth", authController)
|
||||
.use("/api/v1/volumes/*", requireAuth)
|
||||
.use("/api/v1/repositories/*", requireAuth)
|
||||
.use("/api/v1/backups/*", requireAuth)
|
||||
.use("/api/v1/notifications/*", requireAuth)
|
||||
.use("/api/v1/system/*", requireAuth)
|
||||
.use("/api/v1/events/*", requireAuth)
|
||||
.use("/api/v1/config/*", requireAuth)
|
||||
.route("/api/v1/volumes", volumeController)
|
||||
.route("/api/v1/repositories", repositoriesController)
|
||||
.route("/api/v1/backups", backupScheduleController)
|
||||
.route("/api/v1/notifications", notificationsController)
|
||||
.route("/api/v1/system", systemController)
|
||||
.route("/api/v1/events", eventsController)
|
||||
.route("/api/v1/config", configExportController);
|
||||
|
||||
app.get("/api/v1/openapi.json", generalDescriptor(app));
|
||||
app.get("/api/v1/docs", requireAuth, scalarDescriptor);
|
||||
|
||||
app.onError((err, c) => {
|
||||
logger.error(`${c.req.url}: ${err.message}`);
|
||||
|
||||
if (err.cause instanceof Error) {
|
||||
logger.error(err.cause.message);
|
||||
}
|
||||
|
||||
const { status, message } = handleServiceError(err);
|
||||
|
||||
return c.json({ message }, status);
|
||||
});
|
||||
|
||||
return app;
|
||||
};
|
||||
38
app/server/core/__tests__/repository-mutex.test.ts
Normal file
38
app/server/core/__tests__/repository-mutex.test.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { test, describe, expect } from "bun:test";
|
||||
import { repoMutex } from "../repository-mutex";
|
||||
|
||||
describe("RepositoryMutex", () => {
|
||||
test("should prioritize waiting exclusive locks over new shared locks", async () => {
|
||||
const repoId = "test-repo";
|
||||
const results: string[] = [];
|
||||
|
||||
const releaseShared1 = await repoMutex.acquireShared(repoId, "backup-1");
|
||||
results.push("acquired-shared-1");
|
||||
|
||||
const exclusivePromise = repoMutex.acquireExclusive(repoId, "unlock").then((release) => {
|
||||
results.push("acquired-exclusive");
|
||||
return release;
|
||||
});
|
||||
|
||||
const shared2Promise = repoMutex.acquireShared(repoId, "backup-2").then((release) => {
|
||||
results.push("acquired-shared-2");
|
||||
return release;
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(results).toEqual(["acquired-shared-1"]);
|
||||
|
||||
releaseShared1();
|
||||
|
||||
const releaseExclusive = await exclusivePromise;
|
||||
expect(results).toEqual(["acquired-shared-1", "acquired-exclusive"]);
|
||||
|
||||
releaseExclusive();
|
||||
|
||||
const releaseShared2 = await shared2Promise;
|
||||
expect(results).toEqual(["acquired-shared-1", "acquired-exclusive", "acquired-shared-2"]);
|
||||
|
||||
releaseShared2();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
export const OPERATION_TIMEOUT = 5000;
|
||||
export const VOLUME_MOUNT_BASE = "/var/lib/zerobyte/volumes";
|
||||
export const REPOSITORY_BASE = "/var/lib/zerobyte/repositories";
|
||||
export const DATABASE_URL = "/var/lib/zerobyte/data/ironmount.db";
|
||||
export const DATABASE_URL = process.env.DATABASE_URL || "/var/lib/zerobyte/data/ironmount.db";
|
||||
export const RESTIC_PASS_FILE = "/var/lib/zerobyte/data/restic.pass";
|
||||
|
||||
export const DEFAULT_EXCLUDES = [DATABASE_URL, RESTIC_PASS_FILE, REPOSITORY_BASE];
|
||||
|
||||
export const REQUIRED_MIGRATIONS = ["v0.14.0"];
|
||||
|
|
|
|||
|
|
@ -49,7 +49,9 @@ class RepositoryMutex {
|
|||
async acquireShared(repositoryId: string, operation: string): Promise<() => void> {
|
||||
const state = this.getOrCreateState(repositoryId);
|
||||
|
||||
if (!state.exclusiveHolder) {
|
||||
const hasExclusiveInQueue = state.waitQueue.some((item) => item.type === "exclusive");
|
||||
|
||||
if (!state.exclusiveHolder && !hasExclusiveInQueue) {
|
||||
const lockId = this.generateLockId();
|
||||
state.sharedHolders.set(lockId, {
|
||||
id: lockId,
|
||||
|
|
@ -60,7 +62,7 @@ class RepositoryMutex {
|
|||
}
|
||||
|
||||
logger.debug(
|
||||
`[Mutex] Waiting for shared lock on repo ${repositoryId}: ${operation} (exclusive held by: ${state.exclusiveHolder.operation})`,
|
||||
`[Mutex] Waiting for shared lock on repo ${repositoryId}: ${operation} (exclusive held by: ${state.exclusiveHolder?.operation ?? "none"}, queue: ${state.waitQueue.length})`,
|
||||
);
|
||||
const lockId = await new Promise<string>((resolve) => {
|
||||
state.waitQueue.push({ type: "shared", operation, resolve });
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export const volumesTable = sqliteTable("volumes_table", {
|
|||
autoRemount: int("auto_remount", { mode: "boolean" }).notNull().default(true),
|
||||
});
|
||||
export type Volume = typeof volumesTable.$inferSelect;
|
||||
export type VolumeInsert = typeof volumesTable.$inferInsert;
|
||||
|
||||
/**
|
||||
* Users Table
|
||||
|
|
@ -61,6 +62,7 @@ export const repositoriesTable = sqliteTable("repositories_table", {
|
|||
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
||||
});
|
||||
export type Repository = typeof repositoriesTable.$inferSelect;
|
||||
export type RepositoryInsert = typeof repositoriesTable.$inferInsert;
|
||||
|
||||
/**
|
||||
* Backup Schedules Table
|
||||
|
|
@ -92,10 +94,12 @@ export const backupSchedulesTable = sqliteTable("backup_schedules_table", {
|
|||
lastBackupStatus: text("last_backup_status").$type<"success" | "error" | "in_progress" | "warning">(),
|
||||
lastBackupError: text("last_backup_error"),
|
||||
nextBackupAt: int("next_backup_at", { mode: "number" }),
|
||||
oneFileSystem: int("one_file_system", { mode: "boolean" }).notNull().default(false),
|
||||
sortOrder: int("sort_order", { mode: "number" }).notNull().default(0),
|
||||
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
||||
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
||||
});
|
||||
export type BackupScheduleInsert = typeof backupSchedulesTable.$inferInsert;
|
||||
|
||||
export const backupScheduleRelations = relations(backupSchedulesTable, ({ one, many }) => ({
|
||||
volume: one(volumesTable, {
|
||||
|
|
|
|||
|
|
@ -1,78 +1,14 @@
|
|||
import { createHonoServer } from "react-router-hono-server/bun";
|
||||
import { Scalar } from "@scalar/hono-api-reference";
|
||||
import { Hono } from "hono";
|
||||
import { logger as honoLogger } from "hono/logger";
|
||||
import { openAPIRouteHandler } from "hono-openapi";
|
||||
import { runDbMigrations } from "./db/db";
|
||||
import { authController } from "./modules/auth/auth.controller";
|
||||
import { requireAuth } from "./modules/auth/auth.middleware";
|
||||
import { startup } from "./modules/lifecycle/startup";
|
||||
import { migrateToShortIds } from "./modules/lifecycle/migration";
|
||||
import { repositoriesController } from "./modules/repositories/repositories.controller";
|
||||
import { systemController } from "./modules/system/system.controller";
|
||||
import { volumeController } from "./modules/volumes/volume.controller";
|
||||
import { backupScheduleController } from "./modules/backups/backups.controller";
|
||||
import { eventsController } from "./modules/events/events.controller";
|
||||
import { notificationsController } from "./modules/notifications/notifications.controller";
|
||||
import { configExportController } from "./modules/lifecycle/config-export.controller";
|
||||
import { handleServiceError } from "./utils/errors";
|
||||
import { logger } from "./utils/logger";
|
||||
import { shutdown } from "./modules/lifecycle/shutdown";
|
||||
import { REQUIRED_MIGRATIONS } from "./core/constants";
|
||||
import { validateRequiredMigrations } from "./modules/lifecycle/checkpoint";
|
||||
import { config } from "./core/config";
|
||||
import { createApp } from "./app";
|
||||
|
||||
export const generalDescriptor = (app: Hono) =>
|
||||
openAPIRouteHandler(app, {
|
||||
documentation: {
|
||||
info: {
|
||||
title: "Zerobyte API",
|
||||
version: "1.0.0",
|
||||
description: "API for managing volumes",
|
||||
},
|
||||
servers: [{ url: `http://${config.serverIp}:4096`, description: "Development Server" }],
|
||||
},
|
||||
});
|
||||
|
||||
export const scalarDescriptor = Scalar({
|
||||
title: "Zerobyte API Docs",
|
||||
pageTitle: "Zerobyte API Docs",
|
||||
url: "/api/v1/openapi.json",
|
||||
});
|
||||
|
||||
const app = new Hono()
|
||||
.use(honoLogger())
|
||||
.get("healthcheck", (c) => c.json({ status: "ok" }))
|
||||
.route("/api/v1/auth", authController.basePath("/api/v1"))
|
||||
.use("/api/v1/volumes/*", requireAuth)
|
||||
.use("/api/v1/repositories/*", requireAuth)
|
||||
.use("/api/v1/backups/*", requireAuth)
|
||||
.use("/api/v1/notifications/*", requireAuth)
|
||||
.use("/api/v1/system/*", requireAuth)
|
||||
.use("/api/v1/events/*", requireAuth)
|
||||
.use("/api/v1/config/*", requireAuth)
|
||||
.route("/api/v1/volumes", volumeController)
|
||||
.route("/api/v1/repositories", repositoriesController)
|
||||
.route("/api/v1/backups", backupScheduleController)
|
||||
.route("/api/v1/notifications", notificationsController)
|
||||
.route("/api/v1/system", systemController)
|
||||
.route("/api/v1/config", configExportController)
|
||||
.route("/api/v1/events", eventsController);
|
||||
|
||||
app.get("/api/v1/openapi.json", generalDescriptor(app));
|
||||
app.get("/api/v1/docs", scalarDescriptor);
|
||||
|
||||
app.onError((err, c) => {
|
||||
logger.error(`${c.req.url}: ${err.message}`);
|
||||
|
||||
if (err.cause instanceof Error) {
|
||||
logger.error(err.cause.message);
|
||||
}
|
||||
|
||||
const { status, message } = handleServiceError(err);
|
||||
|
||||
return c.json({ message }, status);
|
||||
});
|
||||
const app = createApp();
|
||||
|
||||
runDbMigrations();
|
||||
|
||||
|
|
|
|||
28
app/server/jobs/auto-remount.ts
Normal file
28
app/server/jobs/auto-remount.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { Job } from "../core/scheduler";
|
||||
import { volumeService } from "../modules/volumes/volume.service";
|
||||
import { logger } from "../utils/logger";
|
||||
import { db } from "../db/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { volumesTable } from "../db/schema";
|
||||
|
||||
export class VolumeAutoRemountJob extends Job {
|
||||
async run() {
|
||||
logger.debug("Running auto-remount for all errored volumes...");
|
||||
|
||||
const volumes = await db.query.volumesTable.findMany({
|
||||
where: eq(volumesTable.status, "error"),
|
||||
});
|
||||
|
||||
for (const volume of volumes) {
|
||||
if (volume.autoRemount) {
|
||||
try {
|
||||
await volumeService.mountVolume(volume.name);
|
||||
} catch (err) {
|
||||
logger.error(`Failed to auto-remount volume ${volume.name}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { done: true, timestamp: new Date() };
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { Job } from "../core/scheduler";
|
||||
import { backupsService } from "../modules/backups/backups.service";
|
||||
import { toMessage } from "../utils/errors";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export class BackupExecutionJob extends Job {
|
||||
|
|
@ -17,8 +16,8 @@ export class BackupExecutionJob extends Job {
|
|||
logger.info(`Found ${scheduleIds.length} backup schedule(s) to execute`);
|
||||
|
||||
for (const scheduleId of scheduleIds) {
|
||||
backupsService.executeBackup(scheduleId).catch((error) => {
|
||||
logger.error(`Failed to execute backup for schedule ${scheduleId}: ${toMessage(error)}`);
|
||||
backupsService.executeBackup(scheduleId).catch((err) => {
|
||||
logger.error(`Error executing backup for schedule ${scheduleId}:`, err);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export class CleanupDanglingMountsJob extends Job {
|
|||
if (!matchingVolume) {
|
||||
const fullPath = path.join(VOLUME_MOUNT_BASE, dir);
|
||||
logger.info(`Found dangling mount directory at ${fullPath}, attempting to remove...`);
|
||||
await fs.rmdir(fullPath, { recursive: true }).catch((err) => {
|
||||
await fs.rm(fullPath, { recursive: true, force: true }).catch((err) => {
|
||||
logger.warn(`Failed to remove dangling mount directory ${fullPath}: ${toMessage(err)}`);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { logger } from "../utils/logger";
|
|||
import { db } from "../db/db";
|
||||
import { eq, or } from "drizzle-orm";
|
||||
import { repositoriesTable } from "../db/schema";
|
||||
import { repoMutex } from "../core/repository-mutex";
|
||||
|
||||
export class RepositoryHealthCheckJob extends Job {
|
||||
async run() {
|
||||
|
|
@ -15,11 +14,6 @@ export class RepositoryHealthCheckJob extends Job {
|
|||
});
|
||||
|
||||
for (const repository of repositories) {
|
||||
if (repoMutex.isLocked(repository.id)) {
|
||||
logger.debug(`Skipping health check for repository ${repository.name}: currently locked`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await repositoriesService.checkHealth(repository.id);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { validator } from "hono-openapi";
|
||||
|
||||
import { rateLimiter } from "hono-rate-limiter";
|
||||
import { Hono } from "hono";
|
||||
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
||||
import {
|
||||
|
|
@ -21,6 +21,7 @@ import {
|
|||
} from "./auth.dto";
|
||||
import { authService } from "./auth.service";
|
||||
import { toMessage } from "../../utils/errors";
|
||||
import { config } from "~/server/core/config";
|
||||
|
||||
const COOKIE_NAME = "session_id";
|
||||
const COOKIE_OPTIONS = {
|
||||
|
|
@ -30,8 +31,17 @@ const COOKIE_OPTIONS = {
|
|||
path: "/",
|
||||
};
|
||||
|
||||
const authRateLimiter = rateLimiter({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
limit: 20,
|
||||
keyGenerator: (c) => c.req.header("x-forwarded-for") ?? "",
|
||||
skip: () => {
|
||||
return config.__prod__ === false;
|
||||
},
|
||||
});
|
||||
|
||||
export const authController = new Hono()
|
||||
.post("/register", registerDto, validator("json", registerBodySchema), async (c) => {
|
||||
.post("/register", authRateLimiter, registerDto, validator("json", registerBodySchema), async (c) => {
|
||||
const body = c.req.valid("json");
|
||||
|
||||
try {
|
||||
|
|
@ -58,7 +68,7 @@ export const authController = new Hono()
|
|||
return c.json<RegisterDto>({ success: false, message: toMessage(error) }, 400);
|
||||
}
|
||||
})
|
||||
.post("/login", loginDto, validator("json", loginBodySchema), async (c) => {
|
||||
.post("/login", authRateLimiter, loginDto, validator("json", loginBodySchema), async (c) => {
|
||||
const body = c.req.valid("json");
|
||||
|
||||
try {
|
||||
|
|
@ -82,7 +92,7 @@ export const authController = new Hono()
|
|||
return c.json<LoginDto>({ success: false, message: toMessage(error) }, 401);
|
||||
}
|
||||
})
|
||||
.post("/logout", logoutDto, async (c) => {
|
||||
.post("/logout", authRateLimiter, logoutDto, async (c) => {
|
||||
const sessionId = getCookie(c, COOKIE_NAME);
|
||||
|
||||
if (sessionId) {
|
||||
|
|
@ -116,26 +126,32 @@ export const authController = new Hono()
|
|||
const hasUsers = await authService.hasUsers();
|
||||
return c.json<GetStatusDto>({ hasUsers });
|
||||
})
|
||||
.post("/change-password", changePasswordDto, validator("json", changePasswordBodySchema), async (c) => {
|
||||
const sessionId = getCookie(c, COOKIE_NAME);
|
||||
.post(
|
||||
"/change-password",
|
||||
authRateLimiter,
|
||||
changePasswordDto,
|
||||
validator("json", changePasswordBodySchema),
|
||||
async (c) => {
|
||||
const sessionId = getCookie(c, COOKIE_NAME);
|
||||
|
||||
if (!sessionId) {
|
||||
return c.json<ChangePasswordDto>({ success: false, message: "Not authenticated" }, 401);
|
||||
}
|
||||
if (!sessionId) {
|
||||
return c.json<ChangePasswordDto>({ success: false, message: "Not authenticated" }, 401);
|
||||
}
|
||||
|
||||
const session = await authService.verifySession(sessionId);
|
||||
const session = await authService.verifySession(sessionId);
|
||||
|
||||
if (!session) {
|
||||
deleteCookie(c, COOKIE_NAME, COOKIE_OPTIONS);
|
||||
return c.json<ChangePasswordDto>({ success: false, message: "Not authenticated" }, 401);
|
||||
}
|
||||
if (!session) {
|
||||
deleteCookie(c, COOKIE_NAME, COOKIE_OPTIONS);
|
||||
return c.json<ChangePasswordDto>({ success: false, message: "Not authenticated" }, 401);
|
||||
}
|
||||
|
||||
const body = c.req.valid("json");
|
||||
const body = c.req.valid("json");
|
||||
|
||||
try {
|
||||
await authService.changePassword(session.user.id, body.currentPassword, body.newPassword);
|
||||
return c.json<ChangePasswordDto>({ success: true, message: "Password changed successfully" });
|
||||
} catch (error) {
|
||||
return c.json<ChangePasswordDto>({ success: false, message: toMessage(error) }, 400);
|
||||
}
|
||||
});
|
||||
try {
|
||||
await authService.changePassword(session.user.id, body.currentPassword, body.newPassword);
|
||||
return c.json<ChangePasswordDto>({ success: true, message: "Password changed successfully" });
|
||||
} catch (error) {
|
||||
return c.json<ChangePasswordDto>({ success: false, message: toMessage(error) }, 400);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -27,8 +27,10 @@ const mount = async (config: BackendConfig, path: string) => {
|
|||
return { status: BACKEND_STATUS.mounted };
|
||||
}
|
||||
|
||||
logger.debug(`Trying to unmount any existing mounts at ${path} before mounting...`);
|
||||
await unmount(path);
|
||||
if (status === "error") {
|
||||
logger.debug(`Trying to unmount any existing mounts at ${path} before mounting...`);
|
||||
await unmount(path);
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
await fs.mkdir(path, { recursive: true });
|
||||
|
|
@ -64,16 +66,15 @@ const unmount = async (path: string) => {
|
|||
}
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
await fs.access(path);
|
||||
} catch {
|
||||
logger.warn(`Path ${path} does not exist. Skipping unmount.`);
|
||||
const mount = await getMountForPath(path);
|
||||
if (!mount || mount.mountPoint !== path) {
|
||||
logger.debug(`Path ${path} is not a mount point. Skipping unmount.`);
|
||||
return { status: BACKEND_STATUS.unmounted };
|
||||
}
|
||||
|
||||
await executeUnmount(path);
|
||||
|
||||
await fs.rmdir(path);
|
||||
await fs.rmdir(path).catch(() => {});
|
||||
|
||||
logger.info(`NFS volume at ${path} unmounted successfully.`);
|
||||
return { status: BACKEND_STATUS.unmounted };
|
||||
|
|
@ -89,13 +90,20 @@ const unmount = async (path: string) => {
|
|||
|
||||
const checkHealth = async (path: string) => {
|
||||
const run = async () => {
|
||||
logger.debug(`Checking health of NFS volume at ${path}...`);
|
||||
await fs.access(path);
|
||||
try {
|
||||
await fs.access(path);
|
||||
} catch {
|
||||
throw new Error("Volume is not mounted");
|
||||
}
|
||||
|
||||
const mount = await getMountForPath(path);
|
||||
|
||||
if (!mount || !mount.fstype.startsWith("nfs")) {
|
||||
throw new Error(`Path ${path} is not mounted as NFS.`);
|
||||
if (!mount || mount.mountPoint !== path) {
|
||||
throw new Error("Volume is not mounted");
|
||||
}
|
||||
|
||||
if (!mount.fstype.startsWith("nfs")) {
|
||||
throw new Error(`Path ${path} is not mounted as NFS (found ${mount.fstype}).`);
|
||||
}
|
||||
|
||||
logger.debug(`NFS volume at ${path} is healthy and mounted.`);
|
||||
|
|
@ -105,8 +113,11 @@ const checkHealth = async (path: string) => {
|
|||
try {
|
||||
return await withTimeout(run(), OPERATION_TIMEOUT, "NFS health check");
|
||||
} catch (error) {
|
||||
logger.error("NFS volume health check failed:", toMessage(error));
|
||||
return { status: BACKEND_STATUS.error, error: toMessage(error) };
|
||||
const message = toMessage(error);
|
||||
if (message !== "Volume is not mounted") {
|
||||
logger.error("NFS volume health check failed:", message);
|
||||
}
|
||||
return { status: BACKEND_STATUS.error, error: message };
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -28,8 +28,10 @@ const mount = async (config: BackendConfig, path: string) => {
|
|||
return { status: BACKEND_STATUS.mounted };
|
||||
}
|
||||
|
||||
logger.debug(`Trying to unmount any existing mounts at ${path} before mounting...`);
|
||||
await unmount(path);
|
||||
if (status === "error") {
|
||||
logger.debug(`Trying to unmount any existing mounts at ${path} before mounting...`);
|
||||
await unmount(path);
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
await fs.mkdir(path, { recursive: true });
|
||||
|
|
@ -76,15 +78,14 @@ const unmount = async (path: string) => {
|
|||
}
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
await fs.access(path);
|
||||
} catch (e) {
|
||||
logger.warn(`Path ${path} does not exist. Skipping unmount.`, e);
|
||||
const mount = await getMountForPath(path);
|
||||
if (!mount || mount.mountPoint !== path) {
|
||||
logger.debug(`Path ${path} is not a mount point. Skipping unmount.`);
|
||||
return { status: BACKEND_STATUS.unmounted };
|
||||
}
|
||||
|
||||
await executeUnmount(path);
|
||||
await fs.rmdir(path);
|
||||
await fs.rmdir(path).catch(() => {});
|
||||
|
||||
logger.info(`Rclone volume at ${path} unmounted successfully.`);
|
||||
return { status: BACKEND_STATUS.unmounted };
|
||||
|
|
@ -100,13 +101,20 @@ const unmount = async (path: string) => {
|
|||
|
||||
const checkHealth = async (path: string) => {
|
||||
const run = async () => {
|
||||
logger.debug(`Checking health of rclone volume at ${path}...`);
|
||||
await fs.access(path);
|
||||
try {
|
||||
await fs.access(path);
|
||||
} catch {
|
||||
throw new Error("Volume is not mounted");
|
||||
}
|
||||
|
||||
const mount = await getMountForPath(path);
|
||||
|
||||
if (!mount || mount.fstype !== "fuse.rclone") {
|
||||
throw new Error(`Path ${path} is not mounted as rclone.`);
|
||||
if (!mount || mount.mountPoint !== path) {
|
||||
throw new Error("Volume is not mounted");
|
||||
}
|
||||
|
||||
if (!mount.fstype.includes("rclone")) {
|
||||
throw new Error(`Path ${path} is not mounted as rclone (found ${mount.fstype}).`);
|
||||
}
|
||||
|
||||
logger.debug(`Rclone volume at ${path} is healthy and mounted.`);
|
||||
|
|
@ -116,8 +124,11 @@ const checkHealth = async (path: string) => {
|
|||
try {
|
||||
return await withTimeout(run(), OPERATION_TIMEOUT, "Rclone health check");
|
||||
} catch (error) {
|
||||
logger.error("Rclone volume health check failed:", toMessage(error));
|
||||
return { status: BACKEND_STATUS.error, error: toMessage(error) };
|
||||
const message = toMessage(error);
|
||||
if (message !== "Volume is not mounted") {
|
||||
logger.error("Rclone volume health check failed:", message);
|
||||
}
|
||||
return { status: BACKEND_STATUS.error, error: message };
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -28,8 +28,10 @@ const mount = async (config: BackendConfig, path: string) => {
|
|||
return { status: BACKEND_STATUS.mounted };
|
||||
}
|
||||
|
||||
logger.debug(`Trying to unmount any existing mounts at ${path} before mounting...`);
|
||||
await unmount(path);
|
||||
if (status === "error") {
|
||||
logger.debug(`Trying to unmount any existing mounts at ${path} before mounting...`);
|
||||
await unmount(path);
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
await fs.mkdir(path, { recursive: true });
|
||||
|
|
@ -80,16 +82,15 @@ const unmount = async (path: string) => {
|
|||
}
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
await fs.access(path);
|
||||
} catch {
|
||||
logger.warn(`Path ${path} does not exist. Skipping unmount.`);
|
||||
const mount = await getMountForPath(path);
|
||||
if (!mount || mount.mountPoint !== path) {
|
||||
logger.debug(`Path ${path} is not a mount point. Skipping unmount.`);
|
||||
return { status: BACKEND_STATUS.unmounted };
|
||||
}
|
||||
|
||||
await executeUnmount(path);
|
||||
|
||||
await fs.rmdir(path);
|
||||
await fs.rmdir(path).catch(() => {});
|
||||
|
||||
logger.info(`SMB volume at ${path} unmounted successfully.`);
|
||||
return { status: BACKEND_STATUS.unmounted };
|
||||
|
|
@ -105,13 +106,20 @@ const unmount = async (path: string) => {
|
|||
|
||||
const checkHealth = async (path: string) => {
|
||||
const run = async () => {
|
||||
logger.debug(`Checking health of SMB volume at ${path}...`);
|
||||
await fs.access(path);
|
||||
try {
|
||||
await fs.access(path);
|
||||
} catch {
|
||||
throw new Error("Volume is not mounted");
|
||||
}
|
||||
|
||||
const mount = await getMountForPath(path);
|
||||
|
||||
if (!mount || mount.fstype !== "cifs") {
|
||||
throw new Error(`Path ${path} is not mounted as CIFS/SMB.`);
|
||||
if (!mount || mount.mountPoint !== path) {
|
||||
throw new Error("Volume is not mounted");
|
||||
}
|
||||
|
||||
if (mount.fstype !== "cifs") {
|
||||
throw new Error(`Path ${path} is not mounted as CIFS/SMB (found ${mount.fstype}).`);
|
||||
}
|
||||
|
||||
logger.debug(`SMB volume at ${path} is healthy and mounted.`);
|
||||
|
|
@ -121,8 +129,11 @@ const checkHealth = async (path: string) => {
|
|||
try {
|
||||
return await withTimeout(run(), OPERATION_TIMEOUT, "SMB health check");
|
||||
} catch (error) {
|
||||
logger.error("SMB volume health check failed:", toMessage(error));
|
||||
return { status: BACKEND_STATUS.error, error: toMessage(error) };
|
||||
const message = toMessage(error);
|
||||
if (message !== "Volume is not mounted") {
|
||||
logger.error("SMB volume health check failed:", message);
|
||||
}
|
||||
return { status: BACKEND_STATUS.error, error: message };
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { $ } from "bun";
|
|||
export const executeMount = async (args: string[]): Promise<void> => {
|
||||
let stderr: string | undefined;
|
||||
|
||||
logger.debug(`Executing mount ${args.join(" ")}`);
|
||||
const result = await $`mount ${args}`.nothrow();
|
||||
stderr = result.stderr.toString();
|
||||
|
||||
|
|
@ -22,7 +23,8 @@ export const executeMount = async (args: string[]): Promise<void> => {
|
|||
export const executeUnmount = async (path: string): Promise<void> => {
|
||||
let stderr: string | undefined;
|
||||
|
||||
const result = await $`umount -l -f ${path}`.nothrow();
|
||||
logger.debug(`Executing umount -l ${path}`);
|
||||
const result = await $`umount -l ${path}`.nothrow();
|
||||
stderr = result.stderr.toString();
|
||||
|
||||
if (stderr?.trim()) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { execFile as execFileCb } from "node:child_process";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import { promisify } from "node:util";
|
||||
import { OPERATION_TIMEOUT } from "../../../core/constants";
|
||||
import { cryptoUtils } from "../../../utils/crypto";
|
||||
import { toMessage } from "../../../utils/errors";
|
||||
|
|
@ -12,8 +10,6 @@ import type { VolumeBackend } from "../backend";
|
|||
import { executeMount, executeUnmount } from "../utils/backend-utils";
|
||||
import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes";
|
||||
|
||||
const execFile = promisify(execFileCb);
|
||||
|
||||
const mount = async (config: BackendConfig, path: string) => {
|
||||
logger.debug(`Mounting WebDAV volume ${path}...`);
|
||||
|
||||
|
|
@ -32,8 +28,10 @@ const mount = async (config: BackendConfig, path: string) => {
|
|||
return { status: BACKEND_STATUS.mounted };
|
||||
}
|
||||
|
||||
logger.debug(`Trying to unmount any existing mounts at ${path} before mounting...`);
|
||||
await unmount(path);
|
||||
if (status === "error") {
|
||||
logger.debug(`Trying to unmount any existing mounts at ${path} before mounting...`);
|
||||
await unmount(path);
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
await fs.mkdir(path, { recursive: true }).catch((err) => {
|
||||
|
|
@ -58,18 +56,9 @@ const mount = async (config: BackendConfig, path: string) => {
|
|||
|
||||
logger.debug(`Mounting WebDAV volume ${path}...`);
|
||||
|
||||
const args = ["-t", "davfs", source, path];
|
||||
const args = ["-t", "davfs", "-o", options.join(","), source, path];
|
||||
await executeMount(args);
|
||||
|
||||
const { stderr } = await execFile("mount", ["-t", "davfs", "-o", options.join(","), source, path], {
|
||||
timeout: OPERATION_TIMEOUT,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
|
||||
if (stderr?.trim()) {
|
||||
logger.warn(stderr.trim());
|
||||
}
|
||||
|
||||
logger.info(`WebDAV volume at ${path} mounted successfully.`);
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
};
|
||||
|
|
@ -113,16 +102,15 @@ const unmount = async (path: string) => {
|
|||
}
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
await fs.access(path);
|
||||
} catch (e) {
|
||||
logger.warn(`Path ${path} does not exist. Skipping unmount.`, e);
|
||||
const mount = await getMountForPath(path);
|
||||
if (!mount || mount.mountPoint !== path) {
|
||||
logger.debug(`Path ${path} is not a mount point. Skipping unmount.`);
|
||||
return { status: BACKEND_STATUS.unmounted };
|
||||
}
|
||||
|
||||
await executeUnmount(path);
|
||||
|
||||
await fs.rmdir(path);
|
||||
await fs.rmdir(path).catch(() => {});
|
||||
|
||||
logger.info(`WebDAV volume at ${path} unmounted successfully.`);
|
||||
return { status: BACKEND_STATUS.unmounted };
|
||||
|
|
@ -138,13 +126,20 @@ const unmount = async (path: string) => {
|
|||
|
||||
const checkHealth = async (path: string) => {
|
||||
const run = async () => {
|
||||
logger.debug(`Checking health of WebDAV volume at ${path}...`);
|
||||
await fs.access(path);
|
||||
try {
|
||||
await fs.access(path);
|
||||
} catch {
|
||||
throw new Error("Volume is not mounted");
|
||||
}
|
||||
|
||||
const mount = await getMountForPath(path);
|
||||
|
||||
if (!mount || mount.fstype !== "fuse") {
|
||||
throw new Error(`Path ${path} is not mounted as WebDAV.`);
|
||||
if (!mount || mount.mountPoint !== path) {
|
||||
throw new Error("Volume is not mounted");
|
||||
}
|
||||
|
||||
if (mount.fstype !== "fuse" && mount.fstype !== "davfs") {
|
||||
throw new Error(`Path ${path} is not mounted as WebDAV (found ${mount.fstype}).`);
|
||||
}
|
||||
|
||||
logger.debug(`WebDAV volume at ${path} is healthy and mounted.`);
|
||||
|
|
@ -154,8 +149,11 @@ const checkHealth = async (path: string) => {
|
|||
try {
|
||||
return await withTimeout(run(), OPERATION_TIMEOUT, "WebDAV health check");
|
||||
} catch (error) {
|
||||
logger.error("WebDAV volume health check failed:", toMessage(error));
|
||||
return { status: BACKEND_STATUS.error, error: toMessage(error) };
|
||||
const message = toMessage(error);
|
||||
if (message !== "Volume is not mounted") {
|
||||
logger.error("WebDAV volume health check failed:", message);
|
||||
}
|
||||
return { status: BACKEND_STATUS.error, error: message };
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
126
app/server/modules/backups/__tests__/backups.controller.test.ts
Normal file
126
app/server/modules/backups/__tests__/backups.controller.test.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import { test, describe, expect } from "bun:test";
|
||||
import { createApp } from "~/server/app";
|
||||
import { createTestSession } from "~/test/helpers/auth";
|
||||
|
||||
const app = createApp();
|
||||
|
||||
describe("backups security", () => {
|
||||
test("should return 401 if no session cookie is provided", async () => {
|
||||
const res = await app.request("/api/v1/backups");
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Authentication required");
|
||||
});
|
||||
|
||||
test("should return 401 if session is invalid", async () => {
|
||||
const res = await app.request("/api/v1/backups", {
|
||||
headers: {
|
||||
Cookie: "session_id=invalid-session",
|
||||
},
|
||||
});
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Invalid or expired session");
|
||||
|
||||
expect(res.headers.get("Set-Cookie")).toContain("session_id=;");
|
||||
});
|
||||
|
||||
test("should return 200 if session is valid", async () => {
|
||||
const { sessionId } = await createTestSession();
|
||||
|
||||
const res = await app.request("/api/v1/backups", {
|
||||
headers: {
|
||||
Cookie: `session_id=${sessionId}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
describe("unauthenticated access", () => {
|
||||
const endpoints: { method: string; path: string }[] = [
|
||||
{ method: "GET", path: "/api/v1/backups" },
|
||||
{ method: "GET", path: "/api/v1/backups/1" },
|
||||
{ method: "GET", path: "/api/v1/backups/volume/1" },
|
||||
{ method: "POST", path: "/api/v1/backups" },
|
||||
{ method: "PATCH", path: "/api/v1/backups/1" },
|
||||
{ method: "DELETE", path: "/api/v1/backups/1" },
|
||||
{ method: "POST", path: "/api/v1/backups/1/run" },
|
||||
{ method: "POST", path: "/api/v1/backups/1/stop" },
|
||||
{ method: "POST", path: "/api/v1/backups/1/forget" },
|
||||
{ method: "GET", path: "/api/v1/backups/1/notifications" },
|
||||
{ method: "PUT", path: "/api/v1/backups/1/notifications" },
|
||||
{ method: "GET", path: "/api/v1/backups/1/mirrors" },
|
||||
{ method: "PUT", path: "/api/v1/backups/1/mirrors" },
|
||||
{ method: "GET", path: "/api/v1/backups/1/mirrors/compatibility" },
|
||||
{ method: "POST", path: "/api/v1/backups/reorder" },
|
||||
];
|
||||
|
||||
for (const { method, path } of endpoints) {
|
||||
test(`${method} ${path} should return 401`, async () => {
|
||||
const res = await app.request(path, { method });
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Authentication required");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("information disclosure", () => {
|
||||
test("should not disclose if a schedule exists when unauthenticated", async () => {
|
||||
const res = await app.request("/api/v1/backups/999999");
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Authentication required");
|
||||
});
|
||||
|
||||
test("should not disclose if a volume exists when unauthenticated", async () => {
|
||||
const res = await app.request("/api/v1/backups/volume/999999");
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Authentication required");
|
||||
});
|
||||
});
|
||||
|
||||
describe("input validation", () => {
|
||||
test("should return 404 for malformed schedule ID", async () => {
|
||||
const { sessionId } = await createTestSession();
|
||||
const res = await app.request("/api/v1/backups/not-a-number", {
|
||||
headers: {
|
||||
Cookie: `session_id=${sessionId}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
test("should return 404 for non-existent schedule ID", async () => {
|
||||
const { sessionId } = await createTestSession();
|
||||
const res = await app.request("/api/v1/backups/999999", {
|
||||
headers: {
|
||||
Cookie: `session_id=${sessionId}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Backup schedule not found");
|
||||
});
|
||||
|
||||
test("should return 400 for invalid payload on create", async () => {
|
||||
const { sessionId } = await createTestSession();
|
||||
const res = await app.request("/api/v1/backups", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Cookie: `session_id=${sessionId}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: "Test",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
});
|
||||
136
app/server/modules/backups/__tests__/backups.patterns.test.ts
Normal file
136
app/server/modules/backups/__tests__/backups.patterns.test.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { test, describe, mock, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
||||
import { backupsService } from "../backups.service";
|
||||
import { createTestVolume } from "~/test/helpers/volume";
|
||||
import { createTestBackupSchedule } from "~/test/helpers/backup";
|
||||
import { createTestRepository } from "~/test/helpers/repository";
|
||||
import { generateBackupOutput } from "~/test/helpers/restic";
|
||||
import { getVolumePath } from "../../volumes/helpers";
|
||||
import { restic } from "~/server/utils/restic";
|
||||
import path from "node:path";
|
||||
|
||||
const backupMock = mock(() => Promise.resolve({ exitCode: 0, result: JSON.parse(generateBackupOutput()) }));
|
||||
|
||||
beforeEach(() => {
|
||||
backupMock.mockClear();
|
||||
spyOn(restic, "backup").mockImplementation(backupMock);
|
||||
spyOn(restic, "forget").mockImplementation(mock(() => Promise.resolve({ success: true })));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe("executeBackup - include / exclude patterns", () => {
|
||||
test("should correctly build include and exclude patterns", async () => {
|
||||
// arrange
|
||||
const volume = await createTestVolume();
|
||||
const repository = await createTestRepository();
|
||||
const volumePath = getVolumePath(volume);
|
||||
|
||||
const schedule = await createTestBackupSchedule({
|
||||
volumeId: volume.id,
|
||||
repositoryId: repository.id,
|
||||
includePatterns: ["*.zip", "/Photos", "!/Temp", "!*.log"],
|
||||
excludePatterns: [".DS_Store", "/Config", "!/Important", "!*.tmp"],
|
||||
excludeIfPresent: [".nobackup"],
|
||||
});
|
||||
|
||||
// act
|
||||
await backupsService.executeBackup(schedule.id);
|
||||
|
||||
// assert
|
||||
expect(backupMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
volumePath,
|
||||
expect.objectContaining({
|
||||
include: ["*.zip", path.join(volumePath, "Photos"), `!${path.join(volumePath, "Temp")}`, "!*.log"],
|
||||
exclude: [".DS_Store", path.join(volumePath, "Config"), `!${path.join(volumePath, "Important")}`, "!*.tmp"],
|
||||
excludeIfPresent: [".nobackup"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("should not join with volume path if pattern already starts with it", async () => {
|
||||
// arrange
|
||||
const volume = await createTestVolume();
|
||||
const volumePath = getVolumePath(volume);
|
||||
const repository = await createTestRepository();
|
||||
|
||||
const alreadyJoinedInclude = path.join(volumePath, "already/joined");
|
||||
const alreadyJoinedExclude = path.join(volumePath, "already/excluded");
|
||||
|
||||
const schedule = await createTestBackupSchedule({
|
||||
volumeId: volume.id,
|
||||
repositoryId: repository.id,
|
||||
includePatterns: [alreadyJoinedInclude],
|
||||
excludePatterns: [alreadyJoinedExclude],
|
||||
});
|
||||
|
||||
// act
|
||||
await backupsService.executeBackup(schedule.id);
|
||||
|
||||
// assert
|
||||
expect(backupMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
volumePath,
|
||||
expect.objectContaining({
|
||||
include: [alreadyJoinedInclude],
|
||||
exclude: [alreadyJoinedExclude],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("should correctly mix relative and absolute patterns", async () => {
|
||||
// arrange
|
||||
const volume = await createTestVolume();
|
||||
const volumePath = getVolumePath(volume);
|
||||
const repository = await createTestRepository();
|
||||
|
||||
const alreadyJoinedInclude = path.join(volumePath, "already/joined");
|
||||
const relativeInclude = "relative/include";
|
||||
const anchoredInclude = "/anchored/include";
|
||||
|
||||
const schedule = await createTestBackupSchedule({
|
||||
volumeId: volume.id,
|
||||
repositoryId: repository.id,
|
||||
includePatterns: [alreadyJoinedInclude, relativeInclude, anchoredInclude],
|
||||
});
|
||||
|
||||
// act
|
||||
await backupsService.executeBackup(schedule.id);
|
||||
|
||||
// assert
|
||||
expect(backupMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
volumePath,
|
||||
expect.objectContaining({
|
||||
include: [alreadyJoinedInclude, relativeInclude, path.join(volumePath, "anchored/include")],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle empty include and exclude patterns", async () => {
|
||||
// arrange
|
||||
const volume = await createTestVolume();
|
||||
const repository = await createTestRepository();
|
||||
const schedule = await createTestBackupSchedule({
|
||||
volumeId: volume.id,
|
||||
repositoryId: repository.id,
|
||||
includePatterns: [],
|
||||
excludePatterns: [],
|
||||
});
|
||||
|
||||
// act
|
||||
await backupsService.executeBackup(schedule.id);
|
||||
|
||||
// assert
|
||||
expect(backupMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
getVolumePath(volume),
|
||||
expect.not.objectContaining({
|
||||
include: expect.anything(),
|
||||
exclude: expect.anything(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
176
app/server/modules/backups/__tests__/backups.service.test.ts
Normal file
176
app/server/modules/backups/__tests__/backups.service.test.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import { test, describe, mock, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
||||
import { backupsService } from "../backups.service";
|
||||
import { createTestVolume } from "~/test/helpers/volume";
|
||||
import { createTestBackupSchedule } from "~/test/helpers/backup";
|
||||
import { createTestRepository } from "~/test/helpers/repository";
|
||||
import { generateBackupOutput } from "~/test/helpers/restic";
|
||||
import { faker } from "@faker-js/faker";
|
||||
import * as spawnModule from "~/server/utils/spawn";
|
||||
|
||||
const resticBackupMock = mock(() => Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }));
|
||||
|
||||
beforeEach(() => {
|
||||
resticBackupMock.mockClear();
|
||||
spyOn(spawnModule, "safeSpawn").mockImplementation(resticBackupMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe("execute backup", () => {
|
||||
test("should correctly set next backup time", async () => {
|
||||
// arrange
|
||||
const volume = await createTestVolume();
|
||||
const repository = await createTestRepository();
|
||||
const schedule = await createTestBackupSchedule({
|
||||
volumeId: volume.id,
|
||||
repositoryId: repository.id,
|
||||
cronExpression: "*/5 * * * *",
|
||||
});
|
||||
expect(schedule.nextBackupAt).toBeNull();
|
||||
|
||||
resticBackupMock.mockImplementationOnce(() =>
|
||||
Promise.resolve({ exitCode: 0, stdout: generateBackupOutput(), stderr: "" }),
|
||||
);
|
||||
|
||||
// act
|
||||
await backupsService.executeBackup(schedule.id);
|
||||
|
||||
// assert
|
||||
const updatedSchedule = await backupsService.getSchedule(schedule.id);
|
||||
expect(updatedSchedule.nextBackupAt).not.toBeNull();
|
||||
|
||||
const nextBackupAt = new Date(updatedSchedule.nextBackupAt ?? 0);
|
||||
const now = new Date();
|
||||
|
||||
expect(nextBackupAt.getTime()).toBeGreaterThanOrEqual(now.getTime());
|
||||
expect(nextBackupAt.getTime() - now.getTime()).toBeLessThanOrEqual(5 * 60 * 1000);
|
||||
});
|
||||
|
||||
test("should skip backup if schedule is disabled", async () => {
|
||||
// arrange
|
||||
const volume = await createTestVolume();
|
||||
const repository = await createTestRepository();
|
||||
const schedule = await createTestBackupSchedule({
|
||||
volumeId: volume.id,
|
||||
repositoryId: repository.id,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
// act
|
||||
await backupsService.executeBackup(schedule.id);
|
||||
|
||||
// assert
|
||||
expect(resticBackupMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should execute backup if schedule is disabled but the run is manual", async () => {
|
||||
// arrange
|
||||
const volume = await createTestVolume();
|
||||
const repository = await createTestRepository();
|
||||
const schedule = await createTestBackupSchedule({
|
||||
volumeId: volume.id,
|
||||
repositoryId: repository.id,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
resticBackupMock.mockImplementationOnce(() =>
|
||||
Promise.resolve({ exitCode: 0, stdout: generateBackupOutput(), stderr: "" }),
|
||||
);
|
||||
|
||||
// act
|
||||
await backupsService.executeBackup(schedule.id, true);
|
||||
|
||||
// assert
|
||||
expect(resticBackupMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should skip the backup if the previous one is still running", async () => {
|
||||
// arrange
|
||||
const volume = await createTestVolume();
|
||||
const repository = await createTestRepository();
|
||||
const schedule = await createTestBackupSchedule({
|
||||
volumeId: volume.id,
|
||||
repositoryId: repository.id,
|
||||
});
|
||||
|
||||
resticBackupMock.mockImplementation(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
return Promise.resolve({ exitCode: 0, stdout: generateBackupOutput(), stderr: "" });
|
||||
});
|
||||
|
||||
// act
|
||||
backupsService.executeBackup(schedule.id);
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
await backupsService.executeBackup(schedule.id);
|
||||
|
||||
// assert
|
||||
expect(resticBackupMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("should set the backup status to failed if restic returns a 3 exit code", async () => {
|
||||
// arrange
|
||||
const volume = await createTestVolume();
|
||||
const repository = await createTestRepository();
|
||||
const schedule = await createTestBackupSchedule({
|
||||
volumeId: volume.id,
|
||||
repositoryId: repository.id,
|
||||
});
|
||||
|
||||
resticBackupMock.mockImplementationOnce(() =>
|
||||
Promise.resolve({ exitCode: 3, stdout: generateBackupOutput(), stderr: "Some error occurred" }),
|
||||
);
|
||||
|
||||
// act
|
||||
await backupsService.executeBackup(schedule.id);
|
||||
|
||||
// assert
|
||||
const updatedSchedule = await backupsService.getSchedule(schedule.id);
|
||||
expect(updatedSchedule.lastBackupStatus).toBe("warning");
|
||||
});
|
||||
|
||||
test("should set the backup status to failed if restic returns a non zero exit code", async () => {
|
||||
// arrange
|
||||
const volume = await createTestVolume();
|
||||
const repository = await createTestRepository();
|
||||
const schedule = await createTestBackupSchedule({
|
||||
volumeId: volume.id,
|
||||
repositoryId: repository.id,
|
||||
});
|
||||
|
||||
resticBackupMock.mockImplementationOnce(() =>
|
||||
Promise.resolve({ exitCode: 1, stdout: generateBackupOutput(), stderr: "Some error occurred" }),
|
||||
);
|
||||
|
||||
// act
|
||||
await backupsService.executeBackup(schedule.id);
|
||||
|
||||
// assert
|
||||
const updatedSchedule = await backupsService.getSchedule(schedule.id);
|
||||
expect(updatedSchedule.lastBackupStatus).toBe("error");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSchedulesToExecute", () => {
|
||||
test("should return schedules with NULL lastBackupStatus", async () => {
|
||||
// arrange
|
||||
const volume = await createTestVolume();
|
||||
const repository = await createTestRepository();
|
||||
|
||||
const schedule = await createTestBackupSchedule({
|
||||
volumeId: volume.id,
|
||||
repositoryId: repository.id,
|
||||
enabled: true,
|
||||
cronExpression: "* * * * *",
|
||||
lastBackupStatus: null,
|
||||
nextBackupAt: faker.date.past().getTime(),
|
||||
});
|
||||
|
||||
// act
|
||||
const schedulesToExecute = await backupsService.getSchedulesToExecute();
|
||||
|
||||
// assert
|
||||
expect(schedulesToExecute).toContain(schedule.id);
|
||||
});
|
||||
});
|
||||
|
|
@ -86,8 +86,8 @@ export const backupScheduleController = new Hono()
|
|||
.post("/:scheduleId/run", runBackupNowDto, async (c) => {
|
||||
const scheduleId = c.req.param("scheduleId");
|
||||
|
||||
backupsService.executeBackup(Number(scheduleId), true).catch((error) => {
|
||||
console.error("Backup execution failed:", error);
|
||||
backupsService.executeBackup(Number(scheduleId), true).catch((err) => {
|
||||
console.error(`Error executing manual backup for schedule ${scheduleId}:`, err);
|
||||
});
|
||||
|
||||
return c.json<RunBackupNowDto>({ success: true }, 200);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ const backupScheduleSchema = type({
|
|||
excludePatterns: "string[] | null",
|
||||
excludeIfPresent: "string[] | null",
|
||||
includePatterns: "string[] | null",
|
||||
oneFileSystem: "boolean",
|
||||
lastBackupAt: "number | null",
|
||||
lastBackupStatus: "'success' | 'error' | 'in_progress' | 'warning' | null",
|
||||
lastBackupError: "string | null",
|
||||
|
|
@ -131,6 +132,7 @@ export const createBackupScheduleBody = type({
|
|||
excludePatterns: "string[]?",
|
||||
excludeIfPresent: "string[]?",
|
||||
includePatterns: "string[]?",
|
||||
oneFileSystem: "boolean?",
|
||||
tags: "string[]?",
|
||||
});
|
||||
|
||||
|
|
@ -168,6 +170,7 @@ export const updateBackupScheduleBody = type({
|
|||
excludePatterns: "string[]?",
|
||||
excludeIfPresent: "string[]?",
|
||||
includePatterns: "string[]?",
|
||||
oneFileSystem: "boolean?",
|
||||
tags: "string[]?",
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { and, asc, eq, ne } from "drizzle-orm";
|
||||
import { and, asc, eq, isNull, ne, or } from "drizzle-orm";
|
||||
import cron from "node-cron";
|
||||
import { CronExpressionParser } from "cron-parser";
|
||||
import { NotFoundError, BadRequestError, ConflictError } from "http-errors-enhanced";
|
||||
|
|
@ -13,6 +13,7 @@ import { serverEvents } from "../../core/events";
|
|||
import { notificationsService } from "../notifications/notifications.service";
|
||||
import { repoMutex } from "../../core/repository-mutex";
|
||||
import { checkMirrorCompatibility, getIncompatibleMirrorError } from "~/server/utils/backend-compatibility";
|
||||
import path from "node:path";
|
||||
|
||||
const runningBackups = new Map<number, AbortController>();
|
||||
|
||||
|
|
@ -32,6 +33,27 @@ const calculateNextRun = (cronExpression: string): number => {
|
|||
}
|
||||
};
|
||||
|
||||
const processPattern = (pattern: string, volumePath: string): string => {
|
||||
let isNegated = false;
|
||||
let p = pattern;
|
||||
|
||||
if (p.startsWith("!")) {
|
||||
isNegated = true;
|
||||
p = p.slice(1);
|
||||
}
|
||||
|
||||
if (p.startsWith(volumePath)) {
|
||||
return pattern;
|
||||
}
|
||||
|
||||
if (p.startsWith("/")) {
|
||||
const processed = path.join(volumePath, p.slice(1));
|
||||
return isNegated ? `!${processed}` : processed;
|
||||
}
|
||||
|
||||
return pattern;
|
||||
};
|
||||
|
||||
const listSchedules = async () => {
|
||||
const schedules = await db.query.backupSchedulesTable.findMany({
|
||||
with: {
|
||||
|
|
@ -102,6 +124,7 @@ const createSchedule = async (data: CreateBackupScheduleBody) => {
|
|||
excludePatterns: data.excludePatterns ?? [],
|
||||
excludeIfPresent: data.excludeIfPresent ?? [],
|
||||
includePatterns: data.includePatterns ?? [],
|
||||
oneFileSystem: data.oneFileSystem,
|
||||
nextBackupAt: nextBackupAt,
|
||||
})
|
||||
.returning();
|
||||
|
|
@ -251,14 +274,16 @@ const executeBackup = async (scheduleId: number, manual = false) => {
|
|||
excludeIfPresent?: string[];
|
||||
include?: string[];
|
||||
tags?: string[];
|
||||
oneFileSystem?: boolean;
|
||||
signal?: AbortSignal;
|
||||
} = {
|
||||
tags: [schedule.id.toString()],
|
||||
oneFileSystem: schedule.oneFileSystem,
|
||||
signal: abortController.signal,
|
||||
};
|
||||
|
||||
if (schedule.excludePatterns && schedule.excludePatterns.length > 0) {
|
||||
backupOptions.exclude = schedule.excludePatterns;
|
||||
backupOptions.exclude = schedule.excludePatterns.map((p) => processPattern(p, volumePath));
|
||||
}
|
||||
|
||||
if (schedule.excludeIfPresent && schedule.excludeIfPresent.length > 0) {
|
||||
|
|
@ -266,7 +291,7 @@ const executeBackup = async (scheduleId: number, manual = false) => {
|
|||
}
|
||||
|
||||
if (schedule.includePatterns && schedule.includePatterns.length > 0) {
|
||||
backupOptions.include = schedule.includePatterns;
|
||||
backupOptions.include = schedule.includePatterns.map((p) => processPattern(p, volumePath));
|
||||
}
|
||||
|
||||
const releaseBackupLock = await repoMutex.acquireShared(repository.id, `backup:${volume.name}`);
|
||||
|
|
@ -363,8 +388,6 @@ const executeBackup = async (scheduleId: number, manual = false) => {
|
|||
.catch((notifError) => {
|
||||
logger.error(`Failed to send backup failure notification: ${toMessage(notifError)}`);
|
||||
});
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
runningBackups.delete(scheduleId);
|
||||
}
|
||||
|
|
@ -373,7 +396,10 @@ const executeBackup = async (scheduleId: number, manual = false) => {
|
|||
const getSchedulesToExecute = async () => {
|
||||
const now = Date.now();
|
||||
const schedules = await db.query.backupSchedulesTable.findMany({
|
||||
where: and(eq(backupSchedulesTable.enabled, true), ne(backupSchedulesTable.lastBackupStatus, "in_progress")),
|
||||
where: and(
|
||||
eq(backupSchedulesTable.enabled, true),
|
||||
or(ne(backupSchedulesTable.lastBackupStatus, "in_progress"), isNull(backupSchedulesTable.lastBackupStatus)),
|
||||
),
|
||||
});
|
||||
|
||||
const schedulesToRun: number[] = [];
|
||||
|
|
@ -405,23 +431,25 @@ const stopBackup = async (scheduleId: number) => {
|
|||
throw new NotFoundError("Backup schedule not found");
|
||||
}
|
||||
|
||||
await db
|
||||
.update(backupSchedulesTable)
|
||||
.set({
|
||||
lastBackupStatus: "error",
|
||||
lastBackupError: "Backup was stopped by user",
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(backupSchedulesTable.id, scheduleId));
|
||||
try {
|
||||
const abortController = runningBackups.get(scheduleId);
|
||||
if (!abortController) {
|
||||
throw new ConflictError("No backup is currently running for this schedule");
|
||||
}
|
||||
|
||||
const abortController = runningBackups.get(scheduleId);
|
||||
if (!abortController) {
|
||||
throw new ConflictError("No backup is currently running for this schedule");
|
||||
logger.info(`Stopping backup for schedule ${scheduleId}`);
|
||||
|
||||
abortController.abort();
|
||||
} finally {
|
||||
await db
|
||||
.update(backupSchedulesTable)
|
||||
.set({
|
||||
lastBackupStatus: "warning",
|
||||
lastBackupError: "Backup was stopped by user",
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(backupSchedulesTable.id, scheduleId));
|
||||
}
|
||||
|
||||
logger.info(`Stopping backup for schedule ${scheduleId}`);
|
||||
|
||||
abortController.abort();
|
||||
};
|
||||
|
||||
const runForget = async (scheduleId: number, repositoryId?: string) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
import { test, describe, expect } from "bun:test";
|
||||
import { createApp } from "~/server/app";
|
||||
import { createTestSession } from "~/test/helpers/auth";
|
||||
|
||||
const app = createApp();
|
||||
|
||||
describe("events security", () => {
|
||||
test("should return 401 if no session cookie is provided", async () => {
|
||||
const res = await app.request("/api/v1/events");
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Authentication required");
|
||||
});
|
||||
|
||||
test("should return 401 if session is invalid", async () => {
|
||||
const res = await app.request("/api/v1/events", {
|
||||
headers: {
|
||||
Cookie: "session_id=invalid-session",
|
||||
},
|
||||
});
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Invalid or expired session");
|
||||
|
||||
expect(res.headers.get("Set-Cookie")).toContain("session_id=;");
|
||||
});
|
||||
|
||||
test("should return 200 if session is valid", async () => {
|
||||
const { sessionId } = await createTestSession();
|
||||
|
||||
const res = await app.request("/api/v1/events", {
|
||||
headers: {
|
||||
Cookie: `session_id=${sessionId}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get("Content-Type")).toBe("text/event-stream");
|
||||
});
|
||||
|
||||
describe("unauthenticated access", () => {
|
||||
const endpoints: { method: string; path: string }[] = [{ method: "GET", path: "/api/v1/events" }];
|
||||
|
||||
for (const { method, path } of endpoints) {
|
||||
test(`${method} ${path} should return 401`, async () => {
|
||||
const res = await app.request(path, { method });
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Authentication required");
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { Scheduler } from "../../core/scheduler";
|
||||
import { and, eq, or } from "drizzle-orm";
|
||||
import { db } from "../../db/db";
|
||||
import { volumesTable } from "../../db/schema";
|
||||
import { backupSchedulesTable, volumesTable } from "../../db/schema";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { restic } from "../../utils/restic";
|
||||
import { volumeService } from "../volumes/volume.service";
|
||||
|
|
@ -10,6 +10,35 @@ import { VolumeHealthCheckJob } from "../../jobs/healthchecks";
|
|||
import { RepositoryHealthCheckJob } from "../../jobs/repository-healthchecks";
|
||||
import { BackupExecutionJob } from "../../jobs/backup-execution";
|
||||
import { CleanupSessionsJob } from "../../jobs/cleanup-sessions";
|
||||
import { repositoriesService } from "../repositories/repositories.service";
|
||||
import { notificationsService } from "../notifications/notifications.service";
|
||||
import { VolumeAutoRemountJob } from "~/server/jobs/auto-remount";
|
||||
|
||||
const ensureLatestConfigurationSchema = async () => {
|
||||
const volumes = await db.query.volumesTable.findMany({});
|
||||
|
||||
for (const volume of volumes) {
|
||||
await volumeService.updateVolume(volume.name, volume).catch((err) => {
|
||||
logger.error(`Failed to update volume ${volume.name}: ${err}`);
|
||||
});
|
||||
}
|
||||
|
||||
const repositories = await db.query.repositoriesTable.findMany({});
|
||||
|
||||
for (const repo of repositories) {
|
||||
await repositoriesService.updateRepository(repo.name, {}).catch((err) => {
|
||||
logger.error(`Failed to update repository ${repo.name}: ${err}`);
|
||||
});
|
||||
}
|
||||
|
||||
const notifications = await db.query.notificationDestinationsTable.findMany({});
|
||||
|
||||
for (const notification of notifications) {
|
||||
await notificationsService.updateDestination(notification.id, notification).catch((err) => {
|
||||
logger.error(`Failed to update notification destination ${notification.id}: ${err}`);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const startup = async () => {
|
||||
await Scheduler.start();
|
||||
|
|
@ -19,6 +48,8 @@ export const startup = async () => {
|
|||
logger.error(`Error ensuring restic passfile exists: ${err.message}`);
|
||||
});
|
||||
|
||||
await ensureLatestConfigurationSchema();
|
||||
|
||||
const volumes = await db.query.volumesTable.findMany({
|
||||
where: or(
|
||||
eq(volumesTable.status, "mounted"),
|
||||
|
|
@ -32,9 +63,22 @@ export const startup = async () => {
|
|||
});
|
||||
}
|
||||
|
||||
await db
|
||||
.update(backupSchedulesTable)
|
||||
.set({
|
||||
lastBackupStatus: "warning",
|
||||
lastBackupError: "Zerobyte was restarted during the last scheduled backup",
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(backupSchedulesTable.lastBackupStatus, "in_progress"))
|
||||
.catch((err) => {
|
||||
logger.error(`Failed to update stuck backup schedules on startup: ${err.message}`);
|
||||
});
|
||||
|
||||
Scheduler.build(CleanupDanglingMountsJob).schedule("0 * * * *");
|
||||
Scheduler.build(VolumeHealthCheckJob).schedule("*/30 * * * *");
|
||||
Scheduler.build(RepositoryHealthCheckJob).schedule("50 12 * * *");
|
||||
Scheduler.build(BackupExecutionJob).schedule("* * * * *");
|
||||
Scheduler.build(CleanupSessionsJob).schedule("0 0 * * *");
|
||||
Scheduler.build(VolumeAutoRemountJob).schedule("*/5 * * * *");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
import { test, describe, expect } from "bun:test";
|
||||
import { createApp } from "~/server/app";
|
||||
import { createTestSession } from "~/test/helpers/auth";
|
||||
|
||||
const app = createApp();
|
||||
|
||||
describe("notifications security", () => {
|
||||
test("should return 401 if no session cookie is provided", async () => {
|
||||
const res = await app.request("/api/v1/notifications/destinations");
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Authentication required");
|
||||
});
|
||||
|
||||
test("should return 401 if session is invalid", async () => {
|
||||
const res = await app.request("/api/v1/notifications/destinations", {
|
||||
headers: {
|
||||
Cookie: "session_id=invalid-session",
|
||||
},
|
||||
});
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Invalid or expired session");
|
||||
|
||||
expect(res.headers.get("Set-Cookie")).toContain("session_id=;");
|
||||
});
|
||||
|
||||
test("should return 200 if session is valid", async () => {
|
||||
const { sessionId } = await createTestSession();
|
||||
|
||||
const res = await app.request("/api/v1/notifications/destinations", {
|
||||
headers: {
|
||||
Cookie: `session_id=${sessionId}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
describe("unauthenticated access", () => {
|
||||
const endpoints: { method: string; path: string }[] = [
|
||||
{ method: "GET", path: "/api/v1/notifications/destinations" },
|
||||
{ method: "POST", path: "/api/v1/notifications/destinations" },
|
||||
{ method: "GET", path: "/api/v1/notifications/destinations/1" },
|
||||
{ method: "PATCH", path: "/api/v1/notifications/destinations/1" },
|
||||
{ method: "DELETE", path: "/api/v1/notifications/destinations/1" },
|
||||
{ method: "POST", path: "/api/v1/notifications/destinations/1/test" },
|
||||
];
|
||||
|
||||
for (const { method, path } of endpoints) {
|
||||
test(`${method} ${path} should return 401`, async () => {
|
||||
const res = await app.request(path, { method });
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Authentication required");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("information disclosure", () => {
|
||||
test("should not disclose if a destination exists when unauthenticated", async () => {
|
||||
const res = await app.request("/api/v1/notifications/destinations/999999");
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Authentication required");
|
||||
});
|
||||
});
|
||||
|
||||
describe("input validation", () => {
|
||||
test("should return 404 for malformed destination ID", async () => {
|
||||
const { sessionId } = await createTestSession();
|
||||
const res = await app.request("/api/v1/notifications/destinations/not-a-number", {
|
||||
headers: {
|
||||
Cookie: `session_id=${sessionId}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
test("should return 404 for non-existent destination ID", async () => {
|
||||
const { sessionId } = await createTestSession();
|
||||
const res = await app.request("/api/v1/notifications/destinations/999999", {
|
||||
headers: {
|
||||
Cookie: `session_id=${sessionId}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Notification destination not found");
|
||||
});
|
||||
|
||||
test("should return 400 for invalid payload on create", async () => {
|
||||
const { sessionId } = await createTestSession();
|
||||
const res = await app.request("/api/v1/notifications/destinations", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Cookie: `session_id=${sessionId}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: "Test",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -11,8 +11,9 @@ import { cryptoUtils } from "../../utils/crypto";
|
|||
import { logger } from "../../utils/logger";
|
||||
import { sendNotification } from "../../utils/shoutrrr";
|
||||
import { buildShoutrrrUrl } from "./builders";
|
||||
import type { NotificationConfig, NotificationEvent } from "~/schemas/notifications";
|
||||
import { notificationConfigSchema, type NotificationConfig, type NotificationEvent } from "~/schemas/notifications";
|
||||
import { toMessage } from "../../utils/errors";
|
||||
import { type } from "arktype";
|
||||
|
||||
const listDestinations = async () => {
|
||||
const destinations = await db.query.notificationDestinationsTable.findMany({
|
||||
|
|
@ -187,12 +188,15 @@ const updateDestination = async (
|
|||
updateData.enabled = updates.enabled;
|
||||
}
|
||||
|
||||
if (updates.config !== undefined) {
|
||||
const encryptedConfig = await encryptSensitiveFields(updates.config);
|
||||
updateData.config = encryptedConfig;
|
||||
updateData.type = updates.config.type;
|
||||
const newConfig = notificationConfigSchema(updates.config || existing.config);
|
||||
if (newConfig instanceof type.errors) {
|
||||
throw new InternalServerError("Invalid notification configuration");
|
||||
}
|
||||
|
||||
const encryptedConfig = await encryptSensitiveFields(newConfig);
|
||||
updateData.config = encryptedConfig;
|
||||
updateData.type = newConfig.type;
|
||||
|
||||
const [updated] = await db
|
||||
.update(notificationDestinationsTable)
|
||||
.set(updateData)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
import { test, describe, expect } from "bun:test";
|
||||
import { createApp } from "~/server/app";
|
||||
import { createTestSession } from "~/test/helpers/auth";
|
||||
|
||||
const app = createApp();
|
||||
|
||||
describe("repositories security", () => {
|
||||
test("should return 401 if no session cookie is provided", async () => {
|
||||
const res = await app.request("/api/v1/repositories");
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Authentication required");
|
||||
});
|
||||
|
||||
test("should return 401 if session is invalid", async () => {
|
||||
const res = await app.request("/api/v1/repositories", {
|
||||
headers: {
|
||||
Cookie: "session_id=invalid-session",
|
||||
},
|
||||
});
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Invalid or expired session");
|
||||
|
||||
expect(res.headers.get("Set-Cookie")).toContain("session_id=;");
|
||||
});
|
||||
|
||||
test("should return 200 if session is valid", async () => {
|
||||
const { sessionId } = await createTestSession();
|
||||
|
||||
const res = await app.request("/api/v1/repositories", {
|
||||
headers: {
|
||||
Cookie: `session_id=${sessionId}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
describe("unauthenticated access", () => {
|
||||
const endpoints: { method: string; path: string }[] = [
|
||||
{ method: "GET", path: "/api/v1/repositories" },
|
||||
{ method: "POST", path: "/api/v1/repositories" },
|
||||
{ method: "GET", path: "/api/v1/repositories/rclone-remotes" },
|
||||
{ method: "GET", path: "/api/v1/repositories/test-repo" },
|
||||
{ method: "DELETE", path: "/api/v1/repositories/test-repo" },
|
||||
{ method: "GET", path: "/api/v1/repositories/test-repo/snapshots" },
|
||||
{ method: "GET", path: "/api/v1/repositories/test-repo/snapshots/test-snapshot" },
|
||||
{ method: "GET", path: "/api/v1/repositories/test-repo/snapshots/test-snapshot/files" },
|
||||
{ method: "POST", path: "/api/v1/repositories/test-repo/restore" },
|
||||
{ method: "POST", path: "/api/v1/repositories/test-repo/doctor" },
|
||||
{ method: "DELETE", path: "/api/v1/repositories/test-repo/snapshots/test-snapshot" },
|
||||
{ method: "PATCH", path: "/api/v1/repositories/test-repo" },
|
||||
];
|
||||
|
||||
for (const { method, path } of endpoints) {
|
||||
test(`${method} ${path} should return 401`, async () => {
|
||||
const res = await app.request(path, { method });
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Authentication required");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("information disclosure", () => {
|
||||
test("should not disclose if a repository exists when unauthenticated", async () => {
|
||||
const res = await app.request("/api/v1/repositories/non-existent-repo");
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Authentication required");
|
||||
});
|
||||
});
|
||||
|
||||
describe("input validation", () => {
|
||||
test("should return 404 for non-existent repository", async () => {
|
||||
const { sessionId } = await createTestSession();
|
||||
const res = await app.request("/api/v1/repositories/non-existent-repo", {
|
||||
headers: {
|
||||
Cookie: `session_id=${sessionId}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Repository not found");
|
||||
});
|
||||
|
||||
test("should return 400 for invalid payload on create", async () => {
|
||||
const { sessionId } = await createTestSession();
|
||||
const res = await app.request("/api/v1/repositories", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Cookie: `session_id=${sessionId}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: "Test",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -9,7 +9,13 @@ import { generateShortId } from "../../utils/id";
|
|||
import { restic } from "../../utils/restic";
|
||||
import { cryptoUtils } from "../../utils/crypto";
|
||||
import { repoMutex } from "../../core/repository-mutex";
|
||||
import type { CompressionMode, OverwriteMode, RepositoryConfig } from "~/schemas/restic";
|
||||
import {
|
||||
repositoryConfigSchema,
|
||||
type CompressionMode,
|
||||
type OverwriteMode,
|
||||
type RepositoryConfig,
|
||||
} from "~/schemas/restic";
|
||||
import { type } from "arktype";
|
||||
|
||||
const listRepositories = async () => {
|
||||
const repositories = await db.query.repositoriesTable.findMany({});
|
||||
|
|
@ -424,6 +430,13 @@ const updateRepository = async (name: string, updates: { name?: string; compress
|
|||
throw new ConflictError("Cannot rename an imported local repository");
|
||||
}
|
||||
|
||||
const newConfig = repositoryConfigSchema(existing.config);
|
||||
if (newConfig instanceof type.errors) {
|
||||
throw new InternalServerError("Invalid repository configuration");
|
||||
}
|
||||
|
||||
const encryptedConfig = await encryptConfig(newConfig);
|
||||
|
||||
let newName = existing.name;
|
||||
if (updates.name !== undefined && updates.name !== existing.name) {
|
||||
const newSlug = slugify(updates.name, { lower: true, strict: true });
|
||||
|
|
@ -445,6 +458,7 @@ const updateRepository = async (name: string, updates: { name?: string; compress
|
|||
name: newName,
|
||||
compressionMode: updates.compressionMode ?? existing.compressionMode,
|
||||
updatedAt: Date.now(),
|
||||
config: encryptedConfig,
|
||||
})
|
||||
.where(eq(repositoriesTable.id, existing.id))
|
||||
.returning();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
import { test, describe, expect } from "bun:test";
|
||||
import { createApp } from "~/server/app";
|
||||
import { createTestSession } from "~/test/helpers/auth";
|
||||
|
||||
const app = createApp();
|
||||
|
||||
describe("system security", () => {
|
||||
test("should return 401 if no session cookie is provided", async () => {
|
||||
const res = await app.request("/api/v1/system/info");
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Authentication required");
|
||||
});
|
||||
|
||||
test("should return 401 if session is invalid", async () => {
|
||||
const res = await app.request("/api/v1/system/info", {
|
||||
headers: {
|
||||
Cookie: "session_id=invalid-session",
|
||||
},
|
||||
});
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Invalid or expired session");
|
||||
|
||||
expect(res.headers.get("Set-Cookie")).toContain("session_id=;");
|
||||
});
|
||||
|
||||
test("should return 200 if session is valid", async () => {
|
||||
const { sessionId } = await createTestSession();
|
||||
|
||||
const res = await app.request("/api/v1/system/info", {
|
||||
headers: {
|
||||
Cookie: `session_id=${sessionId}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
describe("unauthenticated access", () => {
|
||||
const endpoints: { method: string; path: string }[] = [
|
||||
{ method: "GET", path: "/api/v1/system/info" },
|
||||
{ method: "POST", path: "/api/v1/system/restic-password" },
|
||||
];
|
||||
|
||||
for (const { method, path } of endpoints) {
|
||||
test(`${method} ${path} should return 401`, async () => {
|
||||
const res = await app.request(path, { method });
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Authentication required");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("input validation", () => {
|
||||
test("should return 400 for invalid payload on restic-password", async () => {
|
||||
const { sessionId } = await createTestSession();
|
||||
const res = await app.request("/api/v1/system/restic-password", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Cookie: `session_id=${sessionId}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
test("should return 401 for incorrect password on restic-password", async () => {
|
||||
const { sessionId } = await createTestSession();
|
||||
const res = await app.request("/api/v1/system/restic-password", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Cookie: `session_id=${sessionId}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
password: "wrong-password",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Incorrect password");
|
||||
});
|
||||
});
|
||||
});
|
||||
104
app/server/modules/volumes/__tests__/volumes.controller.test.ts
Normal file
104
app/server/modules/volumes/__tests__/volumes.controller.test.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { test, describe, expect } from "bun:test";
|
||||
import { createApp } from "~/server/app";
|
||||
import { createTestSession } from "~/test/helpers/auth";
|
||||
|
||||
const app = createApp();
|
||||
|
||||
describe("volumes security", () => {
|
||||
test("should return 401 if no session cookie is provided", async () => {
|
||||
const res = await app.request("/api/v1/volumes");
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Authentication required");
|
||||
});
|
||||
|
||||
test("should return 401 if session is invalid", async () => {
|
||||
const res = await app.request("/api/v1/volumes", {
|
||||
headers: {
|
||||
Cookie: "session_id=invalid-session",
|
||||
},
|
||||
});
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Invalid or expired session");
|
||||
|
||||
expect(res.headers.get("Set-Cookie")).toContain("session_id=;");
|
||||
});
|
||||
|
||||
test("should return 200 if session is valid", async () => {
|
||||
const { sessionId } = await createTestSession();
|
||||
|
||||
const res = await app.request("/api/v1/volumes", {
|
||||
headers: {
|
||||
Cookie: `session_id=${sessionId}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
describe("unauthenticated access", () => {
|
||||
const endpoints: { method: string; path: string }[] = [
|
||||
{ method: "GET", path: "/api/v1/volumes" },
|
||||
{ method: "POST", path: "/api/v1/volumes" },
|
||||
{ method: "POST", path: "/api/v1/volumes/test-connection" },
|
||||
{ method: "DELETE", path: "/api/v1/volumes/test-volume" },
|
||||
{ method: "GET", path: "/api/v1/volumes/test-volume" },
|
||||
{ method: "PUT", path: "/api/v1/volumes/test-volume" },
|
||||
{ method: "POST", path: "/api/v1/volumes/test-volume/mount" },
|
||||
{ method: "POST", path: "/api/v1/volumes/test-volume/unmount" },
|
||||
{ method: "POST", path: "/api/v1/volumes/test-volume/health-check" },
|
||||
{ method: "GET", path: "/api/v1/volumes/test-volume/files" },
|
||||
{ method: "GET", path: "/api/v1/volumes/filesystem/browse" },
|
||||
];
|
||||
|
||||
for (const { method, path } of endpoints) {
|
||||
test(`${method} ${path} should return 401`, async () => {
|
||||
const res = await app.request(path, { method });
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Authentication required");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("information disclosure", () => {
|
||||
test("should not disclose if a volume exists when unauthenticated", async () => {
|
||||
const res = await app.request("/api/v1/volumes/non-existent-volume");
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Authentication required");
|
||||
});
|
||||
});
|
||||
|
||||
describe("input validation", () => {
|
||||
test("should return 404 for non-existent volume", async () => {
|
||||
const { sessionId } = await createTestSession();
|
||||
const res = await app.request("/api/v1/volumes/non-existent-volume", {
|
||||
headers: {
|
||||
Cookie: `session_id=${sessionId}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Volume not found");
|
||||
});
|
||||
|
||||
test("should return 400 for invalid payload on create", async () => {
|
||||
const { sessionId } = await createTestSession();
|
||||
const res = await app.request("/api/v1/volumes", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Cookie: `session_id=${sessionId}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: "Test",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -16,7 +16,8 @@ import type { UpdateVolumeBody } from "./volume.dto";
|
|||
import { getVolumePath } from "./helpers";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { serverEvents } from "../../core/events";
|
||||
import type { BackendConfig } from "~/schemas/volumes";
|
||||
import { volumeConfigSchema, type BackendConfig } from "~/schemas/volumes";
|
||||
import { type } from "arktype";
|
||||
|
||||
async function encryptSensitiveFields(config: BackendConfig): Promise<BackendConfig> {
|
||||
switch (config.backend) {
|
||||
|
|
@ -192,7 +193,12 @@ const updateVolume = async (name: string, volumeData: UpdateVolumeBody) => {
|
|||
await backend.unmount();
|
||||
}
|
||||
|
||||
const encryptedConfig = volumeData.config ? await encryptSensitiveFields(volumeData.config) : undefined;
|
||||
const newConfig = volumeConfigSchema(volumeData.config || existing.config);
|
||||
if (newConfig instanceof type.errors) {
|
||||
throw new InternalServerError("Invalid volume configuration");
|
||||
}
|
||||
|
||||
const encryptedConfig = await encryptSensitiveFields(newConfig);
|
||||
|
||||
const [updated] = await db
|
||||
.update(volumesTable)
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ import path from "node:path";
|
|||
import os from "node:os";
|
||||
import { throttle } from "es-toolkit";
|
||||
import { type } from "arktype";
|
||||
import { $ } from "bun";
|
||||
import { REPOSITORY_BASE, RESTIC_PASS_FILE } from "../core/constants";
|
||||
import { REPOSITORY_BASE, RESTIC_PASS_FILE, DEFAULT_EXCLUDES } from "../core/constants";
|
||||
import { logger } from "./logger";
|
||||
import { cryptoUtils } from "./crypto";
|
||||
import type { RetentionPolicy } from "../modules/backups/backups.dto";
|
||||
|
|
@ -29,6 +28,7 @@ const backupOutputSchema = type({
|
|||
total_duration: "number",
|
||||
snapshot_id: "string",
|
||||
});
|
||||
export type BackupOutput = typeof backupOutputSchema.infer;
|
||||
|
||||
const snapshotInfoSchema = type({
|
||||
gid: "number?",
|
||||
|
|
@ -176,6 +176,10 @@ const buildEnv = async (config: RepositoryConfig) => {
|
|||
"UserKnownHostsFile=/dev/null",
|
||||
"-o",
|
||||
"LogLevel=VERBOSE",
|
||||
"-o",
|
||||
"ServerAliveInterval=60",
|
||||
"-o",
|
||||
"ServerAliveCountMax=240",
|
||||
"-i",
|
||||
keyPath,
|
||||
];
|
||||
|
|
@ -205,12 +209,12 @@ const init = async (config: RepositoryConfig) => {
|
|||
const args = ["init", "--repo", repoUrl];
|
||||
addCommonArgs(args, env);
|
||||
|
||||
const res = await $`restic ${args}`.env(env).nothrow();
|
||||
const res = await safeSpawn({ command: "restic", args, env });
|
||||
await cleanupTemporaryKeys(config, env);
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
logger.error(`Restic init failed: ${res.stderr}`);
|
||||
return { success: false, error: res.stderr.toString() };
|
||||
return { success: false, error: res.stderr };
|
||||
}
|
||||
|
||||
logger.info(`Restic repository initialized: ${repoUrl}`);
|
||||
|
|
@ -238,6 +242,7 @@ const backup = async (
|
|||
excludeIfPresent?: string[];
|
||||
include?: string[];
|
||||
tags?: string[];
|
||||
oneFileSystem?: boolean;
|
||||
compressionMode?: CompressionMode;
|
||||
signal?: AbortSignal;
|
||||
onProgress?: (progress: BackupProgress) => void;
|
||||
|
|
@ -246,14 +251,11 @@ const backup = async (
|
|||
const repoUrl = buildRepoUrl(config);
|
||||
const env = await buildEnv(config);
|
||||
|
||||
const args: string[] = [
|
||||
"--repo",
|
||||
repoUrl,
|
||||
"backup",
|
||||
"--one-file-system",
|
||||
"--compression",
|
||||
options?.compressionMode ?? "auto",
|
||||
];
|
||||
const args: string[] = ["--repo", repoUrl, "backup", "--compression", options?.compressionMode ?? "auto"];
|
||||
|
||||
if (options?.oneFileSystem) {
|
||||
args.push("--one-file-system");
|
||||
}
|
||||
|
||||
if (options?.tags && options.tags.length > 0) {
|
||||
for (const tag of options.tags) {
|
||||
|
|
@ -266,19 +268,25 @@ const backup = async (
|
|||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-restic-include-"));
|
||||
includeFile = path.join(tmp, `include.txt`);
|
||||
|
||||
const includePaths = options.include.map((p) => path.join(source, p));
|
||||
|
||||
await fs.writeFile(includeFile, includePaths.join("\n"), "utf-8");
|
||||
await fs.writeFile(includeFile, options.include.join("\n"), "utf-8");
|
||||
|
||||
args.push("--files-from", includeFile);
|
||||
} else {
|
||||
args.push(source);
|
||||
}
|
||||
|
||||
for (const exclude of DEFAULT_EXCLUDES) {
|
||||
args.push("--exclude", exclude);
|
||||
}
|
||||
|
||||
let excludeFile: string | null = null;
|
||||
if (options?.exclude && options.exclude.length > 0) {
|
||||
for (const pattern of options.exclude) {
|
||||
args.push("--exclude", pattern);
|
||||
}
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-restic-exclude-"));
|
||||
excludeFile = path.join(tmp, `exclude.txt`);
|
||||
|
||||
await fs.writeFile(excludeFile, options.exclude.join("\n"), "utf-8");
|
||||
|
||||
args.push("--exclude-file", excludeFile);
|
||||
}
|
||||
|
||||
if (options?.excludeIfPresent && options.excludeIfPresent.length > 0) {
|
||||
|
|
@ -325,22 +333,28 @@ const backup = async (
|
|||
},
|
||||
finally: async () => {
|
||||
includeFile && (await fs.unlink(includeFile).catch(() => {}));
|
||||
excludeFile && (await fs.unlink(excludeFile).catch(() => {}));
|
||||
await cleanupTemporaryKeys(config, env);
|
||||
},
|
||||
});
|
||||
|
||||
if (options?.signal?.aborted) {
|
||||
logger.warn("Restic backup was aborted by signal.");
|
||||
return { result: null, exitCode: res.exitCode };
|
||||
}
|
||||
|
||||
if (res.exitCode === 3) {
|
||||
logger.error(`Restic backup encountered read errors: ${res.stderr.toString()}`);
|
||||
logger.error(`Restic backup encountered read errors: ${res.stderr}`);
|
||||
}
|
||||
|
||||
if (res.exitCode !== 0 && res.exitCode !== 3) {
|
||||
logger.error(`Restic backup failed: ${res.stderr.toString()}`);
|
||||
logger.error(`Restic backup failed: ${res.stderr}`);
|
||||
logger.error(`Command executed: restic ${args.join(" ")}`);
|
||||
|
||||
throw new ResticError(res.exitCode, res.stderr.toString());
|
||||
throw new ResticError(res.exitCode, res.stderr);
|
||||
}
|
||||
|
||||
const lastLine = stdout.trim();
|
||||
const lastLine = (stdout || res.stdout).trim();
|
||||
let summaryLine = "";
|
||||
try {
|
||||
const resSummary = JSON.parse(lastLine ?? "{}");
|
||||
|
|
@ -416,16 +430,16 @@ const restore = async (
|
|||
addCommonArgs(args, env);
|
||||
|
||||
logger.debug(`Executing: restic ${args.join(" ")}`);
|
||||
const res = await $`restic ${args}`.env(env).nothrow();
|
||||
const res = await safeSpawn({ command: "restic", args, env });
|
||||
|
||||
await cleanupTemporaryKeys(config, env);
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
logger.error(`Restic restore failed: ${res.stderr}`);
|
||||
throw new ResticError(res.exitCode, res.stderr.toString());
|
||||
throw new ResticError(res.exitCode, res.stderr);
|
||||
}
|
||||
|
||||
const stdout = res.text();
|
||||
const outputLines = stdout.trim().split("\n");
|
||||
const outputLines = res.stdout.trim().split("\n");
|
||||
const lastLine = outputLines[outputLines.length - 1];
|
||||
|
||||
if (!lastLine) {
|
||||
|
|
@ -478,7 +492,7 @@ const snapshots = async (config: RepositoryConfig, options: { tags?: string[] }
|
|||
|
||||
addCommonArgs(args, env);
|
||||
|
||||
const res = await $`restic ${args}`.env(env).nothrow().quiet();
|
||||
const res = await safeSpawn({ command: "restic", args, env });
|
||||
await cleanupTemporaryKeys(config, env);
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
|
|
@ -486,7 +500,7 @@ const snapshots = async (config: RepositoryConfig, options: { tags?: string[] }
|
|||
throw new Error(`Restic snapshots retrieval failed: ${res.stderr}`);
|
||||
}
|
||||
|
||||
const result = snapshotInfoSchema.array()(res.json());
|
||||
const result = snapshotInfoSchema.array()(JSON.parse(res.stdout));
|
||||
|
||||
if (result instanceof type.errors) {
|
||||
logger.error(`Restic snapshots output validation failed: ${result}`);
|
||||
|
|
@ -527,12 +541,12 @@ const forget = async (config: RepositoryConfig, options: RetentionPolicy, extra:
|
|||
args.push("--prune");
|
||||
addCommonArgs(args, env);
|
||||
|
||||
const res = await $`restic ${args}`.env(env).nothrow();
|
||||
const res = await safeSpawn({ command: "restic", args, env });
|
||||
await cleanupTemporaryKeys(config, env);
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
logger.error(`Restic forget failed: ${res.stderr}`);
|
||||
throw new ResticError(res.exitCode, res.stderr.toString());
|
||||
throw new ResticError(res.exitCode, res.stderr);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
|
|
@ -545,12 +559,12 @@ const deleteSnapshot = async (config: RepositoryConfig, snapshotId: string) => {
|
|||
const args: string[] = ["--repo", repoUrl, "forget", snapshotId, "--prune"];
|
||||
addCommonArgs(args, env);
|
||||
|
||||
const res = await $`restic ${args}`.env(env).nothrow();
|
||||
const res = await safeSpawn({ command: "restic", args, env });
|
||||
await cleanupTemporaryKeys(config, env);
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
logger.error(`Restic snapshot deletion failed: ${res.stderr}`);
|
||||
throw new ResticError(res.exitCode, res.stderr.toString());
|
||||
throw new ResticError(res.exitCode, res.stderr);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
|
|
@ -600,7 +614,7 @@ const ls = async (config: RepositoryConfig, snapshotId: string, path?: string) =
|
|||
|
||||
if (res.exitCode !== 0) {
|
||||
logger.error(`Restic ls failed: ${res.stderr}`);
|
||||
throw new ResticError(res.exitCode, res.stderr.toString());
|
||||
throw new ResticError(res.exitCode, res.stderr);
|
||||
}
|
||||
|
||||
// The output is a stream of JSON objects, first is snapshot info, rest are file/dir nodes
|
||||
|
|
@ -646,12 +660,12 @@ const unlock = async (config: RepositoryConfig) => {
|
|||
const args = ["unlock", "--repo", repoUrl, "--remove-all"];
|
||||
addCommonArgs(args, env);
|
||||
|
||||
const res = await $`restic ${args}`.env(env).nothrow();
|
||||
const res = await safeSpawn({ command: "restic", args, env });
|
||||
await cleanupTemporaryKeys(config, env);
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
logger.error(`Restic unlock failed: ${res.stderr}`);
|
||||
throw new ResticError(res.exitCode, res.stderr.toString());
|
||||
throw new ResticError(res.exitCode, res.stderr);
|
||||
}
|
||||
|
||||
logger.info(`Restic unlock succeeded for repository: ${repoUrl}`);
|
||||
|
|
@ -670,11 +684,10 @@ const check = async (config: RepositoryConfig, options?: { readData?: boolean })
|
|||
|
||||
addCommonArgs(args, env);
|
||||
|
||||
const res = await $`restic ${args}`.env(env).nothrow();
|
||||
const res = await safeSpawn({ command: "restic", args, env });
|
||||
await cleanupTemporaryKeys(config, env);
|
||||
|
||||
const stdout = res.text();
|
||||
const stderr = res.stderr.toString();
|
||||
const { stdout, stderr } = res;
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
logger.error(`Restic check failed: ${stderr}`);
|
||||
|
|
@ -704,11 +717,10 @@ const repairIndex = async (config: RepositoryConfig) => {
|
|||
const args = ["repair", "index", "--repo", repoUrl];
|
||||
addCommonArgs(args, env);
|
||||
|
||||
const res = await $`restic ${args}`.env(env).nothrow();
|
||||
const res = await safeSpawn({ command: "restic", args, env });
|
||||
await cleanupTemporaryKeys(config, env);
|
||||
|
||||
const stdout = res.text();
|
||||
const stderr = res.stderr.toString();
|
||||
const { stdout, stderr } = res;
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
logger.error(`Restic repair index failed: ${stderr}`);
|
||||
|
|
@ -764,13 +776,12 @@ const copy = async (
|
|||
logger.info(`Copying snapshots from ${sourceRepoUrl} to ${destRepoUrl}...`);
|
||||
logger.debug(`Executing: restic ${args.join(" ")}`);
|
||||
|
||||
const res = await $`restic ${args}`.env(env).nothrow();
|
||||
const res = await safeSpawn({ command: "restic", args, env });
|
||||
|
||||
await cleanupTemporaryKeys(sourceConfig, sourceEnv);
|
||||
await cleanupTemporaryKeys(destConfig, destEnv);
|
||||
|
||||
const stdout = res.text();
|
||||
const stderr = res.stderr.toString();
|
||||
const { stdout, stderr } = res;
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
logger.error(`Restic copy failed: ${stderr}`);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { spawn } from "node:child_process";
|
||||
|
||||
interface Params {
|
||||
export interface SafeSpawnParams {
|
||||
command: string;
|
||||
args: string[];
|
||||
env?: NodeJS.ProcessEnv;
|
||||
|
|
@ -18,7 +18,7 @@ type SpawnResult = {
|
|||
stderr: string;
|
||||
};
|
||||
|
||||
export const safeSpawn = (params: Params) => {
|
||||
export const safeSpawn = (params: SafeSpawnParams) => {
|
||||
const { command, args, env = {}, signal, ...callbacks } = params;
|
||||
|
||||
return new Promise<SpawnResult>((resolve) => {
|
||||
|
|
|
|||
24
app/test/helpers/auth.ts
Normal file
24
app/test/helpers/auth.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { authService } from "~/server/modules/auth/auth.service";
|
||||
import { db } from "~/server/db/db";
|
||||
import { usersTable, sessionsTable } from "~/server/db/schema";
|
||||
|
||||
export async function createTestSession() {
|
||||
const [existingUser] = await db.select().from(usersTable);
|
||||
|
||||
if (!existingUser) {
|
||||
await authService.register("testadmin", "testpassword");
|
||||
}
|
||||
|
||||
const [user] = await db.select().from(usersTable);
|
||||
|
||||
const sessionId = crypto.randomUUID();
|
||||
const expiresAt = Date.now() + 1000 * 60 * 60 * 24; // 24 hours
|
||||
|
||||
await db.insert(sessionsTable).values({
|
||||
id: sessionId,
|
||||
userId: user.id,
|
||||
expiresAt,
|
||||
});
|
||||
|
||||
return { sessionId, user };
|
||||
}
|
||||
16
app/test/helpers/backup.ts
Normal file
16
app/test/helpers/backup.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { db } from "~/server/db/db";
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { backupSchedulesTable, type BackupScheduleInsert } from "~/server/db/schema";
|
||||
|
||||
export const createTestBackupSchedule = async (overrides: Partial<BackupScheduleInsert> = {}) => {
|
||||
const backup: BackupScheduleInsert = {
|
||||
name: faker.system.fileName(),
|
||||
cronExpression: "0 0 * * *",
|
||||
repositoryId: "repo_123",
|
||||
volumeId: 1,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
const data = await db.insert(backupSchedulesTable).values(backup).returning();
|
||||
return data[0];
|
||||
};
|
||||
20
app/test/helpers/repository.ts
Normal file
20
app/test/helpers/repository.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { db } from "~/server/db/db";
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { repositoriesTable, type RepositoryInsert } from "~/server/db/schema";
|
||||
|
||||
export const createTestRepository = async (overrides: Partial<RepositoryInsert> = {}) => {
|
||||
const repository: RepositoryInsert = {
|
||||
id: faker.string.alphanumeric(6),
|
||||
name: faker.string.alphanumeric(10),
|
||||
shortId: faker.string.alphanumeric(6),
|
||||
config: {
|
||||
name: "test-repo",
|
||||
backend: "local",
|
||||
},
|
||||
type: "local",
|
||||
...overrides,
|
||||
};
|
||||
|
||||
const data = await db.insert(repositoriesTable).values(repository).returning();
|
||||
return data[0];
|
||||
};
|
||||
18
app/test/helpers/restic.ts
Normal file
18
app/test/helpers/restic.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
export const generateBackupOutput = () => {
|
||||
return JSON.stringify({
|
||||
message_type: "summary",
|
||||
files_new: 10,
|
||||
files_changed: 5,
|
||||
files_unmodified: 85,
|
||||
dirs_new: 2,
|
||||
dirs_changed: 1,
|
||||
dirs_unmodified: 17,
|
||||
data_blobs: 20,
|
||||
tree_blobs: 5,
|
||||
data_added: 1048576,
|
||||
total_files_processed: 100,
|
||||
total_bytes_processed: 2097152,
|
||||
total_duration: 12.34,
|
||||
snapshot_id: "abcd1234",
|
||||
});
|
||||
};
|
||||
21
app/test/helpers/volume.ts
Normal file
21
app/test/helpers/volume.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { db } from "~/server/db/db";
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { volumesTable, type VolumeInsert } from "~/server/db/schema";
|
||||
|
||||
export const createTestVolume = async (overrides: Partial<VolumeInsert> = {}) => {
|
||||
const volume: VolumeInsert = {
|
||||
name: faker.system.fileName(),
|
||||
config: {
|
||||
backend: "directory",
|
||||
path: `/mnt/volumes/${faker.system.fileName()}`,
|
||||
},
|
||||
status: "mounted",
|
||||
autoRemount: true,
|
||||
shortId: faker.string.alphanumeric(6),
|
||||
type: "directory",
|
||||
...overrides,
|
||||
};
|
||||
|
||||
const data = await db.insert(volumesTable).values(volume).returning();
|
||||
return data[0];
|
||||
};
|
||||
19
app/test/setup.ts
Normal file
19
app/test/setup.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { beforeAll, mock } from "bun:test";
|
||||
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
||||
import path from "node:path";
|
||||
import { cwd } from "node:process";
|
||||
import { db } from "~/server/db/db";
|
||||
|
||||
mock.module("~/server/utils/logger", () => ({
|
||||
logger: {
|
||||
debug: () => {},
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
},
|
||||
}));
|
||||
|
||||
beforeAll(async () => {
|
||||
const migrationsFolder = path.join(cwd(), "app", "drizzle");
|
||||
migrate(db, { migrationsFolder });
|
||||
});
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
import { intervalToDuration } from "date-fns";
|
||||
|
||||
export const getCronExpression = (frequency: string, dailyTime?: string, weeklyDay?: string): string => {
|
||||
export const getCronExpression = (
|
||||
frequency: string,
|
||||
dailyTime?: string,
|
||||
weeklyDay?: string,
|
||||
monthlyDays?: string[],
|
||||
): string => {
|
||||
if (frequency === "hourly") {
|
||||
return "0 * * * *";
|
||||
}
|
||||
|
|
@ -15,6 +20,15 @@ export const getCronExpression = (frequency: string, dailyTime?: string, weeklyD
|
|||
return `${minutes} ${hours} * * *`;
|
||||
}
|
||||
|
||||
if (frequency === "monthly") {
|
||||
const sortedDays = (monthlyDays || [])
|
||||
.map(Number)
|
||||
.filter((day) => day >= 1 && day <= 31)
|
||||
.sort((a, b) => a - b);
|
||||
const days = sortedDays.length > 0 ? sortedDays.join(",") : "1";
|
||||
return `${minutes} ${hours} ${days} * *`;
|
||||
}
|
||||
|
||||
return `${minutes} ${hours} * * ${weeklyDay ?? "0"}`;
|
||||
};
|
||||
|
||||
|
|
|
|||
401
bun.lock
401
bun.lock
|
|
@ -1,8 +1,9 @@
|
|||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "zerobyte",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
|
|
@ -36,6 +37,7 @@
|
|||
"es-toolkit": "^1.42.0",
|
||||
"hono": "4.10.5",
|
||||
"hono-openapi": "^1.1.1",
|
||||
"hono-rate-limiter": "^0.5.0",
|
||||
"http-errors-enhanced": "^4.0.2",
|
||||
"isbot": "^5.1.32",
|
||||
"lucide-react": "^0.555.0",
|
||||
|
|
@ -56,14 +58,16 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.8",
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@hey-api/openapi-ts": "^0.88.0",
|
||||
"@react-router/dev": "^7.10.0",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@tanstack/react-query-devtools": "^5.91.1",
|
||||
"@types/bun": "^1.3.3",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/bun": "^1.3.4",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"drizzle-kit": "^0.31.7",
|
||||
"lightningcss": "^1.30.2",
|
||||
"tailwindcss": "^4.1.17",
|
||||
|
|
@ -72,10 +76,13 @@
|
|||
"typescript": "^5.9.3",
|
||||
"vite": "^7.2.6",
|
||||
"vite-bundle-analyzer": "^1.2.3",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vite-tsconfig-paths": "^6.0.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild": "^0.27.2",
|
||||
},
|
||||
"packages": {
|
||||
"@ark/schema": ["@ark/schema@0.56.0", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA=="],
|
||||
|
||||
|
|
@ -137,23 +144,23 @@
|
|||
|
||||
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||
|
||||
"@biomejs/biome": ["@biomejs/biome@2.3.8", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.8", "@biomejs/cli-darwin-x64": "2.3.8", "@biomejs/cli-linux-arm64": "2.3.8", "@biomejs/cli-linux-arm64-musl": "2.3.8", "@biomejs/cli-linux-x64": "2.3.8", "@biomejs/cli-linux-x64-musl": "2.3.8", "@biomejs/cli-win32-arm64": "2.3.8", "@biomejs/cli-win32-x64": "2.3.8" }, "bin": { "biome": "bin/biome" } }, "sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA=="],
|
||||
"@biomejs/biome": ["@biomejs/biome@2.3.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.10", "@biomejs/cli-darwin-x64": "2.3.10", "@biomejs/cli-linux-arm64": "2.3.10", "@biomejs/cli-linux-arm64-musl": "2.3.10", "@biomejs/cli-linux-x64": "2.3.10", "@biomejs/cli-linux-x64-musl": "2.3.10", "@biomejs/cli-win32-arm64": "2.3.10", "@biomejs/cli-win32-x64": "2.3.10" }, "bin": { "biome": "bin/biome" } }, "sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HM4Zg9CGQ3txTPflxD19n8MFPrmUAjaC7PQdLkugeeC0cQ+PiVrd7i09gaBS/11QKsTDBJhVg85CEIK9f50Qww=="],
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w=="],
|
||||
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-lUDQ03D7y/qEao7RgdjWVGCu+BLYadhKTm40HkpJIi6kn8LSv5PAwRlew/DmwP4YZ9ke9XXoTIQDO1vAnbRZlA=="],
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-Uo1OJnIkJgSgF+USx970fsM/drtPcQ39I+JO+Fjsaa9ZdCN1oysQmy6oAGbyESlouz+rzEckLTF6DS7cWse95g=="],
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-PShR4mM0sjksUMyxbyPNMxoKFPVF48fU8Qe8Sfx6w6F42verbwRLbz+QiKNiDPRJwUoMG1nPM50OBL3aOnTevA=="],
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A=="],
|
||||
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-QDPMD5bQz6qOVb3kiBui0zKZXASLo0NIQ9JVJio5RveBEFgDgsvJFUvZIbMbUZT3T00M/1wdzwWXk4GIh0KaAw=="],
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw=="],
|
||||
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-YGLkqU91r1276uwSjiUD/xaVikdxgV1QpsicT0bIA1TaieM6E5ibMZeSyjQ/izBn4tKQthUSsVZacmoJfa3pDA=="],
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-H4IoCHvL1fXKDrTALeTKMiE7GGWFAraDwBYFquE/L/5r1927Te0mYIGseXi4F+lrrwhSWbSGt5qPFswNoBaCxg=="],
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ=="],
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.8", "", { "os": "win32", "cpu": "x64" }, "sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w=="],
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.10", "", { "os": "win32", "cpu": "x64" }, "sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ=="],
|
||||
|
||||
"@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="],
|
||||
|
||||
|
|
@ -173,57 +180,59 @@
|
|||
|
||||
"@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
||||
|
||||
"@faker-js/faker": ["@faker-js/faker@10.1.0", "", {}, "sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
|
||||
|
||||
|
|
@ -235,15 +244,15 @@
|
|||
|
||||
"@hey-api/codegen-core": ["@hey-api/codegen-core@0.3.3", "", { "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-vArVDtrvdzFewu1hnjUm4jX1NBITlSCeO81EdWq676MxQbyxsGcDPAgohaSA+Wvr4HjPSvsg2/1s2zYxUtXebg=="],
|
||||
|
||||
"@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.2.1", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0", "lodash": "^4.17.21" } }, "sha512-inPeksRLq+j3ArnuGOzQPQE//YrhezQG0+9Y9yizScBN2qatJ78fIByhEgKdNAbtguDCn4RPxmEhcrePwHxs4A=="],
|
||||
"@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.2.2", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.1", "lodash": "^4.17.21" } }, "sha512-oS+5yAdwnK20lSeFO1d53Ku+yaGCsY8PcrmSq2GtSs3bsBfRnHAbpPKSVzQcaxAOrzj5NB+f34WhZglVrNayBA=="],
|
||||
|
||||
"@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.88.0", "", { "dependencies": { "@hey-api/codegen-core": "^0.3.3", "@hey-api/json-schema-ref-parser": "1.2.1", "ansi-colors": "4.1.3", "c12": "3.3.2", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.2" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-ZrvmDfmVf+N4ry786LAhS/DoH+xkIjIJIeDnj2aL1qnMTIDsdRIXXvr80EnAZVBgunzu1wTBbHb3H9OfyDQ2EQ=="],
|
||||
"@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.88.2", "", { "dependencies": { "@hey-api/codegen-core": "^0.3.3", "@hey-api/json-schema-ref-parser": "1.2.2", "ansi-colors": "4.1.3", "c12": "3.3.2", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.2" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-JMae1RJ8S8D8lVCFQnsrWaKY0CxrR4TUmZIoU1SuvaeZ+pkMPYuQoj8fO37R5d8juUfFVzCGYV5/Ro0vbDqBbA=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.6", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw=="],
|
||||
"@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="],
|
||||
|
||||
"@hono/node-ws": ["@hono/node-ws@1.2.0", "", { "dependencies": { "ws": "^8.17.0" }, "peerDependencies": { "@hono/node-server": "^1.11.1", "hono": "^4.6.0" } }, "sha512-OBPQ8OSHBw29mj00wT/xGYtB6HY54j0fNSdVZ7gZM3TUeq0So11GXaWtFf1xWxQNfumKIsj0wRuLKWfVsO5GgQ=="],
|
||||
|
||||
"@hono/standard-validator": ["@hono/standard-validator@0.2.0", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-pFq0UVAnjzXcDAgqFpDeVL3MOUPrlIh/kPqBDvbCYoThVhhS+Vf37VcdsakdOFFGiqoiYVxp3LifXFhGhp/rgQ=="],
|
||||
"@hono/standard-validator": ["@hono/standard-validator@0.2.1", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-uF1W7/iSWi0r5Mugj5ZzdlCVp/KeGatS6JYt+Eht9xKO0IDtdZbdaLApgjrzHINQQa+Wnxw2pcX6EfO+vgB+Wg=="],
|
||||
|
||||
"@hono/vite-dev-server": ["@hono/vite-dev-server@0.23.0", "", { "dependencies": { "@hono/node-server": "^1.14.2", "minimatch": "^9.0.3" }, "peerDependencies": { "hono": "*", "miniflare": "*", "wrangler": "*" }, "optionalPeers": ["miniflare", "wrangler"] }, "sha512-tHV86xToed9Up0j/dubQW2PDP4aYNFDSfQrjcV6Ra7bqCGrxhtg/zZBmbgSco3aTxKOEPzDXKK+6seAAfsbIXw=="],
|
||||
|
||||
|
|
@ -339,117 +348,119 @@
|
|||
|
||||
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
||||
|
||||
"@react-router/dev": ["@react-router/dev@7.10.0", "", { "dependencies": { "@babel/core": "^7.27.7", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@react-router/node": "7.10.0", "@remix-run/node-fetch-server": "^0.9.0", "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chokidar": "^4.0.0", "dedent": "^1.5.3", "es-module-lexer": "^1.3.1", "exit-hook": "2.2.1", "isbot": "^5.1.11", "jsesc": "3.0.2", "lodash": "^4.17.21", "p-map": "^7.0.3", "pathe": "^1.1.2", "picocolors": "^1.1.1", "pkg-types": "^2.3.0", "prettier": "^3.6.2", "react-refresh": "^0.14.0", "semver": "^7.3.7", "tinyglobby": "^0.2.14", "valibot": "^1.1.0", "vite-node": "^3.2.2" }, "peerDependencies": { "@react-router/serve": "^7.10.0", "@vitejs/plugin-rsc": "*", "react-router": "^7.10.0", "typescript": "^5.1.0", "vite": "^5.1.0 || ^6.0.0 || ^7.0.0", "wrangler": "^3.28.2 || ^4.0.0" }, "optionalPeers": ["@react-router/serve", "@vitejs/plugin-rsc", "typescript", "wrangler"], "bin": { "react-router": "bin.js" } }, "sha512-3UgkV0N5lp3+Ol3q64L4ZHgPXv2XA4KHJ59MVLSK2prokrOrPaYvqbdx40r602M+hRZp/u04ln2A6cOfBW6kxA=="],
|
||||
"@react-router/dev": ["@react-router/dev@7.11.0", "", { "dependencies": { "@babel/core": "^7.27.7", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@react-router/node": "7.11.0", "@remix-run/node-fetch-server": "^0.9.0", "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chokidar": "^4.0.0", "dedent": "^1.5.3", "es-module-lexer": "^1.3.1", "exit-hook": "2.2.1", "isbot": "^5.1.11", "jsesc": "3.0.2", "lodash": "^4.17.21", "p-map": "^7.0.3", "pathe": "^1.1.2", "picocolors": "^1.1.1", "pkg-types": "^2.3.0", "prettier": "^3.6.2", "react-refresh": "^0.14.0", "semver": "^7.3.7", "tinyglobby": "^0.2.14", "valibot": "^1.2.0", "vite-node": "^3.2.2" }, "peerDependencies": { "@react-router/serve": "^7.11.0", "@vitejs/plugin-rsc": "~0.5.7", "react-router": "^7.11.0", "react-server-dom-webpack": "^19.2.3", "typescript": "^5.1.0", "vite": "^5.1.0 || ^6.0.0 || ^7.0.0", "wrangler": "^3.28.2 || ^4.0.0" }, "optionalPeers": ["@react-router/serve", "@vitejs/plugin-rsc", "react-server-dom-webpack", "typescript", "wrangler"], "bin": { "react-router": "bin.js" } }, "sha512-g1ou5Zw3r4mCU0L+EXH4vRtAiyt8qz1JOvL1k+PW4rZ4+71h5nBy/fLgD7cg5BnzQZmjRO1PzCgpF5BIrlKYxQ=="],
|
||||
|
||||
"@react-router/express": ["@react-router/express@7.10.0", "", { "dependencies": { "@react-router/node": "7.10.0" }, "peerDependencies": { "express": "^4.17.1 || ^5", "react-router": "7.10.0", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-3cBJ2cyHn5J+wSNFn+XdNSpXVAlQ+nbj7CMa3OsiEpFb+d0GLthirvSESqRjX2Eid94xNHICqKpYS9bR4QqIxg=="],
|
||||
"@react-router/express": ["@react-router/express@7.11.0", "", { "dependencies": { "@react-router/node": "7.11.0" }, "peerDependencies": { "express": "^4.17.1 || ^5", "react-router": "7.11.0", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-o5DeO9tqUrZcUWAgmPGgK4I/S6iFpqnj/e20xMGA04trk+90b9KAx9eqmRMgHERubVKANTM9gTDPduobQjeH1A=="],
|
||||
|
||||
"@react-router/node": ["@react-router/node@7.10.0", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0" }, "peerDependencies": { "react-router": "7.10.0", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-pff3Xz3gASrIUUX54QdlPzasdN9XRLnzoFEwUVsH5y2sZ6vijQdjZExLS6aQhPiuUr/uVPwN2WngO0Ryfrxulg=="],
|
||||
"@react-router/node": ["@react-router/node@7.11.0", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0" }, "peerDependencies": { "react-router": "7.11.0", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-11ha8EW+F7wTMmPz2pdi11LJxz2irtuksiCpunpZjtpPmYU37S+GGihG8vFeTa2xFPNunEaHNlfzKyzeYm570Q=="],
|
||||
|
||||
"@react-router/serve": ["@react-router/serve@7.10.0", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0", "@react-router/express": "7.10.0", "@react-router/node": "7.10.0", "compression": "^1.7.4", "express": "^4.19.2", "get-port": "5.1.1", "morgan": "^1.10.0", "source-map-support": "^0.5.21" }, "peerDependencies": { "react-router": "7.10.0" }, "bin": { "react-router-serve": "bin.js" } }, "sha512-tgdbw1lmDkzF3gCMj//iNklgUrYHUxz35rj0sbyLeti8K2gVsNxaZWyt5omanFgkeZ7WYfi0wzLHviqxl228eA=="],
|
||||
"@react-router/serve": ["@react-router/serve@7.11.0", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0", "@react-router/express": "7.11.0", "@react-router/node": "7.11.0", "compression": "^1.8.1", "express": "^4.19.2", "get-port": "5.1.1", "morgan": "^1.10.1", "source-map-support": "^0.5.21" }, "peerDependencies": { "react-router": "7.11.0" }, "bin": { "react-router-serve": "bin.js" } }, "sha512-U5Ht9PmUYF4Ti1ssaWlddLY4ZCbXBtHDGFU/u1h3VsHqleSdHsFuGAFrr/ZEuqTuEWp1CLqn2npEDAmlV9IUKQ=="],
|
||||
|
||||
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.10.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^10.2.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA=="],
|
||||
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="],
|
||||
|
||||
"@remix-run/node-fetch-server": ["@remix-run/node-fetch-server@0.9.0", "", {}, "sha512-SoLMv7dbH+njWzXnOY6fI08dFMI5+/dQ+vY3n8RnnbdG7MdJEgiP28Xj/xWlnRnED/aB6SFw56Zop+LbmaaKqA=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.2", "", { "os": "android", "cpu": "arm" }, "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA=="],
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.5", "", { "os": "android", "cpu": "arm" }, "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.2", "", { "os": "android", "cpu": "arm64" }, "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g=="],
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.5", "", { "os": "android", "cpu": "arm64" }, "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.53.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ=="],
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.53.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.53.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw=="],
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.53.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.53.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA=="],
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.53.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.53.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA=="],
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.53.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.53.2", "", { "os": "linux", "cpu": "arm" }, "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg=="],
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.53.5", "", { "os": "linux", "cpu": "arm" }, "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.53.2", "", { "os": "linux", "cpu": "arm" }, "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q=="],
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.53.5", "", { "os": "linux", "cpu": "arm" }, "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.53.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA=="],
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.53.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.53.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ=="],
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.53.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.53.2", "", { "os": "linux", "cpu": "none" }, "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ=="],
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.53.5", "", { "os": "linux", "cpu": "none" }, "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.53.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g=="],
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.53.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.53.2", "", { "os": "linux", "cpu": "none" }, "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA=="],
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.53.5", "", { "os": "linux", "cpu": "none" }, "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.53.2", "", { "os": "linux", "cpu": "none" }, "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ=="],
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.53.5", "", { "os": "linux", "cpu": "none" }, "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.53.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w=="],
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.53.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.2", "", { "os": "linux", "cpu": "x64" }, "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw=="],
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA=="],
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.5", "", { "os": "linux", "cpu": "x64" }, "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.53.2", "", { "os": "none", "cpu": "arm64" }, "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A=="],
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.53.5", "", { "os": "none", "cpu": "arm64" }, "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.53.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA=="],
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.53.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.53.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg=="],
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.53.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.53.2", "", { "os": "win32", "cpu": "x64" }, "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw=="],
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.53.5", "", { "os": "win32", "cpu": "x64" }, "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.2", "", { "os": "win32", "cpu": "x64" }, "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA=="],
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.5", "", { "os": "win32", "cpu": "x64" }, "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ=="],
|
||||
|
||||
"@scalar/core": ["@scalar/core@0.3.23", "", { "dependencies": { "@scalar/types": "0.5.0" } }, "sha512-hop7LVR3MKB2VpS8dly3gmmbB3lBGxQRtL0pBaC77zFMRHoBv1DuB2bj8l4gxd5grzitJ1LsYduvywLAMY9F6g=="],
|
||||
"@scalar/core": ["@scalar/core@0.3.28", "", { "dependencies": { "@scalar/types": "0.5.4" } }, "sha512-Ka+g5P3Fe4f9lsJcBxfI+XAgwMYeZRgzIBWw1/HBrDoRmH3rV/N//410MBKEYXUw7pWpS+dZPJANZRvU5jtxhw=="],
|
||||
|
||||
"@scalar/hono-api-reference": ["@scalar/hono-api-reference@0.9.25", "", { "dependencies": { "@scalar/core": "0.3.23" }, "peerDependencies": { "hono": "^4.10.3" } }, "sha512-ZEQAhvVU/FXdJs8+rVXdfWjwzkE+M6Zr+4W+zNhy8DF17BIpxFXfVL7i3OxK1V/4EtkTplkETjYGTR4ju3RFZw=="],
|
||||
"@scalar/helpers": ["@scalar/helpers@0.2.4", "", {}, "sha512-G7oGybO2QXM+MIxa4OZLXaYsS9mxKygFgOcY4UOXO6xpVoY5+8rahdak9cPk7HNj8RZSt4m/BveoT8g5BtnXxg=="],
|
||||
|
||||
"@scalar/types": ["@scalar/types@0.5.0", "", { "dependencies": { "nanoid": "5.1.5", "type-fest": "5.0.0", "zod": "4.1.11" } }, "sha512-imDMuTieOc5kHM9/Kt/1lmiI5ZtusuaYlzsXTP99IsWvD8mJ7ivF73lPBRj4PKtg4vY+ta5CO/vJpvnCYandRg=="],
|
||||
"@scalar/hono-api-reference": ["@scalar/hono-api-reference@0.9.30", "", { "dependencies": { "@scalar/core": "0.3.28" }, "peerDependencies": { "hono": "^4.10.3" } }, "sha512-a9cPluqfi1bgX2p7PJl/2O4jgPcoAl/ecSAe74TbPYIi27A0O0bkUBscO7WNRJhWJ1GVVxX8NvJTNlDxUNBlpg=="],
|
||||
|
||||
"@scalar/types": ["@scalar/types@0.5.4", "", { "dependencies": { "@scalar/helpers": "0.2.4", "nanoid": "5.1.5", "type-fest": "5.0.0", "zod": "^4.1.11" } }, "sha512-5FNQH/zx3tnERzxfpErscPHfRxLCuhncmhFYiaSz196Xi2iG1YI08BtxTV2slfT6of52epJ/MrKerarplKf9eg=="],
|
||||
|
||||
"@so-ric/colorspace": ["@so-ric/colorspace@1.1.6", "", { "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" } }, "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw=="],
|
||||
|
||||
"@standard-community/standard-json": ["@standard-community/standard-json@0.3.5", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "@types/json-schema": "^7.0.15", "@valibot/to-json-schema": "^1.3.0", "arktype": "^2.1.20", "effect": "^3.16.8", "quansync": "^0.2.11", "sury": "^10.0.0", "typebox": "^1.0.17", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.5" }, "optionalPeers": ["@valibot/to-json-schema", "arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-to-json-schema"] }, "sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA=="],
|
||||
|
||||
"@standard-community/standard-openapi": ["@standard-community/standard-openapi@0.2.8", "", { "peerDependencies": { "@standard-community/standard-json": "^0.3.5", "@standard-schema/spec": "^1.0.0", "arktype": "^2.1.20", "effect": "^3.17.14", "openapi-types": "^12.1.3", "sury": "^10.0.0", "typebox": "^1.0.0", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-openapi": "^4" }, "optionalPeers": ["arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-openapi"] }, "sha512-80ap74p5oy/SU4al5HkPwO5+NbN2wH/FBr6kwaE5ROq7AvcDFaxzUfTazewroNaCotbvdGcvzXb9oEoOIyfC/Q=="],
|
||||
"@standard-community/standard-openapi": ["@standard-community/standard-openapi@0.2.9", "", { "peerDependencies": { "@standard-community/standard-json": "^0.3.5", "@standard-schema/spec": "^1.0.0", "arktype": "^2.1.20", "effect": "^3.17.14", "openapi-types": "^12.1.3", "sury": "^10.0.0", "typebox": "^1.0.0", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-openapi": "^4" }, "optionalPeers": ["arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-openapi"] }, "sha512-htj+yldvN1XncyZi4rehbf9kLbu8os2Ke/rfqoZHCMHuw34kiF3LP/yQPdA0tQ940y8nDq3Iou8R3wG+AGGyvg=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.17", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.17" } }, "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg=="],
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.17", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.17", "@tailwindcss/oxide-darwin-arm64": "4.1.17", "@tailwindcss/oxide-darwin-x64": "4.1.17", "@tailwindcss/oxide-freebsd-x64": "4.1.17", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", "@tailwindcss/oxide-linux-x64-musl": "4.1.17", "@tailwindcss/oxide-wasm32-wasi": "4.1.17", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" } }, "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA=="],
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.17", "", { "os": "android", "cpu": "arm64" }, "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ=="],
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg=="],
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog=="],
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g=="],
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17", "", { "os": "linux", "cpu": "arm" }, "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ=="],
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ=="],
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg=="],
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.17", "", { "os": "linux", "cpu": "x64" }, "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ=="],
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.17", "", { "os": "linux", "cpu": "x64" }, "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ=="],
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.17", "", { "dependencies": { "@emnapi/core": "^1.6.0", "@emnapi/runtime": "^1.6.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.7", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A=="],
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.17", "", { "os": "win32", "cpu": "x64" }, "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw=="],
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="],
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.17", "", { "dependencies": { "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "tailwindcss": "4.1.17" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA=="],
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.90.11", "", {}, "sha512-f9z/nXhCgWDF4lHqgIE30jxLe4sYv15QodfdPDKYAk7nAEjNcndy4dHz3ezhdUaR23BpWa4I2EH4/DZ0//Uf8A=="],
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.90.12", "", {}, "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg=="],
|
||||
|
||||
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.91.1", "", {}, "sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg=="],
|
||||
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.90.11", "", { "dependencies": { "@tanstack/query-core": "5.90.11" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-3uyzz01D1fkTLXuxF3JfoJoHQMU2fxsfJwE+6N5hHy0dVNoZOvwKP8Z2k7k1KDeD54N20apcJnG75TBAStIrBA=="],
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.90.12", "", { "dependencies": { "@tanstack/query-core": "5.90.12" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg=="],
|
||||
|
||||
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.1", "", { "dependencies": { "@tanstack/query-devtools": "5.91.1" }, "peerDependencies": { "@tanstack/react-query": "^5.90.10", "react": "^18 || ^19" } }, "sha512-tRnJYwEbH0kAOuToy8Ew7bJw1lX3AjkkgSlf/vzb+NpnqmHPdWM+lA2DSdGQSLi1SU0PDRrrCI1vnZnci96CsQ=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
|
||||
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
|
||||
|
||||
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
||||
|
||||
|
|
@ -473,7 +484,7 @@
|
|||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
|
||||
|
||||
|
|
@ -487,37 +498,39 @@
|
|||
|
||||
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
|
||||
|
||||
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||
|
||||
"arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
|
||||
|
||||
"arkregex": ["arkregex@0.0.4", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-biS/FkvSwQq59TZ453piUp8bxMui11pgOMV9WHAnli1F8o0ayNCZzUwQadL/bGIUic5TkS/QlPcyMuI8ZIwedQ=="],
|
||||
"arkregex": ["arkregex@0.0.5", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw=="],
|
||||
|
||||
"arktype": ["arktype@2.1.28", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.4" } }, "sha512-LVZqXl2zWRpNFnbITrtFmqeqNkPPo+KemuzbGSY6jvJwCb4v8NsDzrWOLHnQgWl26TkJeWWcUNUeBpq2Mst1/Q=="],
|
||||
"arktype": ["arktype@2.1.29", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.5" } }, "sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ=="],
|
||||
|
||||
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
|
||||
|
||||
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||
|
||||
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.10", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA=="],
|
||||
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.11", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-mwq3W3e/pKSI6TG8lXMiDWvEi1VXYlSBlJlB3l+I0bAb5u1RNUl88udos85eOPNK3m5EXK9uO7d2g08pesTySQ=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="],
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="],
|
||||
|
||||
"basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="],
|
||||
|
||||
"body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="],
|
||||
"body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="],
|
||||
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
|
||||
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
||||
|
||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||
|
||||
|
|
@ -531,7 +544,7 @@
|
|||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001754", "", {}, "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg=="],
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001761", "", {}, "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g=="],
|
||||
|
||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
|
|
@ -541,13 +554,13 @@
|
|||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"color": ["color@5.0.2", "", { "dependencies": { "color-convert": "^3.0.1", "color-string": "^2.0.0" } }, "sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA=="],
|
||||
"color": ["color@5.0.3", "", { "dependencies": { "color-convert": "^3.1.3", "color-string": "^2.1.3" } }, "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA=="],
|
||||
|
||||
"color-convert": ["color-convert@3.1.2", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg=="],
|
||||
"color-convert": ["color-convert@3.1.3", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg=="],
|
||||
|
||||
"color-name": ["color-name@2.1.0", "", {}, "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg=="],
|
||||
|
||||
"color-string": ["color-string@2.1.2", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA=="],
|
||||
"color-string": ["color-string@2.1.4", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg=="],
|
||||
|
||||
"color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="],
|
||||
|
||||
|
|
@ -567,12 +580,18 @@
|
|||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="],
|
||||
"cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="],
|
||||
|
||||
"cron-parser": ["cron-parser@5.4.0", "", { "dependencies": { "luxon": "^3.7.1" } }, "sha512-HxYB8vTvnQFx4dLsZpGRa0uHp6X3qIzS3ZJgJ9v6l/5TJMgeWQbLkR5yiJ5hOxGbc9+jCADDnydIe15ReLZnJA=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
|
||||
|
|
@ -603,11 +622,11 @@
|
|||
|
||||
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
|
||||
|
||||
"dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="],
|
||||
"dedent": ["dedent@1.7.1", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg=="],
|
||||
|
||||
"default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="],
|
||||
|
||||
"default-browser-id": ["default-browser-id@5.0.0", "", {}, "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA=="],
|
||||
"default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
|
||||
|
||||
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
|
||||
|
||||
|
|
@ -627,7 +646,11 @@
|
|||
|
||||
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
||||
|
||||
"drizzle-kit": ["drizzle-kit@0.31.7", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-hOzRGSdyKIU4FcTSFYGKdXEjFsncVwHZ43gY3WU5Bz9j5Iadp6Rh6hxLSQ1IWXpKLBKt/d5y1cpSPcV+FcoQ1A=="],
|
||||
"dotenv-cli": ["dotenv-cli@11.0.0", "", { "dependencies": { "cross-spawn": "^7.0.6", "dotenv": "^17.1.0", "dotenv-expand": "^12.0.0", "minimist": "^1.2.6" }, "bin": { "dotenv": "cli.js" } }, "sha512-r5pA8idbk7GFWuHEU7trSTflWcdBpQEK+Aw17UrSHjS6CReuhrrPcyC3zcQBPQvhArRHnBo/h6eLH1fkCvNlww=="],
|
||||
|
||||
"dotenv-expand": ["dotenv-expand@12.0.3", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA=="],
|
||||
|
||||
"drizzle-kit": ["drizzle-kit@0.31.8", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg=="],
|
||||
|
||||
"drizzle-orm": ["drizzle-orm@0.44.7", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="],
|
||||
|
||||
|
|
@ -635,13 +658,13 @@
|
|||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.250", "", {}, "sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw=="],
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="],
|
||||
|
||||
"enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
|
||||
"enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
|
|
@ -651,9 +674,9 @@
|
|||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"es-toolkit": ["es-toolkit@1.42.0", "", {}, "sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA=="],
|
||||
"es-toolkit": ["es-toolkit@1.43.0", "", {}, "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
"esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
|
||||
|
||||
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
|
||||
|
||||
|
|
@ -667,7 +690,7 @@
|
|||
|
||||
"exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="],
|
||||
|
||||
"express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="],
|
||||
"express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="],
|
||||
|
||||
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
|
||||
|
||||
|
|
@ -675,7 +698,7 @@
|
|||
|
||||
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
|
||||
|
||||
"finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="],
|
||||
"finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
|
||||
|
||||
"fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="],
|
||||
|
||||
|
|
@ -707,15 +730,19 @@
|
|||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"h3": ["h3@1.15.4", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.2", "radix3": "^1.1.2", "ufo": "^1.6.1", "uncrypto": "^0.1.3" } }, "sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hono": ["hono@4.10.5", "", {}, "sha512-h/MXuTkoAK8NG1EfDp0jI1YLf6yGdDnfkebRO2pwEh5+hE3RAJFXkCsnD0vamSiARK4ZrB6MY+o3E/hCnOyHrQ=="],
|
||||
|
||||
"hono-openapi": ["hono-openapi@1.1.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.1.2", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.8", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-AC3HNhZYPHhnZdSy2Je7GDoTTNxPos6rKRQKVDBbSilY3cWJPqsxRnN6zA4pU7tfxmQEMTqkiLXbw6sAaemB8Q=="],
|
||||
"hono-openapi": ["hono-openapi@1.1.2", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-toUcO60MftRBxqcVyxsHNYs2m4vf4xkQaiARAucQx3TiBPDtMNNkoh+C4I1vAretQZiGyaLOZNWn1YxfSyUA5g=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
||||
"hono-rate-limiter": ["hono-rate-limiter@0.5.1", "", { "peerDependencies": { "hono": "^4.10.8", "unstorage": "^1.17.3" } }, "sha512-c3bUn6IRgFKjlouvRNBy+ZIPZ2CTyTt3fc0uat2bv3GiHmLM4jI0QJ6fHd3Tf4R6dO2sX2Uvl9Gtp+kny4KdXg=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"http-errors-enhanced": ["http-errors-enhanced@4.0.2", "", {}, "sha512-5EXN1gmhJVvuWpNfz+RclWvLnnENEXNMPfww3gm30H9mQzPF4QSBj/MD5FRkVDxGIUhO/cR2GSLCd/6C6xpBcw=="],
|
||||
|
||||
|
|
@ -729,6 +756,8 @@
|
|||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
|
||||
|
||||
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||
|
||||
"is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="],
|
||||
|
|
@ -741,6 +770,8 @@
|
|||
|
||||
"isbot": ["isbot@5.1.32", "", {}, "sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
|
@ -781,7 +812,7 @@
|
|||
|
||||
"logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"lucide-react": ["lucide-react@0.555.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA=="],
|
||||
|
||||
|
|
@ -805,6 +836,8 @@
|
|||
|
||||
"minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
"morgan": ["morgan@1.10.1", "", { "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", "on-headers": "~1.1.0" } }, "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
|
@ -819,12 +852,18 @@
|
|||
|
||||
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||
|
||||
"node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
"nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
||||
|
||||
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
|
@ -841,6 +880,8 @@
|
|||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
|
||||
|
||||
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||
|
|
@ -857,37 +898,39 @@
|
|||
|
||||
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
|
||||
|
||||
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
|
||||
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
||||
|
||||
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
|
||||
|
||||
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="],
|
||||
"raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
|
||||
|
||||
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
|
||||
|
||||
"react": ["react@19.2.1", "", {}, "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw=="],
|
||||
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="],
|
||||
"react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
|
||||
|
||||
"react-hook-form": ["react-hook-form@7.68.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q=="],
|
||||
|
||||
"react-is": ["react-is@19.2.0", "", {}, "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA=="],
|
||||
"react-is": ["react-is@19.2.3", "", {}, "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA=="],
|
||||
|
||||
"react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
|
||||
|
||||
"react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="],
|
||||
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
|
||||
|
||||
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||
|
||||
"react-router": ["react-router@7.10.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-FVyCOH4IZ0eDDRycODfUqoN8ZSR2LbTvtx6RPsBgzvJ8xAXlMZNCrOFpu+jb8QbtZnpAd/cEki2pwE848pNGxw=="],
|
||||
"react-router": ["react-router@7.11.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ=="],
|
||||
|
||||
"react-router-hono-server": ["react-router-hono-server@2.22.0", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@hono/node-server": "^1.19.5", "@hono/node-ws": "^1.2.0", "@hono/vite-dev-server": "^0.23.0", "hono": "^4.10.3" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250317.0", "@react-router/dev": "^7.9.0", "@types/react": "^19.0.0", "miniflare": "^3.20241205.0", "react-router": "^7.9.0", "vite": "^7.0.0", "wrangler": "^4.2.0" }, "optionalPeers": ["@cloudflare/workers-types", "miniflare", "wrangler"], "bin": { "react-router-hono-server": "dist/cli.js" } }, "sha512-XPJp1PQtkjHsFrneUdvmv22o7LPBGYEW11KtTDEmpXQINW5ViwpzUuwb0/eGjZ9E4RQh6AOKYsXA4+QIqXTkrQ=="],
|
||||
|
||||
|
|
@ -907,7 +950,7 @@
|
|||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"rollup": ["rollup@4.53.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.2", "@rollup/rollup-android-arm64": "4.53.2", "@rollup/rollup-darwin-arm64": "4.53.2", "@rollup/rollup-darwin-x64": "4.53.2", "@rollup/rollup-freebsd-arm64": "4.53.2", "@rollup/rollup-freebsd-x64": "4.53.2", "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", "@rollup/rollup-linux-arm-musleabihf": "4.53.2", "@rollup/rollup-linux-arm64-gnu": "4.53.2", "@rollup/rollup-linux-arm64-musl": "4.53.2", "@rollup/rollup-linux-loong64-gnu": "4.53.2", "@rollup/rollup-linux-ppc64-gnu": "4.53.2", "@rollup/rollup-linux-riscv64-gnu": "4.53.2", "@rollup/rollup-linux-riscv64-musl": "4.53.2", "@rollup/rollup-linux-s390x-gnu": "4.53.2", "@rollup/rollup-linux-x64-gnu": "4.53.2", "@rollup/rollup-linux-x64-musl": "4.53.2", "@rollup/rollup-openharmony-arm64": "4.53.2", "@rollup/rollup-win32-arm64-msvc": "4.53.2", "@rollup/rollup-win32-ia32-msvc": "4.53.2", "@rollup/rollup-win32-x64-gnu": "4.53.2", "@rollup/rollup-win32-x64-msvc": "4.53.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g=="],
|
||||
"rollup": ["rollup@4.53.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.5", "@rollup/rollup-android-arm64": "4.53.5", "@rollup/rollup-darwin-arm64": "4.53.5", "@rollup/rollup-darwin-x64": "4.53.5", "@rollup/rollup-freebsd-arm64": "4.53.5", "@rollup/rollup-freebsd-x64": "4.53.5", "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", "@rollup/rollup-linux-arm-musleabihf": "4.53.5", "@rollup/rollup-linux-arm64-gnu": "4.53.5", "@rollup/rollup-linux-arm64-musl": "4.53.5", "@rollup/rollup-linux-loong64-gnu": "4.53.5", "@rollup/rollup-linux-ppc64-gnu": "4.53.5", "@rollup/rollup-linux-riscv64-gnu": "4.53.5", "@rollup/rollup-linux-riscv64-musl": "4.53.5", "@rollup/rollup-linux-s390x-gnu": "4.53.5", "@rollup/rollup-linux-x64-gnu": "4.53.5", "@rollup/rollup-linux-x64-musl": "4.53.5", "@rollup/rollup-openharmony-arm64": "4.53.5", "@rollup/rollup-win32-arm64-msvc": "4.53.5", "@rollup/rollup-win32-ia32-msvc": "4.53.5", "@rollup/rollup-win32-x64-gnu": "4.53.5", "@rollup/rollup-win32-x64-msvc": "4.53.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ=="],
|
||||
|
||||
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
|
||||
|
||||
|
|
@ -921,14 +964,18 @@
|
|||
|
||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
|
||||
"send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="],
|
||||
|
||||
"serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="],
|
||||
"serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
|
||||
|
||||
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
|
|
@ -949,7 +996,7 @@
|
|||
|
||||
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
|
||||
|
||||
"statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
|
|
@ -957,7 +1004,7 @@
|
|||
|
||||
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="],
|
||||
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
||||
|
||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||
|
||||
|
|
@ -987,11 +1034,17 @@
|
|||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
|
||||
|
||||
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="],
|
||||
"unstorage": ["unstorage@1.17.3", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^4.0.3", "destr": "^2.0.5", "h3": "^1.15.4", "lru-cache": "^10.4.3", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.1" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
|
||||
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
||||
|
||||
|
|
@ -1003,21 +1056,23 @@
|
|||
|
||||
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
||||
|
||||
"valibot": ["valibot@1.1.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw=="],
|
||||
"valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
|
||||
|
||||
"vite": ["vite@7.2.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ=="],
|
||||
"vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="],
|
||||
|
||||
"vite-bundle-analyzer": ["vite-bundle-analyzer@1.2.3", "", { "bin": { "analyze": "dist/bin.js" } }, "sha512-8nhwDGHWMKKgg6oegAOpDgTT7/yzTVzeYzLF4y8WBJoYu9gO7h29UpHiQnXD2rAvfQzDy5Wqe/Za5cgqhnxi5g=="],
|
||||
"vite-bundle-analyzer": ["vite-bundle-analyzer@1.3.2", "", { "bin": { "analyze": "dist/bin.js" } }, "sha512-Od4ILUKRvBV3LuO/E+S+c1XULlxdkRZPSf6Vzzu+UAXG0D3hZYUu9imZIkSj/PU4e1FB14yB+av8g3KiljH8zQ=="],
|
||||
|
||||
"vite-node": ["vite-node@3.2.4", "", { "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" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
|
||||
|
||||
"vite-tsconfig-paths": ["vite-tsconfig-paths@5.1.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w=="],
|
||||
"vite-tsconfig-paths": ["vite-tsconfig-paths@6.0.3", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-7bL7FPX/DSviaZGYUKowWF1AiDVWjMjxNbE8lyaVGDezkedWqfGhlnQ4BZXre0ZN5P4kAgIJfAlgFDVyjrCIyg=="],
|
||||
|
||||
"winston": ["winston@3.18.3", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww=="],
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"winston": ["winston@3.19.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA=="],
|
||||
|
||||
"winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="],
|
||||
|
||||
|
|
@ -1029,16 +1084,16 @@
|
|||
|
||||
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
|
||||
|
||||
"zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
|
||||
"zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="],
|
||||
|
||||
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||
|
||||
"@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
|
@ -1059,17 +1114,17 @@
|
|||
|
||||
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@react-router/dev/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
"@reduxjs/toolkit/immer": ["immer@11.0.1", "", {}, "sha512-naDCyggtcBWANtIrjQEajhhBEuL9b0Zg4zmlWK2CzS6xCWSE39/vvf4LqnMjUAWHBhot4m9MHCM/Z+mfWhUkiA=="],
|
||||
|
||||
"@scalar/types/nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
|
|
@ -1077,15 +1132,21 @@
|
|||
|
||||
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"basic-auth/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||
|
||||
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"bun-types/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="],
|
||||
|
||||
"c12/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
|
||||
"dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
||||
|
||||
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
|
|
@ -1107,56 +1168,8 @@
|
|||
|
||||
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
||||
|
||||
"vite-node/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"vite-node/vite": ["vite@7.2.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
||||
|
||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
|
|
|||
|
|
@ -12,19 +12,13 @@ services:
|
|||
- SYS_ADMIN
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
# - SMB_PASSWORD=secret
|
||||
ports:
|
||||
- "4096:4096"
|
||||
# secrets:
|
||||
# - smb-password
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /var/lib/zerobyte:/var/lib/zerobyte
|
||||
|
||||
- ./app:/app/app
|
||||
- ~/.config/rclone:/root/.config/rclone
|
||||
# - /run/docker/plugins:/run/docker/plugins
|
||||
# - /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
zerobyte-prod:
|
||||
build:
|
||||
|
|
@ -41,11 +35,5 @@ services:
|
|||
- "4096:4096"
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /var/lib/zerobyte:/var/lib/zerobyte:rshared
|
||||
- /run/docker/plugins:/run/docker/plugins
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /var/lib/zerobyte:/var/lib/zerobyte
|
||||
- ~/.config/rclone:/root/.config/rclone
|
||||
|
||||
# secrets:
|
||||
# smb-password:
|
||||
# file: ./smb-password.txt
|
||||
|
|
|
|||
17
examples/README.md
Normal file
17
examples/README.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Examples
|
||||
|
||||
This folder contains runnable, copy/paste-friendly examples for running Zerobyte in different setups.
|
||||
|
||||
## Table of contents
|
||||
|
||||
### Basic usage
|
||||
|
||||
- [Basic Docker Compose](basic-docker-compose/README.md) — standard deployment with remote mount support (includes `SYS_ADMIN` + `/dev/fuse`).
|
||||
- [Simplified Docker Compose (no remote mounts)](simplified-docker-compose/README.md) — reduced-privilege deployment (no `SYS_ADMIN`, no `/dev/fuse`).
|
||||
- [Bind-mount a local directory](directory-bind-mount/README.md) — back up a host folder by mounting it into the container.
|
||||
- [Mount an rclone config](rclone-config-mount/README.md) — use rclone-based repository backends by mounting your rclone config.
|
||||
- [Secret placeholders + Docker secrets](secrets-placeholders/README.md) — keep secrets out of the DB using `env://...` and `file://...` references.
|
||||
|
||||
### Advanced setups
|
||||
|
||||
- [Tailscale sidecar](tailscale-sidecar/README.md) — run Zerobyte behind a Tailscale sidecar using shared networking.
|
||||
2
examples/basic-docker-compose/.env.example
Normal file
2
examples/basic-docker-compose/.env.example
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Timezone used by the container
|
||||
TZ=UTC
|
||||
25
examples/basic-docker-compose/README.md
Normal file
25
examples/basic-docker-compose/README.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Basic Docker Compose
|
||||
|
||||
Minimal "standard" deployment for Zerobyte.
|
||||
|
||||
This setup enables remote mount backends (NFS/SMB/WebDAV) from inside the container by granting the required capability and FUSE device.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker + Docker Compose
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Access
|
||||
|
||||
- UI/API: `http://<host>:4096`
|
||||
|
||||
## Notes
|
||||
|
||||
- This example uses `cap_add: SYS_ADMIN` and `/dev/fuse` to support mounting remote volumes.
|
||||
- Do not place `/var/lib/zerobyte` on a network share.
|
||||
16
examples/basic-docker-compose/docker-compose.yml
Normal file
16
examples/basic-docker-compose/docker-compose.yml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
services:
|
||||
zerobyte:
|
||||
image: ghcr.io/nicotsx/zerobyte:latest
|
||||
container_name: zerobyte
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
- SYS_ADMIN
|
||||
devices:
|
||||
- /dev/fuse:/dev/fuse
|
||||
ports:
|
||||
- "4096:4096"
|
||||
environment:
|
||||
- TZ=${TZ:-UTC}
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /var/lib/zerobyte:/var/lib/zerobyte
|
||||
8
examples/directory-bind-mount/.env.example
Normal file
8
examples/directory-bind-mount/.env.example
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Timezone used by the container
|
||||
TZ=UTC
|
||||
|
||||
# Absolute path on the Docker host to back up.
|
||||
# Examples:
|
||||
# - /srv/data
|
||||
# - /mnt/storage/photos
|
||||
HOST_DATA_DIR=/path/to/your/directory
|
||||
34
examples/directory-bind-mount/README.md
Normal file
34
examples/directory-bind-mount/README.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Bind-mount a local directory
|
||||
|
||||
This example shows how to back up a host directory by bind-mounting it into the Zerobyte container.
|
||||
|
||||
It uses the simplified setup (no remote mounts).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker + Docker Compose
|
||||
|
||||
## Setup
|
||||
|
||||
1. Copy the env file:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. Edit `.env` and set `HOST_DATA_DIR` to the directory you want to back up.
|
||||
|
||||
3. Start the stack:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Use in Zerobyte
|
||||
|
||||
- Create a new volume of type **Directory**
|
||||
- Select the mounted path shown in the compose file: `/mydata`
|
||||
|
||||
## Access
|
||||
|
||||
- UI/API: `http://<host>:4096`
|
||||
13
examples/directory-bind-mount/docker-compose.yml
Normal file
13
examples/directory-bind-mount/docker-compose.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
services:
|
||||
zerobyte:
|
||||
image: ghcr.io/nicotsx/zerobyte:latest
|
||||
container_name: zerobyte
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4096:4096"
|
||||
environment:
|
||||
- TZ=${TZ:-UTC}
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /var/lib/zerobyte:/var/lib/zerobyte
|
||||
- ${HOST_DATA_DIR:?HOST_DATA_DIR must be set}:/mydata
|
||||
8
examples/rclone-config-mount/.env.example
Normal file
8
examples/rclone-config-mount/.env.example
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Timezone used by the container
|
||||
TZ=UTC
|
||||
|
||||
# Absolute path on the Docker host to your rclone config directory.
|
||||
# Common locations:
|
||||
# - Linux: /home/<user>/.config/rclone
|
||||
# - Linux (root): /root/.config/rclone
|
||||
RCLONE_CONFIG_DIR=/path/to/rclone/config
|
||||
41
examples/rclone-config-mount/README.md
Normal file
41
examples/rclone-config-mount/README.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Mount an rclone config (for rclone repositories)
|
||||
|
||||
This example shows how to make an existing rclone configuration available inside the Zerobyte container.
|
||||
|
||||
Use this if you want to use **rclone** as a repository backend (Dropbox/Google Drive/OneDrive/etc.).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker + Docker Compose
|
||||
- An existing rclone config directory on the Docker host
|
||||
|
||||
If you don't have one yet:
|
||||
|
||||
```bash
|
||||
rclone config
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
1. Copy the env file:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. Edit `.env` and set `RCLONE_CONFIG_DIR` to the absolute path of your host rclone config directory.
|
||||
|
||||
3. Start the stack:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Access
|
||||
|
||||
- UI/API: `http://<host>:4096`
|
||||
|
||||
## Notes
|
||||
|
||||
- This setup does not require `SYS_ADMIN` or `/dev/fuse`.
|
||||
- The rclone config is mounted read-only into `/root/.config/rclone`.
|
||||
13
examples/rclone-config-mount/docker-compose.yml
Normal file
13
examples/rclone-config-mount/docker-compose.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
services:
|
||||
zerobyte:
|
||||
image: ghcr.io/nicotsx/zerobyte:latest
|
||||
container_name: zerobyte
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4096:4096"
|
||||
environment:
|
||||
- TZ=${TZ:-UTC}
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /var/lib/zerobyte:/var/lib/zerobyte
|
||||
- ${RCLONE_CONFIG_DIR}:/root/.config/rclone:ro
|
||||
6
examples/secrets-placeholders/.env.example
Normal file
6
examples/secrets-placeholders/.env.example
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Timezone used by the container
|
||||
TZ=UTC
|
||||
|
||||
# Optional example secret injected via environment variable.
|
||||
# Use it in Zerobyte config fields as: env://ZEROBYTE_SMB_PASSWORD
|
||||
ZEROBYTE_SMB_PASSWORD=
|
||||
5
examples/secrets-placeholders/.gitignore
vendored
Normal file
5
examples/secrets-placeholders/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Secret files - do not commit
|
||||
smb-password.txt
|
||||
*-password.txt
|
||||
*.secret
|
||||
*.key
|
||||
61
examples/secrets-placeholders/README.md
Normal file
61
examples/secrets-placeholders/README.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Secret placeholders (env:// and file://) + Docker secrets
|
||||
|
||||
Zerobyte supports **secret placeholders** in many configuration fields (repositories, volumes, notifications).
|
||||
Instead of storing raw secrets in the database, you can store a reference that gets resolved at runtime.
|
||||
|
||||
Supported formats:
|
||||
|
||||
- `env://VAR_NAME` → reads `process.env.VAR_NAME`
|
||||
- `file://name` → reads `/run/secrets/name` (Docker Compose / Docker secrets)
|
||||
|
||||
This example shows how to run Zerobyte with:
|
||||
|
||||
- an environment variable you can reference via `env://...`
|
||||
- a Docker secret you can reference via `file://...`
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker + Docker Compose
|
||||
|
||||
This example includes `SYS_ADMIN` and `/dev/fuse` because it’s intended for SMB volumes (remote mounts).
|
||||
|
||||
## Setup
|
||||
|
||||
1. Copy the env file:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. Create a Docker secret file.
|
||||
|
||||
⚠️ **Important**: never commit real credentials. This folder includes a `.gitignore` to help prevent accidentally committing secret files.
|
||||
|
||||
```bash
|
||||
printf "your-smb-password" > smb-password.txt
|
||||
```
|
||||
|
||||
3. Start Zerobyte:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Using placeholders in Zerobyte
|
||||
|
||||
You can now use the placeholders for example in these Zerobyte configuration fields:
|
||||
|
||||
| UI section | Type | Field | Example value |
|
||||
| --- | --- | --- | --- |
|
||||
| Volumes → Create volume | SMB | Password | `file://smb_password` or `env://ZEROBYTE_SMB_PASSWORD` |
|
||||
| Volumes → Create volume | WebDAV | Password | `file://webdav_password` or `env://ZEROBYTE_WEBDAV_PASSWORD` |
|
||||
| Repositories → Create repository | S3 | Secret access key | `file://aws_secret_access_key` or `env://AWS_SECRET_ACCESS_KEY` |
|
||||
| Repositories → Create repository | SFTP | SSH Private key | `file://sftp_private_key` or `env://SFTP_PRIVATE_KEY` |
|
||||
| Notifications → Create notification | Telegram | Bot token | `file://telegram_bot_token` or `env://TELEGRAM_BOT_TOKEN` |
|
||||
|
||||
Notes:
|
||||
|
||||
- Placeholder names used in these examples are arbitrary; you can choose any valid name.
|
||||
- Placeholder names are case-sensitive.
|
||||
- With `file://...`, the secret name must be a single path segment (no `/` or `\\`).
|
||||
- You can still paste a raw secret, but placeholders can be considered safer and easier to rotate.
|
||||
26
examples/secrets-placeholders/docker-compose.yml
Normal file
26
examples/secrets-placeholders/docker-compose.yml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
services:
|
||||
zerobyte:
|
||||
image: ghcr.io/nicotsx/zerobyte:latest
|
||||
container_name: zerobyte
|
||||
restart: unless-stopped
|
||||
# Required for mounting remote volumes (SMB/NFS/WebDAV) from inside the container
|
||||
cap_add:
|
||||
- SYS_ADMIN
|
||||
devices:
|
||||
- /dev/fuse:/dev/fuse
|
||||
ports:
|
||||
- "4096:4096"
|
||||
environment:
|
||||
- TZ=${TZ:-UTC}
|
||||
# Example env-backed secret (refer to it in Zerobyte as env://ZEROBYTE_SMB_PASSWORD)
|
||||
- ZEROBYTE_SMB_PASSWORD=${ZEROBYTE_SMB_PASSWORD:-}
|
||||
secrets:
|
||||
# Example file-backed secret (refer to it in Zerobyte as file://smb_password)
|
||||
- smb_password
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /var/lib/zerobyte:/var/lib/zerobyte
|
||||
|
||||
secrets:
|
||||
smb_password:
|
||||
file: ./smb-password.txt
|
||||
2
examples/simplified-docker-compose/.env.example
Normal file
2
examples/simplified-docker-compose/.env.example
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Timezone used by the container
|
||||
TZ=UTC
|
||||
25
examples/simplified-docker-compose/README.md
Normal file
25
examples/simplified-docker-compose/README.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Simplified Docker Compose (no remote mounts)
|
||||
|
||||
A reduced-privilege setup for Zerobyte when you do **not** need to mount NFS/SMB/WebDAV from inside the container.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker + Docker Compose
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Access
|
||||
|
||||
- UI/API: `http://<host>:4096`
|
||||
|
||||
## Trade-offs
|
||||
|
||||
- ✅ No `SYS_ADMIN`
|
||||
- ✅ No `/dev/fuse`
|
||||
- ✅ Still supports all repository backends (local, S3, GCS, Azure, rclone)
|
||||
- ❌ Cannot mount remote shares from inside Zerobyte
|
||||
12
examples/simplified-docker-compose/docker-compose.yml
Normal file
12
examples/simplified-docker-compose/docker-compose.yml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
services:
|
||||
zerobyte:
|
||||
image: ghcr.io/nicotsx/zerobyte:latest
|
||||
container_name: zerobyte
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4096:4096"
|
||||
environment:
|
||||
- TZ=${TZ:-UTC}
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /var/lib/zerobyte:/var/lib/zerobyte
|
||||
22
examples/tailscale-sidecar/.env.example
Normal file
22
examples/tailscale-sidecar/.env.example
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# --- Zerobyte ---
|
||||
TZ=UTC
|
||||
|
||||
# --- Tailscale ---
|
||||
# Create an auth key in the Tailscale admin console.
|
||||
# Recommended: ephemeral + pre-approved (or tagged) key.
|
||||
TS_AUTHKEY=tskey-auth-xxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# How this node should appear in your tailnet.
|
||||
TS_HOSTNAME=zerobyte
|
||||
|
||||
# Kernel-mode (false) vs userspace-mode (true).
|
||||
# - false: requires /dev/net/tun on the host
|
||||
# - true: works on Docker Desktop / hosts without TUN, but you must also remove
|
||||
# the /dev/net/tun device mapping from docker-compose.yml
|
||||
TS_USERSPACE=false
|
||||
|
||||
# Optional extra args passed to `tailscale up`.
|
||||
# Examples:
|
||||
# TS_EXTRA_ARGS=--accept-dns=true
|
||||
# TS_EXTRA_ARGS=--advertise-tags=tag:backup --accept-routes
|
||||
TS_EXTRA_ARGS=
|
||||
93
examples/tailscale-sidecar/README.md
Normal file
93
examples/tailscale-sidecar/README.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# Zerobyte + Tailscale sidecar (Docker Compose)
|
||||
|
||||
This example runs Zerobyte behind a Tailscale sidecar container so the Zerobyte web UI/API can be reached over your tailnet and Zerobyte can access other devices on your tailnet (based on the ACLs/tags you configure in Tailscale).
|
||||
|
||||
It uses a common “sidecar networking” pattern:
|
||||
|
||||
- The `tailscale` container brings up a Tailscale node
|
||||
- The `zerobyte` container shares the `tailscale` network namespace (`network_mode: service:tailscale`)
|
||||
|
||||
## About Tailscale
|
||||
|
||||
Tailscale is a mesh VPN built on WireGuard. It connects devices and containers into a private network (“tailnet”) without opening inbound ports on your router or exposing services directly to the public internet.
|
||||
|
||||
In this example, Tailscale acts as a secure access layer in front of Zerobyte:
|
||||
|
||||
- You reach Zerobyte using the node’s tailnet IP/DNS name.
|
||||
- Access can be restricted using Tailscale ACLs/tags.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker + Docker Compose
|
||||
- A Tailscale account and an auth key
|
||||
|
||||
This example supports two Tailscale modes:
|
||||
|
||||
- **Kernel mode** (`TS_USERSPACE=false`, default):
|
||||
- requires `/dev/net/tun` on the host
|
||||
- requires `NET_ADMIN`
|
||||
- may require `SYS_MODULE` on some hosts (kept commented out in the compose file)
|
||||
|
||||
- **Userspace mode** (`TS_USERSPACE=true`):
|
||||
- does **not** require `/dev/net/tun`
|
||||
- works better on Docker Desktop / restricted hosts
|
||||
- requires a small edit in the compose file (see Troubleshooting)
|
||||
|
||||
## Setup
|
||||
|
||||
1. Copy the env file and fill in your auth key:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. Start the stack:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
3. In the Tailscale admin console, confirm the node is present (and approved if your policy requires it).
|
||||
|
||||
## Access
|
||||
|
||||
- Over Tailscale: `http://<tailscale-ip>:4096` or `http://<tailscale-name>:4096` (if MagicDNS is enabled)
|
||||
- Locally (optional): the example publishes `4096:4096` on the host
|
||||
|
||||
If you want Zerobyte to be reachable only via Tailscale, remove the `ports:` section from the `tailscale` service in [docker-compose.yml](docker-compose.yml). Zerobyte will still be able to access the internet and other resources outside the tailnet, but UI will only be accessible over Tailscale with possibility to further restrict access to it with ACLs/tags.
|
||||
|
||||
## Notes
|
||||
|
||||
- `network_mode: service:tailscale` makes Zerobyte share the Tailscale container’s *entire* network namespace (interfaces + routing table).
|
||||
- Traffic to tailnet IPs (typically `100.x.y.z`) goes over `tailscale0` and is governed by Tailscale ACLs; traffic to your LAN/Internet may still go over the normal network interface depending on routes and host firewall.
|
||||
- If `--accept-routes` is used, the Tailscale container may add routes to your routing table that Zerobyte will also use and be able to access remote networks.
|
||||
- If `--accept-dns` is used, Zerobyte will also use Tailscale’s DNS servers.
|
||||
- Zerobyte still needs `SYS_ADMIN` and `/dev/fuse` if you intend to mount NFS/SMB/WebDAV volumes from inside the container.
|
||||
|
||||
The example uses these environment variables (see [.env.example](.env.example)):
|
||||
|
||||
- `TS_AUTHKEY` (required)
|
||||
- `TS_HOSTNAME` (optional)
|
||||
- `TS_EXTRA_ARGS` (optional; passed to `tailscale up`)
|
||||
- `TS_USERSPACE` (optional; set to `true` to use userspace mode)
|
||||
- `TZ` (optional)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If the `tailscale` container can’t start due to missing TUN support, ensure your host has `/dev/net/tun` available and that Docker is allowed to use it.
|
||||
- If your tailnet uses ACLs/tags, set `TS_EXTRA_ARGS` accordingly (for example `--advertise-tags=tag:backup`).
|
||||
- If the `tailscale` container fails due to Docker Desktop / missing TUN support: set `TS_USERSPACE=true` in your `.env`, remove the `/dev/net/tun:/dev/net/tun` device mapping in [docker-compose.yml](docker-compose.yml), and keep `SYS_MODULE` disabled (commented out).
|
||||
|
||||
To confirm the tailnet address of the container:
|
||||
|
||||
```bash
|
||||
docker exec zerobyte-tailscale tailscale status
|
||||
docker exec zerobyte-tailscale tailscale ip -4
|
||||
```
|
||||
|
||||
To confirm received routes when `--accept-routes` is used in kernel mode:
|
||||
(Missing routes could be due to ACLs or because `--accept-routes` is not set or not supported in userspace mode)
|
||||
```bash
|
||||
docker exec zerobyte-tailscale ip route
|
||||
docker exec zerobyte-tailscale ip route show table 52
|
||||
```
|
||||
51
examples/tailscale-sidecar/docker-compose.yml
Normal file
51
examples/tailscale-sidecar/docker-compose.yml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
services:
|
||||
tailscale:
|
||||
image: tailscale/tailscale:stable
|
||||
container_name: zerobyte-tailscale
|
||||
hostname: ${TS_HOSTNAME:-zerobyte}
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
# Optional: Some hosts require this for kernel-mode Tailscale.
|
||||
# If it causes issues (common on Docker Desktop), keep it commented out (disabled as shown here).
|
||||
# - SYS_MODULE
|
||||
# Kernel-mode Tailscale (TS_USERSPACE=false) requires /dev/net/tun.
|
||||
# If you switch to userspace mode (TS_USERSPACE=true), you MUST remove this
|
||||
# devices section (or at least this mapping), otherwise the container may
|
||||
# fail to start or hit runtime errors on some hosts.
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
environment:
|
||||
- TS_AUTHKEY=${TS_AUTHKEY}
|
||||
- TS_STATE_DIR=/var/lib/tailscale
|
||||
# Kernel-mode (false) vs userspace-mode (true).
|
||||
- TS_USERSPACE=${TS_USERSPACE:-false}
|
||||
# Optional flags passed to `tailscale up`.
|
||||
# Examples:
|
||||
# - --advertise-tags=tag:zerobyte
|
||||
# - --accept-dns=true --accept-routes
|
||||
- TS_EXTRA_ARGS=${TS_EXTRA_ARGS:-}
|
||||
volumes:
|
||||
- /var/lib/tailscale:/var/lib/tailscale
|
||||
# If you only want access over Tailscale (not from the local network), remove this.
|
||||
ports:
|
||||
- "4096:4096"
|
||||
|
||||
zerobyte:
|
||||
image: ghcr.io/nicotsx/zerobyte:latest
|
||||
container_name: zerobyte
|
||||
restart: unless-stopped
|
||||
# Uncomment if you need to mount NFS/SMB/WebDAV volumes:
|
||||
# cap_add:
|
||||
# - SYS_ADMIN
|
||||
# devices:
|
||||
# - /dev/fuse:/dev/fuse
|
||||
# Share the Tailscale network namespace (sidecar pattern)
|
||||
network_mode: service:tailscale
|
||||
depends_on:
|
||||
- tailscale
|
||||
environment:
|
||||
- TZ=${TZ:-UTC}
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /var/lib/zerobyte:/var/lib/zerobyte
|
||||
17
package.json
17
package.json
|
|
@ -2,7 +2,7 @@
|
|||
"name": "zerobyte",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.3",
|
||||
"packageManager": "bun@1.3.5",
|
||||
"scripts": {
|
||||
"build": "react-router build",
|
||||
"dev": "bunx --bun vite",
|
||||
|
|
@ -14,7 +14,11 @@
|
|||
"start:prod": "docker compose down && docker compose up --build zerobyte-prod",
|
||||
"gen:api-client": "openapi-ts",
|
||||
"gen:migrations": "drizzle-kit generate",
|
||||
"studio": "drizzle-kit studio"
|
||||
"studio": "drizzle-kit studio",
|
||||
"test": "dotenv -e .env.test -- bun test --preload ./app/test/setup.ts"
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild": "^0.27.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
|
|
@ -49,6 +53,7 @@
|
|||
"es-toolkit": "^1.42.0",
|
||||
"hono": "4.10.5",
|
||||
"hono-openapi": "^1.1.1",
|
||||
"hono-rate-limiter": "^0.5.0",
|
||||
"http-errors-enhanced": "^4.0.2",
|
||||
"isbot": "^5.1.32",
|
||||
"lucide-react": "^0.555.0",
|
||||
|
|
@ -69,14 +74,16 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.8",
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@hey-api/openapi-ts": "^0.88.0",
|
||||
"@react-router/dev": "^7.10.0",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@tanstack/react-query-devtools": "^5.91.1",
|
||||
"@types/bun": "^1.3.3",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/bun": "^1.3.4",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"drizzle-kit": "^0.31.7",
|
||||
"lightningcss": "^1.30.2",
|
||||
"tailwindcss": "^4.1.17",
|
||||
|
|
@ -85,6 +92,6 @@
|
|||
"typescript": "^5.9.3",
|
||||
"vite": "^7.2.6",
|
||||
"vite-bundle-analyzer": "^1.2.3",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
"vite-tsconfig-paths": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export default defineConfig({
|
|||
plugins: [reactRouterHonoServer({ runtime: "bun" }), reactRouter(), tailwindcss(), tsconfigPaths()],
|
||||
build: {
|
||||
outDir: "dist",
|
||||
sourcemap: true,
|
||||
sourcemap: false,
|
||||
rollupOptions: {
|
||||
external: ["bun"],
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue