Compare commits
1 commit
main
...
chore/miss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca4c74fcb3 |
88 changed files with 1114 additions and 2822 deletions
|
|
@ -3,7 +3,6 @@
|
||||||
!bun.lock
|
!bun.lock
|
||||||
!package.json
|
!package.json
|
||||||
!.gitignore
|
!.gitignore
|
||||||
!bunfig.toml
|
|
||||||
|
|
||||||
!**/package.json
|
!**/package.json
|
||||||
!**/bun.lock
|
!**/bun.lock
|
||||||
|
|
@ -24,4 +23,3 @@
|
||||||
node_modules/**
|
node_modules/**
|
||||||
dist/**
|
dist/**
|
||||||
.output/**
|
.output/**
|
||||||
app/test/integration/artifacts/**
|
|
||||||
|
|
|
||||||
1
.github/workflows/docs-deploy.yml
vendored
1
.github/workflows/docs-deploy.yml
vendored
|
|
@ -13,7 +13,6 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
name: Deploy docs
|
name: Deploy docs
|
||||||
if: github.event.repository.default_branch == github.ref_name
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
|
||||||
|
|
|
||||||
35
.github/workflows/integration.yml
vendored
35
.github/workflows/integration.yml
vendored
|
|
@ -1,35 +0,0 @@
|
||||||
name: Integration Tests
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
workflow_call:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
integration:
|
|
||||||
name: Integration
|
|
||||||
timeout-minutes: 30
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
uses: "./.github/actions/install-dependencies"
|
|
||||||
|
|
||||||
- name: Run integration tests
|
|
||||||
env:
|
|
||||||
SKIP_VOLUME_MOUNT_INTEGRATION_TESTS: "true"
|
|
||||||
run: bun run test:integration
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
||||||
if: failure()
|
|
||||||
with:
|
|
||||||
name: integration-artifacts
|
|
||||||
path: |
|
|
||||||
app/test/integration/artifacts/compose.log
|
|
||||||
app/test/integration/artifacts/docker-output.log
|
|
||||||
app/test/integration/artifacts/runs/**
|
|
||||||
retention-days: 5
|
|
||||||
10
.github/workflows/nightly.yml
vendored
10
.github/workflows/nightly.yml
vendored
|
|
@ -20,20 +20,20 @@ jobs:
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||||
with:
|
with:
|
||||||
driver: cloud
|
driver: cloud
|
||||||
endpoint: "meienberger/runtipi-builder"
|
endpoint: "meienberger/runtipi-builder"
|
||||||
install: true
|
install: true
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
|
@ -41,14 +41,14 @@ jobs:
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository_owner }}/zerobyte
|
images: ghcr.io/${{ github.repository_owner }}/zerobyte
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=nightly
|
type=raw,value=nightly
|
||||||
|
|
||||||
- name: Push images to GitHub Container Registry
|
- name: Push images to GitHub Container Registry
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
target: production
|
target: production
|
||||||
|
|
|
||||||
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
|
|
@ -43,13 +43,10 @@ jobs:
|
||||||
e2e-tests:
|
e2e-tests:
|
||||||
uses: ./.github/workflows/e2e.yml
|
uses: ./.github/workflows/e2e.yml
|
||||||
|
|
||||||
integration-tests:
|
|
||||||
uses: ./.github/workflows/integration.yml
|
|
||||||
|
|
||||||
build-images:
|
build-images:
|
||||||
environment: release
|
environment: release
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
needs: [determine-release-type, checks, e2e-tests, integration-tests]
|
needs: [determine-release-type, checks, e2e-tests]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
@ -63,20 +60,20 @@ jobs:
|
||||||
ref: ${{ github.ref }}
|
ref: ${{ github.ref }}
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||||
with:
|
with:
|
||||||
driver: cloud
|
driver: cloud
|
||||||
endpoint: "meienberger/runtipi-builder"
|
endpoint: "meienberger/runtipi-builder"
|
||||||
install: true
|
install: true
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
|
@ -84,7 +81,7 @@ jobs:
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository_owner }}/zerobyte
|
images: ghcr.io/${{ github.repository_owner }}/zerobyte
|
||||||
tags: |
|
tags: |
|
||||||
|
|
@ -96,7 +93,7 @@ jobs:
|
||||||
latest=${{ needs.determine-release-type.outputs.release_type == 'release' }}
|
latest=${{ needs.determine-release-type.outputs.release_type == 'release' }}
|
||||||
|
|
||||||
- name: Push images to GitHub Container Registry
|
- name: Push images to GitHub Container Registry
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
target: production
|
target: production
|
||||||
|
|
|
||||||
|
|
@ -44,15 +44,6 @@ RUN bzip2 -d restic.bz2 && chmod +x restic
|
||||||
RUN mv rclone-v*-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
|
RUN tar -xzf shoutrrr.tar.gz && chmod +x shoutrrr
|
||||||
|
|
||||||
# ------------------------------
|
|
||||||
# RUNTIME TOOLS
|
|
||||||
# ------------------------------
|
|
||||||
FROM base AS runtime-tools
|
|
||||||
|
|
||||||
COPY --from=deps /deps/restic /usr/local/bin/restic
|
|
||||||
COPY --from=deps /deps/rclone /usr/local/bin/rclone
|
|
||||||
COPY --from=deps /deps/shoutrrr /usr/local/bin/shoutrrr
|
|
||||||
|
|
||||||
# ------------------------------
|
# ------------------------------
|
||||||
# DEVELOPMENT
|
# DEVELOPMENT
|
||||||
# ------------------------------
|
# ------------------------------
|
||||||
|
|
|
||||||
14
README.md
14
README.md
|
|
@ -50,7 +50,7 @@ In order to run Zerobyte, you need to have Docker and Docker Compose installed o
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
zerobyte:
|
zerobyte:
|
||||||
image: ghcr.io/nicotsx/zerobyte:v0.38
|
image: ghcr.io/nicotsx/zerobyte:v0.37
|
||||||
container_name: zerobyte
|
container_name: zerobyte
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
cap_add:
|
cap_add:
|
||||||
|
|
@ -156,7 +156,7 @@ If you only need to back up locally-mounted folders and don't require remote sha
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
zerobyte:
|
zerobyte:
|
||||||
image: ghcr.io/nicotsx/zerobyte:v0.38
|
image: ghcr.io/nicotsx/zerobyte:v0.37
|
||||||
container_name: zerobyte
|
container_name: zerobyte
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
|
|
@ -195,7 +195,7 @@ If you want to backup a local directory on the same host where Zerobyte is runni
|
||||||
```diff
|
```diff
|
||||||
services:
|
services:
|
||||||
zerobyte:
|
zerobyte:
|
||||||
image: ghcr.io/nicotsx/zerobyte:v0.38
|
image: ghcr.io/nicotsx/zerobyte:v0.37
|
||||||
container_name: zerobyte
|
container_name: zerobyte
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
cap_add:
|
cap_add:
|
||||||
|
|
@ -270,7 +270,7 @@ Zerobyte can use [rclone](https://rclone.org/) to support 40+ cloud storage prov
|
||||||
```diff
|
```diff
|
||||||
services:
|
services:
|
||||||
zerobyte:
|
zerobyte:
|
||||||
image: ghcr.io/nicotsx/zerobyte:v0.38
|
image: ghcr.io/nicotsx/zerobyte:v0.37
|
||||||
container_name: zerobyte
|
container_name: zerobyte
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
cap_add:
|
cap_add:
|
||||||
|
|
@ -336,12 +336,6 @@ Zerobyte allows you to easily restore your data from backups. To restore data, n
|
||||||
|
|
||||||
Zerobyte uses [better-auth](https://github.com/better-auth/better-auth) for authentication and session management. The authentication system automatically adapts to your deployment scenario:
|
Zerobyte uses [better-auth](https://github.com/better-auth/better-auth) for authentication and session management. The authentication system automatically adapts to your deployment scenario:
|
||||||
|
|
||||||
### Users and roles
|
|
||||||
|
|
||||||
Zerobyte does not currently provide fine-grained RBAC for backup operations. Authenticated organization members are trusted operators for repositories, volumes, backup schedules, restores, and notification destinations in their organization.
|
|
||||||
|
|
||||||
Organization roles mainly restrict organization-management and instance-management actions. For example, normal members cannot manage members, SSO settings, invitations, registration, or global users, but they can still operate backup resources. Only add users to an organization if you are comfortable with them using the storage, volume, and notification capabilities configured for the instance.
|
|
||||||
|
|
||||||
### Cookie security
|
### Cookie security
|
||||||
|
|
||||||
- **IP Address/HTTP access**: Set `BASE_URL=http://192.168.1.50:4096` (or your IP). Cookies will use `Secure: false`, allowing immediate login without SSL.
|
- **IP Address/HTTP access**: Set `BASE_URL=http://192.168.1.50:4096` (or your IP). Cookies will use `Secure: false`, allowing immediate login without SSL.
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,10 @@ export const createClient = (config: Config = {}): Client => {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (opts.security) {
|
if (opts.security) {
|
||||||
await setAuthParams(opts);
|
await setAuthParams({
|
||||||
|
...opts,
|
||||||
|
security: opts.security,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.requestValidator) {
|
if (opts.requestValidator) {
|
||||||
|
|
|
||||||
|
|
@ -119,12 +119,14 @@ const checkForExistence = (
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function setAuthParams(
|
export const setAuthParams = async ({
|
||||||
options: Pick<RequestOptions, 'auth' | 'query' | 'security'> & {
|
security,
|
||||||
|
...options
|
||||||
|
}: Pick<Required<RequestOptions>, 'security'> &
|
||||||
|
Pick<RequestOptions, 'auth' | 'query'> & {
|
||||||
headers: Headers;
|
headers: Headers;
|
||||||
},
|
}) => {
|
||||||
): Promise<void> {
|
for (const auth of security) {
|
||||||
for (const auth of options.security ?? []) {
|
|
||||||
if (checkForExistence(options, auth.name)) {
|
if (checkForExistence(options, auth.name)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -153,7 +155,7 @@ export async function setAuthParams(
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const buildUrl: Client['buildUrl'] = (options) =>
|
export const buildUrl: Client['buildUrl'] = (options) =>
|
||||||
getUrl({
|
getUrl({
|
||||||
|
|
|
||||||
|
|
@ -2267,7 +2267,6 @@ export type RestoreSnapshotData = {
|
||||||
excludeXattr?: Array<string>;
|
excludeXattr?: Array<string>;
|
||||||
delete?: boolean;
|
delete?: boolean;
|
||||||
targetPath?: string;
|
targetPath?: string;
|
||||||
targetAgentId?: string;
|
|
||||||
overwrite?: 'always' | 'if-changed' | 'if-newer' | 'never';
|
overwrite?: 'always' | 'if-changed' | 'if-newer' | 'never';
|
||||||
};
|
};
|
||||||
path: {
|
path: {
|
||||||
|
|
@ -2279,11 +2278,13 @@ export type RestoreSnapshotData = {
|
||||||
|
|
||||||
export type RestoreSnapshotResponses = {
|
export type RestoreSnapshotResponses = {
|
||||||
/**
|
/**
|
||||||
* Snapshot restore started
|
* Snapshot restored successfully
|
||||||
*/
|
*/
|
||||||
202: {
|
200: {
|
||||||
restoreId: string;
|
success: boolean;
|
||||||
status: 'started';
|
message: string;
|
||||||
|
filesRestored: number;
|
||||||
|
filesSkipped: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,29 @@ describe("SnapshotTreeBrowser", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("prefetches using the query path when display and query roots differ", async () => {
|
||||||
|
const requests = mockListSnapshotFiles();
|
||||||
|
|
||||||
|
renderSnapshotTreeBrowser();
|
||||||
|
|
||||||
|
const row = await screen.findByRole("button", { name: "project" });
|
||||||
|
const initialRequestCount = requests.length;
|
||||||
|
|
||||||
|
await userEvent.hover(row);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(requests.length).toBe(initialRequestCount + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(requests.at(-1)).toEqual({
|
||||||
|
shortId: "repo-1",
|
||||||
|
snapshotId: "snap-1",
|
||||||
|
path: "/mnt/project",
|
||||||
|
offset: "0",
|
||||||
|
limit: "500",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("shows the query root contents when display and query roots differ", async () => {
|
test("shows the query root contents when display and query roots differ", async () => {
|
||||||
mockListSnapshotFiles();
|
mockListSnapshotFiles();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { FileBrowser, type FileBrowserUiProps } from "~/client/components/file-b
|
||||||
import { useFileBrowser } from "~/client/hooks/use-file-browser";
|
import { useFileBrowser } from "~/client/hooks/use-file-browser";
|
||||||
import { parseError } from "~/client/lib/errors";
|
import { parseError } from "~/client/lib/errors";
|
||||||
import { isPathWithin, normalizeAbsolutePath } from "@zerobyte/core/utils";
|
import { isPathWithin, normalizeAbsolutePath } from "@zerobyte/core/utils";
|
||||||
|
import { logger } from "~/client/lib/logger";
|
||||||
|
|
||||||
function createPathPrefixFns(basePath: string) {
|
function createPathPrefixFns(basePath: string) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -83,6 +84,16 @@ export const SnapshotTreeBrowser = (props: SnapshotTreeBrowserProps) => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
prefetchFolder: (displayPath) => {
|
||||||
|
void queryClient
|
||||||
|
.prefetchQuery(
|
||||||
|
listSnapshotFilesOptions({
|
||||||
|
path: { shortId: repositoryId, snapshotId },
|
||||||
|
query: { path: displayPath, offset: 0, limit: pageSize },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.catch((e) => logger.error(e));
|
||||||
|
},
|
||||||
pathTransform: displayPathFns,
|
pathTransform: displayPathFns,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { OTPInput, OTPInputContext } from "input-otp";
|
import { OTPInput, OTPInputContext } from "input-otp";
|
||||||
|
import { MinusIcon } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "~/client/lib/utils";
|
import { cn } from "~/client/lib/utils";
|
||||||
|
|
||||||
|
|
@ -54,8 +55,12 @@ function InputOTPSlot({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InputOTPSeparator({ ...props }: React.ComponentProps<"hr">) {
|
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
||||||
return <hr data-slot="input-otp-separator" {...props} />;
|
return (
|
||||||
|
<div data-slot="input-otp-separator" role="separator" {...props}>
|
||||||
|
<MinusIcon />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import { describe, expect, test } from "vitest";
|
|
||||||
import { getIsSecureContextForRequest, isSecureContextOrigin } from "../is-secure-context";
|
|
||||||
|
|
||||||
describe("isSecureContextOrigin", () => {
|
|
||||||
test("treats HTTPS and loopback HTTP origins as secure contexts", () => {
|
|
||||||
expect(isSecureContextOrigin("https://example.com")).toBe(true);
|
|
||||||
expect(isSecureContextOrigin("http://localhost:3000")).toBe(true);
|
|
||||||
expect(isSecureContextOrigin("http://app.localhost")).toBe(true);
|
|
||||||
expect(isSecureContextOrigin("http://127.0.0.1:3000")).toBe(true);
|
|
||||||
expect(isSecureContextOrigin("http://[::1]:3000")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("treats non-local HTTP origins as insecure contexts", () => {
|
|
||||||
expect(isSecureContextOrigin("http://example.com")).toBe(false);
|
|
||||||
expect(isSecureContextOrigin("http://127.example.com")).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getIsSecureContextForRequest", () => {
|
|
||||||
test("uses forwarded protocol and host when present", () => {
|
|
||||||
const request = new Request("http://internal.local/settings", {
|
|
||||||
headers: {
|
|
||||||
host: "internal.local",
|
|
||||||
"x-forwarded-host": "zerobyte.example.com",
|
|
||||||
"x-forwarded-proto": "https",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(getIsSecureContextForRequest(request)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("uses the request URL when forwarded headers are absent", () => {
|
|
||||||
expect(getIsSecureContextForRequest(new Request("http://zerobyte.example.com/settings"))).toBe(false);
|
|
||||||
expect(getIsSecureContextForRequest(new Request("https://zerobyte.example.com/settings"))).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
import { createIsomorphicFn } from "@tanstack/react-start";
|
|
||||||
import { getRequest } from "@tanstack/react-start/server";
|
|
||||||
import ipaddr from "ipaddr.js";
|
|
||||||
|
|
||||||
const getFirstHeaderValue = (value: string | null) => value?.split(",")[0]?.trim();
|
|
||||||
|
|
||||||
const isLoopbackIp = (hostname: string) => {
|
|
||||||
try {
|
|
||||||
return ipaddr.parse(hostname).range() === "loopback";
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRequestOrigin = (request: Request) => {
|
|
||||||
const requestUrl = new URL(request.url);
|
|
||||||
const forwardedProto = getFirstHeaderValue(request.headers.get("x-forwarded-proto"));
|
|
||||||
const forwardedHost = getFirstHeaderValue(request.headers.get("x-forwarded-host"));
|
|
||||||
const host = forwardedHost || getFirstHeaderValue(request.headers.get("host"));
|
|
||||||
|
|
||||||
if (!host) {
|
|
||||||
return requestUrl.origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
const protocol = (forwardedProto || requestUrl.protocol).replace(/:$/, "");
|
|
||||||
return `${protocol}://${host}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isSecureContextOrigin = (origin: string) => {
|
|
||||||
const url = new URL(origin);
|
|
||||||
|
|
||||||
if (url.protocol === "https:") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hostname = url.hostname.replace(/^\[(.*)\]$/, "$1").toLowerCase();
|
|
||||||
|
|
||||||
return hostname === "localhost" || hostname.endsWith(".localhost") || isLoopbackIp(hostname);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getIsSecureContextForRequest = (request: Request) => isSecureContextOrigin(getRequestOrigin(request));
|
|
||||||
|
|
||||||
export const getIsSecureContext = createIsomorphicFn()
|
|
||||||
.server(() => {
|
|
||||||
try {
|
|
||||||
return getIsSecureContextForRequest(getRequest());
|
|
||||||
} catch {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.client(() => window.isSecureContext ?? true);
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
||||||
import { useServerFn } from "@tanstack/react-start";
|
|
||||||
import { getPublicSsoProvidersOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
|
||||||
import { SsoLoginButtons } from "~/client/modules/sso/components/sso-login-buttons";
|
|
||||||
import { getLoginOptions } from "~/server/lib/functions/login-options";
|
|
||||||
import { PasskeySignInButton } from "./passkey-sign-in-button";
|
|
||||||
|
|
||||||
type AlternativeSignInSectionProps = {
|
|
||||||
onPasskeySignIn: () => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function AlternativeSignInSection({ onPasskeySignIn }: AlternativeSignInSectionProps) {
|
|
||||||
const getOptions = useServerFn(getLoginOptions);
|
|
||||||
const { data: ssoProviders } = useSuspenseQuery({
|
|
||||||
...getPublicSsoProvidersOptions(),
|
|
||||||
});
|
|
||||||
const { data: loginOptions } = useSuspenseQuery({
|
|
||||||
queryKey: ["login-options"],
|
|
||||||
queryFn: getOptions,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (ssoProviders.providers.length === 0 && !loginOptions.hasPasskeySignIn) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="pt-4 border-t border-border/60 space-y-3">
|
|
||||||
<p className="text-sm font-medium">Alternative Sign-in</p>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{loginOptions.hasPasskeySignIn && <PasskeySignInButton onSignIn={onPasskeySignIn} />}
|
|
||||||
<SsoLoginButtons providers={ssoProviders.providers} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
|
||||||
import { Fingerprint } from "lucide-react";
|
|
||||||
import { Button } from "~/client/components/ui/button";
|
|
||||||
import { authClient } from "~/client/lib/auth-client";
|
|
||||||
import { logger } from "~/client/lib/logger";
|
|
||||||
import { LOGIN_ERROR_CODES, PASSKEY_LOGIN_FAILED_ERROR, type LoginErrorCode } from "~/lib/sso-errors";
|
|
||||||
|
|
||||||
type PasskeySignInButtonProps = {
|
|
||||||
onSignIn: () => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PasskeySignInError = {
|
|
||||||
code?: string;
|
|
||||||
message?: string;
|
|
||||||
status?: number;
|
|
||||||
statusText?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LOGIN_ERROR_CODE_SET = new Set<string>(LOGIN_ERROR_CODES);
|
|
||||||
|
|
||||||
function getPasskeyLoginErrorCode(code: string | undefined): LoginErrorCode {
|
|
||||||
if (code && LOGIN_ERROR_CODE_SET.has(code)) {
|
|
||||||
return code as LoginErrorCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
return PASSKEY_LOGIN_FAILED_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PasskeySignInButton({ onSignIn }: PasskeySignInButtonProps) {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const passkeyLoginMutation = useMutation<void, PasskeySignInError>({
|
|
||||||
mutationFn: async () => {
|
|
||||||
const { error } = await authClient.signIn.passkey();
|
|
||||||
if (error) throw error;
|
|
||||||
|
|
||||||
await onSignIn();
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
logger.error(error);
|
|
||||||
|
|
||||||
void navigate({
|
|
||||||
to: "/login",
|
|
||||||
search: {
|
|
||||||
error: getPasskeyLoginErrorCode(error.code),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className="w-full"
|
|
||||||
loading={passkeyLoginMutation.isPending}
|
|
||||||
disabled={passkeyLoginMutation.isPending}
|
|
||||||
onClick={() => passkeyLoginMutation.mutate()}
|
|
||||||
>
|
|
||||||
<Fingerprint className="h-4 w-4 mr-2" />
|
|
||||||
Sign in with passkey
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,49 +1,16 @@
|
||||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
import { HttpResponse, http, server } from "~/test/msw/server";
|
import { HttpResponse, http, server } from "~/test/msw/server";
|
||||||
import { cleanup, render, screen, userEvent, waitFor } from "~/test/test-utils";
|
import { cleanup, render, screen } from "~/test/test-utils";
|
||||||
import { getLoginErrorDescription, PASSKEY_LOGIN_FAILED_ERROR } from "~/lib/sso-errors";
|
|
||||||
|
|
||||||
const { mockGetLoginOptions, mockNavigate, mockPasskeySignIn } = vi.hoisted(() => ({
|
|
||||||
mockGetLoginOptions: vi.fn(async () => ({ hasPasskeySignIn: false })),
|
|
||||||
mockNavigate: vi.fn(async () => {}),
|
|
||||||
mockPasskeySignIn: vi.fn(
|
|
||||||
async (): Promise<{
|
|
||||||
data: unknown;
|
|
||||||
error: { code: string; message: string } | null;
|
|
||||||
}> => ({ data: null, error: null }),
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@tanstack/react-router", async (importOriginal) => {
|
vi.mock("@tanstack/react-router", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("@tanstack/react-router")>();
|
const actual = await importOriginal<typeof import("@tanstack/react-router")>();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
useNavigate: (() => mockNavigate) as typeof actual.useNavigate,
|
useNavigate: (() => vi.fn(async () => {})) as typeof actual.useNavigate,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("@tanstack/react-start", async (importOriginal) => {
|
|
||||||
const actual = await importOriginal<typeof import("@tanstack/react-start")>();
|
|
||||||
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
useServerFn: (fn: unknown) => fn,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mock("~/server/lib/functions/login-options", () => ({
|
|
||||||
getLoginOptions: mockGetLoginOptions,
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("~/client/lib/auth-client", () => ({
|
|
||||||
authClient: {
|
|
||||||
signIn: {
|
|
||||||
passkey: mockPasskeySignIn,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { LoginPage } from "../login";
|
import { LoginPage } from "../login";
|
||||||
const inviteOnlyMessage =
|
const inviteOnlyMessage =
|
||||||
"Access is invite-only. Ask an organization admin to send you an invitation before signing in with SSO.";
|
"Access is invite-only. Ask an organization admin to send you an invitation before signing in with SSO.";
|
||||||
|
|
@ -62,12 +29,6 @@ const mockSsoProvidersRequest = (
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mockGetLoginOptions.mockClear();
|
|
||||||
mockGetLoginOptions.mockResolvedValue({ hasPasskeySignIn: false });
|
|
||||||
mockNavigate.mockClear();
|
|
||||||
mockPasskeySignIn.mockClear();
|
|
||||||
mockPasskeySignIn.mockResolvedValue({ data: null, error: null });
|
|
||||||
vi.unstubAllGlobals();
|
|
||||||
cleanup();
|
cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -120,14 +81,6 @@ describe("LoginPage", () => {
|
||||||
expect(await screen.findByText("SSO authentication failed. Please try again.")).toBeTruthy();
|
expect(await screen.findByText("SSO authentication failed. Please try again.")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("shows passkey login failure message when passkey returns the login error code", async () => {
|
|
||||||
mockSsoProvidersRequest();
|
|
||||||
|
|
||||||
render(<LoginPage error={"PASSKEY_LOGIN_FAILED"} />, { withSuspense: true });
|
|
||||||
|
|
||||||
expect(await screen.findByText(getLoginErrorDescription(PASSKEY_LOGIN_FAILED_ERROR))).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("does not show error message for invalid error codes", async () => {
|
test("does not show error message for invalid error codes", async () => {
|
||||||
mockSsoProvidersRequest();
|
mockSsoProvidersRequest();
|
||||||
|
|
||||||
|
|
@ -137,141 +90,11 @@ describe("LoginPage", () => {
|
||||||
expect(screen.queryByText(inviteOnlyMessage)).toBeNull();
|
expect(screen.queryByText(inviteOnlyMessage)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders available SSO providers from the alternative sign-in section", async () => {
|
test("renders available SSO providers from the real SSO section", async () => {
|
||||||
mockSsoProvidersRequest([{ providerId: "acme", organizationSlug: "acme-org" }]);
|
mockSsoProvidersRequest([{ providerId: "acme", organizationSlug: "acme-org" }]);
|
||||||
|
|
||||||
render(<LoginPage />, { withSuspense: true });
|
render(<LoginPage />, { withSuspense: true });
|
||||||
|
|
||||||
expect(await screen.findByRole("button", { name: "Log in with acme" })).toBeTruthy();
|
expect(await screen.findByRole("button", { name: "Log in with acme" })).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders passkey sign-in when an active user has a passkey", async () => {
|
|
||||||
mockSsoProvidersRequest();
|
|
||||||
mockGetLoginOptions.mockResolvedValue({ hasPasskeySignIn: true });
|
|
||||||
|
|
||||||
render(<LoginPage />, { withSuspense: true });
|
|
||||||
|
|
||||||
expect(await screen.findByRole("button", { name: "Sign in with passkey" })).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("redirects passkey verification failures to the login error box", async () => {
|
|
||||||
mockSsoProvidersRequest();
|
|
||||||
mockGetLoginOptions.mockResolvedValue({ hasPasskeySignIn: true });
|
|
||||||
mockPasskeySignIn.mockResolvedValue({
|
|
||||||
data: null,
|
|
||||||
error: { code: "AUTHENTICATION_FAILED", message: "Authentication failed" },
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<LoginPage />, { withSuspense: true });
|
|
||||||
|
|
||||||
await userEvent.click(await screen.findByRole("button", { name: "Sign in with passkey" }));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockNavigate).toHaveBeenCalledWith({
|
|
||||||
to: "/login",
|
|
||||||
search: {
|
|
||||||
error: PASSKEY_LOGIN_FAILED_ERROR,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("redirects unauthorized passkey failures to the login error box", async () => {
|
|
||||||
mockSsoProvidersRequest();
|
|
||||||
mockGetLoginOptions.mockResolvedValue({ hasPasskeySignIn: true });
|
|
||||||
mockPasskeySignIn.mockResolvedValue({
|
|
||||||
data: null,
|
|
||||||
error: { code: "UNAUTHORIZED", message: "Unauthorized" },
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<LoginPage />, { withSuspense: true });
|
|
||||||
|
|
||||||
await userEvent.click(await screen.findByRole("button", { name: "Sign in with passkey" }));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockNavigate).toHaveBeenCalledWith({
|
|
||||||
to: "/login",
|
|
||||||
search: {
|
|
||||||
error: PASSKEY_LOGIN_FAILED_ERROR,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("preserves specific passkey login error codes", async () => {
|
|
||||||
mockSsoProvidersRequest();
|
|
||||||
mockGetLoginOptions.mockResolvedValue({ hasPasskeySignIn: true });
|
|
||||||
mockPasskeySignIn.mockResolvedValue({
|
|
||||||
data: null,
|
|
||||||
error: { code: "ERROR_INVALID_RP_ID", message: "Auth cancelled" },
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<LoginPage />, { withSuspense: true });
|
|
||||||
|
|
||||||
await userEvent.click(await screen.findByRole("button", { name: "Sign in with passkey" }));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockNavigate).toHaveBeenCalledWith({
|
|
||||||
to: "/login",
|
|
||||||
search: {
|
|
||||||
error: "ERROR_INVALID_RP_ID",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("redirects conditional passkey autofill failures to the login error box", async () => {
|
|
||||||
mockSsoProvidersRequest();
|
|
||||||
mockPasskeySignIn.mockResolvedValue({
|
|
||||||
data: null,
|
|
||||||
error: { code: "AUTHENTICATION_FAILED", message: "Authentication failed" },
|
|
||||||
});
|
|
||||||
vi.stubGlobal("PublicKeyCredential", {
|
|
||||||
isConditionalMediationAvailable: vi.fn(async () => true),
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<LoginPage />, { withSuspense: true });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockPasskeySignIn).toHaveBeenCalledWith({
|
|
||||||
autoFill: true,
|
|
||||||
});
|
|
||||||
expect(mockNavigate).toHaveBeenCalledWith({
|
|
||||||
to: "/login",
|
|
||||||
search: {
|
|
||||||
error: "PASSKEY_LOGIN_FAILED",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ignores conditional passkey autofill cancellation", async () => {
|
|
||||||
mockSsoProvidersRequest();
|
|
||||||
mockPasskeySignIn.mockResolvedValue({
|
|
||||||
data: null,
|
|
||||||
error: { code: "AUTH_CANCELLED", message: "Authentication cancelled" },
|
|
||||||
});
|
|
||||||
vi.stubGlobal("PublicKeyCredential", {
|
|
||||||
isConditionalMediationAvailable: vi.fn(async () => true),
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<LoginPage />, { withSuspense: true });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockPasskeySignIn).toHaveBeenCalledWith({
|
|
||||||
autoFill: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
expect(mockNavigate).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("hides alternative sign-in when no SSO providers or passkeys are available", async () => {
|
|
||||||
mockSsoProvidersRequest();
|
|
||||||
|
|
||||||
render(<LoginPage />, { withSuspense: true });
|
|
||||||
|
|
||||||
await screen.findByText("Login to your account");
|
|
||||||
expect(screen.queryByText("Alternative Sign-in")).toBeNull();
|
|
||||||
expect(screen.queryByRole("button", { name: "Sign in with passkey" })).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AuthLayout } from "~/client/components/auth-layout";
|
import { AuthLayout } from "~/client/components/auth-layout";
|
||||||
|
|
@ -12,12 +12,11 @@ import { authClient } from "~/client/lib/auth-client";
|
||||||
import { logger } from "~/client/lib/logger";
|
import { logger } from "~/client/lib/logger";
|
||||||
import { RECOVERY_KEY_DOWNLOAD_SKIPPED_COOKIE_NAME } from "~/lib/recovery-key-skip";
|
import { RECOVERY_KEY_DOWNLOAD_SKIPPED_COOKIE_NAME } from "~/lib/recovery-key-skip";
|
||||||
import { decodeLoginError, getLoginErrorDescription } from "~/client/lib/sso-errors";
|
import { decodeLoginError, getLoginErrorDescription } from "~/client/lib/sso-errors";
|
||||||
import { PASSKEY_LOGIN_FAILED_ERROR } from "~/lib/sso-errors";
|
|
||||||
import { ResetPasswordDialog } from "../components/reset-password-dialog";
|
import { ResetPasswordDialog } from "../components/reset-password-dialog";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { normalizeUsername } from "~/lib/username";
|
import { normalizeUsername } from "~/lib/username";
|
||||||
import { cn } from "~/client/lib/utils";
|
import { cn } from "~/client/lib/utils";
|
||||||
import { AlternativeSignInSection } from "../components/alternative-sign-in-section";
|
import { SsoLoginSection } from "~/client/modules/sso/components/sso-login-section";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const loginSchema = z.object({
|
const loginSchema = z.object({
|
||||||
|
|
@ -34,17 +33,6 @@ type LoginPageProps = {
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PasskeySignInError = {
|
|
||||||
code?: string;
|
|
||||||
message?: string;
|
|
||||||
status?: number;
|
|
||||||
statusText?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function isPasskeyVerificationFailure(error: PasskeySignInError | null) {
|
|
||||||
return error?.code === "AUTHENTICATION_FAILED" || error?.code === "UNAUTHORIZED";
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasSkippedRecoveryKeyDownload(userId: string) {
|
function hasSkippedRecoveryKeyDownload(userId: string) {
|
||||||
return document.cookie
|
return document.cookie
|
||||||
.split(";")
|
.split(";")
|
||||||
|
|
@ -62,7 +50,20 @@ export function LoginPage({ error }: LoginPageProps = {}) {
|
||||||
const errorCode = decodeLoginError(error);
|
const errorCode = decodeLoginError(error);
|
||||||
const errorDescription = errorCode ? getLoginErrorDescription(errorCode) : null;
|
const errorDescription = errorCode ? getLoginErrorDescription(errorCode) : null;
|
||||||
|
|
||||||
const navigateAfterLogin = useCallback(async () => {
|
useEffect(() => {
|
||||||
|
const autoSignIn = async () => {
|
||||||
|
if (
|
||||||
|
typeof PublicKeyCredential === "undefined" ||
|
||||||
|
!PublicKeyCredential.isConditionalMediationAvailable ||
|
||||||
|
!(await PublicKeyCredential.isConditionalMediationAvailable())
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await authClient.signIn.passkey({
|
||||||
|
autoFill: true,
|
||||||
|
fetchOptions: {
|
||||||
|
onResponse: async () => {
|
||||||
const session = await authClient.getSession();
|
const session = await authClient.getSession();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
@ -74,39 +75,13 @@ export function LoginPage({ error }: LoginPageProps = {}) {
|
||||||
} else {
|
} else {
|
||||||
void navigate({ to: "/volumes" });
|
void navigate({ to: "/volumes" });
|
||||||
}
|
}
|
||||||
}, [navigate]);
|
},
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const autoSignIn = async () => {
|
|
||||||
if (
|
|
||||||
typeof PublicKeyCredential === "undefined" ||
|
|
||||||
!PublicKeyCredential.isConditionalMediationAvailable ||
|
|
||||||
!(await PublicKeyCredential.isConditionalMediationAvailable())
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data, error } = await authClient.signIn.passkey({
|
|
||||||
autoFill: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isPasskeyVerificationFailure(error)) {
|
|
||||||
void navigate({
|
|
||||||
to: "/login",
|
|
||||||
search: {
|
|
||||||
error: PASSKEY_LOGIN_FAILED_ERROR,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
await navigateAfterLogin();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
void autoSignIn();
|
void autoSignIn();
|
||||||
}, [navigate, navigateAfterLogin]);
|
}, [navigate]);
|
||||||
|
|
||||||
const form = useForm<LoginFormValues>({
|
const form = useForm<LoginFormValues>({
|
||||||
resolver: zodResolver(loginSchema),
|
resolver: zodResolver(loginSchema),
|
||||||
|
|
@ -141,7 +116,12 @@ export function LoginPage({ error }: LoginPageProps = {}) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await navigateAfterLogin();
|
const d = await authClient.getSession();
|
||||||
|
if (data.user && !d.data?.user.hasDownloadedResticPassword && !hasSkippedRecoveryKeyDownload(data.user.id)) {
|
||||||
|
void navigate({ to: "/download-recovery-key" });
|
||||||
|
} else {
|
||||||
|
void navigate({ to: "/volumes" });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVerify2FA = async () => {
|
const handleVerify2FA = async () => {
|
||||||
|
|
@ -228,7 +208,6 @@ export function LoginPage({ error }: LoginPageProps = {}) {
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="trust-device"
|
id="trust-device"
|
||||||
aria-label="Trust this device for 30 days"
|
|
||||||
checked={trustDevice}
|
checked={trustDevice}
|
||||||
onChange={(e) => setTrustDevice(e.target.checked)}
|
onChange={(e) => setTrustDevice(e.target.checked)}
|
||||||
className="h-4 w-4"
|
className="h-4 w-4"
|
||||||
|
|
@ -326,7 +305,7 @@ export function LoginPage({ error }: LoginPageProps = {}) {
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<AlternativeSignInSection onPasskeySignIn={navigateAfterLogin} />
|
<SsoLoginSection />
|
||||||
|
|
||||||
<ResetPasswordDialog open={showResetDialog} onOpenChange={setShowResetDialog} />
|
<ResetPasswordDialog open={showResetDialog} onOpenChange={setShowResetDialog} />
|
||||||
</AuthLayout>
|
</AuthLayout>
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,9 @@ export function ScheduleDetailsPage(props: Props) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const searchParams = useSearch({ from: "/(dashboard)/backups/$backupId/" });
|
const searchParams = useSearch({ from: "/(dashboard)/backups/$backupId/" });
|
||||||
const [selectedSnapshotId, setSelectedSnapshotId] = useState<string | undefined>(initialSnapshotId);
|
const [selectedSnapshotId, setSelectedSnapshotId] = useState<string | undefined>(
|
||||||
|
initialSnapshotId ?? loaderData.snapshots?.at(-1)?.short_id,
|
||||||
|
);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
const [snapshotToDelete, setSnapshotToDelete] = useState<string | null>(null);
|
const [snapshotToDelete, setSnapshotToDelete] = useState<string | null>(null);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,6 @@ import {
|
||||||
} from "~/client/components/ui/dialog";
|
} from "~/client/components/ui/dialog";
|
||||||
import { Input } from "~/client/components/ui/input";
|
import { Input } from "~/client/components/ui/input";
|
||||||
import { Label } from "~/client/components/ui/label";
|
import { Label } from "~/client/components/ui/label";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
|
|
||||||
import { getIsSecureContext } from "~/client/functions/is-secure-context";
|
|
||||||
import { authClient } from "~/client/lib/auth-client";
|
import { authClient } from "~/client/lib/auth-client";
|
||||||
import { logger } from "~/client/lib/logger";
|
import { logger } from "~/client/lib/logger";
|
||||||
import { useTimeFormat } from "~/client/lib/datetime";
|
import { useTimeFormat } from "~/client/lib/datetime";
|
||||||
|
|
@ -40,7 +38,6 @@ type PasskeyEntry = {
|
||||||
|
|
||||||
export function PasskeysSection() {
|
export function PasskeysSection() {
|
||||||
const { formatDateTime } = useTimeFormat();
|
const { formatDateTime } = useTimeFormat();
|
||||||
const isSecureContext = getIsSecureContext();
|
|
||||||
const { data: passkeys, isPending } = useQuery({
|
const { data: passkeys, isPending } = useQuery({
|
||||||
queryKey: ["passkeys"],
|
queryKey: ["passkeys"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
|
@ -114,8 +111,6 @@ export function PasskeysSection() {
|
||||||
|
|
||||||
const handleAddPasskey = (e: React.ChangeEvent) => {
|
const handleAddPasskey = (e: React.ChangeEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!isSecureContext) return;
|
|
||||||
|
|
||||||
const name = newPasskeyName.trim() || undefined;
|
const name = newPasskeyName.trim() || undefined;
|
||||||
addPasskeyMutation.mutate(name);
|
addPasskeyMutation.mutate(name);
|
||||||
};
|
};
|
||||||
|
|
@ -154,19 +149,10 @@ export function PasskeysSection() {
|
||||||
Passkeys use your device's biometrics or screen lock instead of a password. They are
|
Passkeys use your device's biometrics or screen lock instead of a password. They are
|
||||||
phishing-resistant and cannot be reused across sites.
|
phishing-resistant and cannot be reused across sites.
|
||||||
</p>
|
</p>
|
||||||
<Tooltip>
|
<Button onClick={() => setAddDialogOpen(true)}>
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<span className="inline-flex">
|
|
||||||
<Button onClick={() => setAddDialogOpen(true)} disabled={!isSecureContext}>
|
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
Add passkey
|
Add passkey
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className={cn({ hidden: isSecureContext })}>
|
|
||||||
<p>Passkeys can only be added over HTTPS or from localhost.</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className={cn("text-sm text-muted-foreground", { hidden: !isPending })}>Loading passkeys...</p>
|
<p className={cn("text-sm text-muted-foreground", { hidden: !isPending })}>Loading passkeys...</p>
|
||||||
|
|
@ -247,7 +233,7 @@ export function PasskeysSection() {
|
||||||
<Button type="button" variant="outline" onClick={() => setAddDialogOpen(false)}>
|
<Button type="button" variant="outline" onClick={() => setAddDialogOpen(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" loading={addPasskeyMutation.isPending} disabled={!isSecureContext}>
|
<Button type="submit" loading={addPasskeyMutation.isPending}>
|
||||||
<Fingerprint className="h-4 w-4 mr-2" />
|
<Fingerprint className="h-4 w-4 mr-2" />
|
||||||
Add passkey
|
Add passkey
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Button } from "~/client/components/ui/button";
|
|
||||||
import { authClient } from "~/client/lib/auth-client";
|
|
||||||
import { logger } from "~/client/lib/logger";
|
|
||||||
|
|
||||||
type SsoProvider = {
|
|
||||||
providerId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SsoLoginButtonsProps = {
|
|
||||||
providers: SsoProvider[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function SsoLoginButtons({ providers }: SsoLoginButtonsProps) {
|
|
||||||
const ssoLoginMutation = useMutation({
|
|
||||||
mutationFn: async (providerId: string) => {
|
|
||||||
const callbackPath = "/login";
|
|
||||||
const { data, error } = await authClient.signIn.sso({
|
|
||||||
providerId: providerId,
|
|
||||||
callbackURL: callbackPath,
|
|
||||||
errorCallbackURL: "/api/v1/auth/login-error",
|
|
||||||
});
|
|
||||||
if (error) throw error;
|
|
||||||
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
onSuccess: (data) => {
|
|
||||||
window.location.href = data.url;
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
logger.error(error);
|
|
||||||
toast.error("SSO Login failed", { description: error.message });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{providers.map((provider) => (
|
|
||||||
<Button
|
|
||||||
key={provider.providerId}
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className="w-full"
|
|
||||||
loading={ssoLoginMutation.isPending}
|
|
||||||
disabled={ssoLoginMutation.isPending}
|
|
||||||
onClick={() => ssoLoginMutation.mutate(provider.providerId)}
|
|
||||||
>
|
|
||||||
Log in with {provider.providerId}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
58
app/client/modules/sso/components/sso-login-section.tsx
Normal file
58
app/client/modules/sso/components/sso-login-section.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Button } from "~/client/components/ui/button";
|
||||||
|
import { authClient } from "~/client/lib/auth-client";
|
||||||
|
import { logger } from "~/client/lib/logger";
|
||||||
|
import { getPublicSsoProvidersOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
export function SsoLoginSection() {
|
||||||
|
const { data: ssoProviders } = useSuspenseQuery({
|
||||||
|
...getPublicSsoProvidersOptions(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ssoLoginMutation = useMutation({
|
||||||
|
mutationFn: async (providerId: string) => {
|
||||||
|
const callbackPath = "/login";
|
||||||
|
const { data, error } = await authClient.signIn.sso({
|
||||||
|
providerId: providerId,
|
||||||
|
callbackURL: callbackPath,
|
||||||
|
errorCallbackURL: "/api/v1/auth/login-error",
|
||||||
|
});
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
window.location.href = data.url;
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
logger.error(error);
|
||||||
|
toast.error("SSO Login failed", { description: error.message });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ssoProviders.providers.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pt-4 border-t border-border/60 space-y-3">
|
||||||
|
<p className="text-sm font-medium">Alternative Sign-in</p>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{ssoProviders.providers.map((provider) => (
|
||||||
|
<Button
|
||||||
|
key={provider.providerId}
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
loading={ssoLoginMutation.isPending}
|
||||||
|
disabled={ssoLoginMutation.isPending}
|
||||||
|
onClick={() => ssoLoginMutation.mutate(provider.providerId)}
|
||||||
|
>
|
||||||
|
Log in with {provider.providerId}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -101,7 +101,6 @@ export const NFSForm = ({ form }: Props) => {
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
aria-label="Mount volume as read-only"
|
|
||||||
checked={field.value ?? false}
|
checked={field.value ?? false}
|
||||||
onChange={(e) => field.onChange(e.target.checked)}
|
onChange={(e) => field.onChange(e.target.checked)}
|
||||||
className="rounded border-gray-300"
|
className="rounded border-gray-300"
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,6 @@ export const RcloneForm = ({ form }: Props) => {
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
aria-label="Mount volume as read-only"
|
|
||||||
checked={field.value ?? false}
|
checked={field.value ?? false}
|
||||||
onChange={(e) => field.onChange(e.target.checked)}
|
onChange={(e) => field.onChange(e.target.checked)}
|
||||||
className="rounded border-gray-300"
|
className="rounded border-gray-300"
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,6 @@ export const SMBForm = ({ form }: Props) => {
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
aria-label="Connect as guest"
|
|
||||||
checked={field.value ?? false}
|
checked={field.value ?? false}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
field.onChange(e.target.checked);
|
field.onChange(e.target.checked);
|
||||||
|
|
@ -180,7 +179,6 @@ export const SMBForm = ({ form }: Props) => {
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
aria-label="Map all files to container user/group"
|
|
||||||
checked={field.value ?? false}
|
checked={field.value ?? false}
|
||||||
onChange={(e) => field.onChange(e.target.checked)}
|
onChange={(e) => field.onChange(e.target.checked)}
|
||||||
className="rounded border-gray-300"
|
className="rounded border-gray-300"
|
||||||
|
|
@ -189,8 +187,8 @@ export const SMBForm = ({ form }: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Keep the old behavior by forcing the SMB mount to present every file and directory as owned
|
Keep the old behavior by forcing the SMB mount to present every file and directory as owned by the
|
||||||
by the container user and group instead of using server reported ownership.
|
container user and group instead of using server reported ownership.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
@ -207,7 +205,6 @@ export const SMBForm = ({ form }: Props) => {
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
aria-label="Mount volume as read-only"
|
|
||||||
checked={field.value ?? false}
|
checked={field.value ?? false}
|
||||||
onChange={(e) => field.onChange(e.target.checked)}
|
onChange={(e) => field.onChange(e.target.checked)}
|
||||||
className="rounded border-gray-300"
|
className="rounded border-gray-300"
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,6 @@ export const WebDAVForm = ({ form }: Props) => {
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
aria-label="Enable HTTPS for secure connections"
|
|
||||||
checked={field.value ?? false}
|
checked={field.value ?? false}
|
||||||
onChange={(e) => field.onChange(e.target.checked)}
|
onChange={(e) => field.onChange(e.target.checked)}
|
||||||
className="rounded border-gray-300"
|
className="rounded border-gray-300"
|
||||||
|
|
@ -129,7 +128,6 @@ export const WebDAVForm = ({ form }: Props) => {
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
aria-label="Mount volume as read-only"
|
|
||||||
checked={field.value ?? false}
|
checked={field.value ?? false}
|
||||||
onChange={(e) => field.onChange(e.target.checked)}
|
onChange={(e) => field.onChange(e.target.checked)}
|
||||||
className="rounded border-gray-300"
|
className="rounded border-gray-300"
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
export const PASSKEY_LOGIN_FAILED_ERROR = "PASSKEY_LOGIN_FAILED";
|
|
||||||
|
|
||||||
export const LOGIN_ERROR_CODES = [
|
export const LOGIN_ERROR_CODES = [
|
||||||
"ACCOUNT_LINK_REQUIRED",
|
"ACCOUNT_LINK_REQUIRED",
|
||||||
"EMAIL_NOT_VERIFIED",
|
"EMAIL_NOT_VERIFIED",
|
||||||
"INVITE_REQUIRED",
|
"INVITE_REQUIRED",
|
||||||
"BANNED_USER",
|
"BANNED_USER",
|
||||||
"SSO_LOGIN_FAILED",
|
"SSO_LOGIN_FAILED",
|
||||||
PASSKEY_LOGIN_FAILED_ERROR,
|
|
||||||
"ERROR_INVALID_RP_ID",
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type LoginErrorCode = (typeof LOGIN_ERROR_CODES)[number];
|
export type LoginErrorCode = (typeof LOGIN_ERROR_CODES)[number];
|
||||||
|
|
@ -24,9 +20,5 @@ export function getLoginErrorDescription(errorCode: LoginErrorCode): string {
|
||||||
return "You have been banned from this application. Please contact support if you believe this is an error.";
|
return "You have been banned from this application. Please contact support if you believe this is an error.";
|
||||||
case "SSO_LOGIN_FAILED":
|
case "SSO_LOGIN_FAILED":
|
||||||
return "SSO authentication failed. Please try again.";
|
return "SSO authentication failed. Please try again.";
|
||||||
case PASSKEY_LOGIN_FAILED_ERROR:
|
|
||||||
return "Passkey sign-in failed. The passkey didn't verify your identity with a PIN, biometrics, or screen lock. Please use a verified passkey or sign in with your password.";
|
|
||||||
case "ERROR_INVALID_RP_ID":
|
|
||||||
return "You can only sign in with a passkey on the domain set by the BASE_URL environment variable";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -134,37 +134,6 @@ describe("RepositoryMutex", () => {
|
||||||
expect(repoMutex.isLocked(repoId)).toBe(false);
|
expect(repoMutex.isLocked(repoId)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should reject aborted queued acquisitions only after waiter cleanup finishes", async () => {
|
|
||||||
const repoId = "abort-waits-for-cleanup";
|
|
||||||
const releaseHolder = await repoMutex.acquireExclusive(repoId, "holder");
|
|
||||||
const controller = new AbortController();
|
|
||||||
const waitingAcquisition = repoMutex.acquireShared(repoId, "waiter", controller.signal);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
||||||
|
|
||||||
const waitersBeforeAbort = await db.query.repositoryLockWaitersTable.findMany({
|
|
||||||
where: { repositoryId: { eq: repoId } },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(waitersBeforeAbort.map((waiter) => waiter.operation)).toEqual(["waiter"]);
|
|
||||||
|
|
||||||
controller.abort(new Error("stop"));
|
|
||||||
await expect(waitingAcquisition).rejects.toThrow("stop");
|
|
||||||
|
|
||||||
const waitersAfterAbort = await db.query.repositoryLockWaitersTable.findMany({
|
|
||||||
where: { repositoryId: { eq: repoId } },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(waitersAfterAbort).toEqual([]);
|
|
||||||
expect(repoMutex.isLocked(repoId)).toBe(true);
|
|
||||||
} finally {
|
|
||||||
releaseHolder();
|
|
||||||
await db.delete(repositoryLockWaitersTable).where(eq(repositoryLockWaitersTable.repositoryId, repoId));
|
|
||||||
await db.delete(repositoryLocksTable).where(eq(repositoryLocksTable.repositoryId, repoId));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should allow concurrent shared locks", async () => {
|
test("should allow concurrent shared locks", async () => {
|
||||||
const repoId = "concurrent-shared";
|
const repoId = "concurrent-shared";
|
||||||
const release1 = await repoMutex.acquireShared(repoId, "op1");
|
const release1 = await repoMutex.acquireShared(repoId, "op1");
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,6 @@ const envSchema = z
|
||||||
MIGRATIONS_PATH: z.string().optional(),
|
MIGRATIONS_PATH: z.string().optional(),
|
||||||
APP_VERSION: z.string().default("dev"),
|
APP_VERSION: z.string().default("dev"),
|
||||||
TRUSTED_ORIGINS: z.string().optional(),
|
TRUSTED_ORIGINS: z.string().optional(),
|
||||||
PORTLESS_URL: z.string().optional(),
|
|
||||||
PORTLESS_TAILSCALE_URL: z.string().optional(),
|
|
||||||
TRUST_PROXY: z.string().default("false"),
|
TRUST_PROXY: z.string().default("false"),
|
||||||
DISABLE_RATE_LIMITING: z.string().default("false"),
|
DISABLE_RATE_LIMITING: z.string().default("false"),
|
||||||
APP_SECRET: z.preprocess((value) => (value === "" ? undefined : value), z.string().min(32).max(256).optional()),
|
APP_SECRET: z.preprocess((value) => (value === "" ? undefined : value), z.string().min(32).max(256).optional()),
|
||||||
|
|
@ -31,24 +29,10 @@ const envSchema = z
|
||||||
PROVISIONING_PATH: z.string().optional(),
|
PROVISIONING_PATH: z.string().optional(),
|
||||||
})
|
})
|
||||||
.transform((s, ctx) => {
|
.transform((s, ctx) => {
|
||||||
let baseUrl = unquote(s.BASE_URL);
|
const baseUrl = unquote(s.BASE_URL);
|
||||||
const trustedOrigins = s.TRUSTED_ORIGINS?.split(",").map(unquote).filter(Boolean) ?? [];
|
const trustedOrigins = s.TRUSTED_ORIGINS?.split(",").map(unquote).filter(Boolean).concat(baseUrl) ?? [baseUrl];
|
||||||
|
|
||||||
if (s.NODE_ENV === "development") {
|
|
||||||
if (s.PORTLESS_URL) {
|
|
||||||
trustedOrigins.push(unquote(s.PORTLESS_URL));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s.PORTLESS_TAILSCALE_URL) {
|
|
||||||
baseUrl = unquote(s.PORTLESS_TAILSCALE_URL);
|
|
||||||
trustedOrigins.push(baseUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trustedOrigins.push(baseUrl);
|
|
||||||
const uniqueTrustedOrigins = Array.from(new Set(trustedOrigins));
|
|
||||||
const webhookAllowedOrigins = s.WEBHOOK_ALLOWED_ORIGINS?.split(",").map(unquote).filter(Boolean) ?? [];
|
const webhookAllowedOrigins = s.WEBHOOK_ALLOWED_ORIGINS?.split(",").map(unquote).filter(Boolean) ?? [];
|
||||||
const authOrigins = [baseUrl, ...uniqueTrustedOrigins];
|
const authOrigins = [baseUrl, ...trustedOrigins];
|
||||||
const { allowedHosts, invalidOrigins } = buildAllowedHosts(authOrigins);
|
const { allowedHosts, invalidOrigins } = buildAllowedHosts(authOrigins);
|
||||||
let appSecret = s.APP_SECRET;
|
let appSecret = s.APP_SECRET;
|
||||||
|
|
||||||
|
|
@ -124,7 +108,7 @@ const envSchema = z
|
||||||
port: s.PORT,
|
port: s.PORT,
|
||||||
migrationsPath: s.MIGRATIONS_PATH,
|
migrationsPath: s.MIGRATIONS_PATH,
|
||||||
appVersion: s.APP_VERSION,
|
appVersion: s.APP_VERSION,
|
||||||
trustedOrigins: uniqueTrustedOrigins,
|
trustedOrigins: trustedOrigins,
|
||||||
trustProxy: s.TRUST_PROXY === "true",
|
trustProxy: s.TRUST_PROXY === "true",
|
||||||
appSecret: appSecret ?? "",
|
appSecret: appSecret ?? "",
|
||||||
baseUrl,
|
baseUrl,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import {
|
||||||
type RepositoryLock,
|
type RepositoryLock,
|
||||||
type RepositoryLockWaiter,
|
type RepositoryLockWaiter,
|
||||||
} from "../db/schema";
|
} from "../db/schema";
|
||||||
import { Effect, Exit, Fiber, Schedule, Scope } from "effect";
|
|
||||||
|
|
||||||
type LockType = "shared" | "exclusive";
|
type LockType = "shared" | "exclusive";
|
||||||
|
|
||||||
|
|
@ -34,30 +33,59 @@ const LOCK_HEARTBEAT_MS = 5_000;
|
||||||
const LOCK_POLL_MS = 250;
|
const LOCK_POLL_MS = 250;
|
||||||
const LOCK_POLL_CLEANUP_MS = 5_000;
|
const LOCK_POLL_CLEANUP_MS = 5_000;
|
||||||
|
|
||||||
const REPOSITORY_MUTEX_INSTANCE = Symbol.for("zerobyte.repositoryMutex.instance");
|
|
||||||
function getRepositoryMutex() {
|
|
||||||
const globalObject = globalThis as typeof globalThis & Record<symbol, RepositoryMutex | undefined>;
|
|
||||||
const mutex = globalObject[REPOSITORY_MUTEX_INSTANCE];
|
|
||||||
|
|
||||||
if (mutex) return mutex;
|
|
||||||
|
|
||||||
const newMutex = new RepositoryMutex();
|
|
||||||
globalObject[REPOSITORY_MUTEX_INSTANCE] = newMutex;
|
|
||||||
return newMutex;
|
|
||||||
}
|
|
||||||
|
|
||||||
class RepositoryMutex {
|
class RepositoryMutex {
|
||||||
private ownerId = `owner_${Bun.randomUUIDv7()}`;
|
private ownerId = `owner_${Bun.randomUUIDv7()}`;
|
||||||
|
private heartbeatTimers = new Map<string, ReturnType<typeof setInterval>>();
|
||||||
private nextPollCleanupAt = 0;
|
private nextPollCleanupAt = 0;
|
||||||
|
|
||||||
private generateLockId(): string {
|
private generateLockId(): string {
|
||||||
return `lock_${Bun.randomUUIDv7()}`;
|
return `lock_${Bun.randomUUIDv7()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private abortReason(signal: AbortSignal): Error {
|
private abortReason(signal: AbortSignal) {
|
||||||
return signal.reason || new Error("Operation aborted");
|
return signal.reason || new Error("Operation aborted");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private throwIfAborted(signal?: AbortSignal) {
|
||||||
|
if (signal?.aborted) {
|
||||||
|
throw this.abortReason(signal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private releaseIfAborted(releaseLock: () => void, signal?: AbortSignal) {
|
||||||
|
if (!signal?.aborted) return;
|
||||||
|
releaseLock();
|
||||||
|
throw this.abortReason(signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private waitForPoll(signal?: AbortSignal) {
|
||||||
|
this.throwIfAborted(signal);
|
||||||
|
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
let settled = false;
|
||||||
|
const timeout = setTimeout(() => settle(resolve), LOCK_POLL_MS);
|
||||||
|
|
||||||
|
const onAbort = () => {
|
||||||
|
settle(() => reject(this.abortReason(signal!)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
signal?.removeEventListener("abort", onAbort);
|
||||||
|
};
|
||||||
|
|
||||||
|
const settle = (callback: () => void) => {
|
||||||
|
if (settled) return;
|
||||||
|
|
||||||
|
settled = true;
|
||||||
|
cleanup();
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
|
||||||
|
signal?.addEventListener("abort", onAbort, { once: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private cleanupExpired(tx: RepositoryMutexTransaction, now: number) {
|
private cleanupExpired(tx: RepositoryMutexTransaction, now: number) {
|
||||||
tx.delete(repositoryLocksTable).where(lte(repositoryLocksTable.expiresAt, now)).run();
|
tx.delete(repositoryLocksTable).where(lte(repositoryLocksTable.expiresAt, now)).run();
|
||||||
tx.delete(repositoryLockWaitersTable).where(lte(repositoryLockWaitersTable.expiresAt, now)).run();
|
tx.delete(repositoryLockWaitersTable).where(lte(repositoryLockWaitersTable.expiresAt, now)).run();
|
||||||
|
|
@ -153,19 +181,20 @@ class RepositoryMutex {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private tryAcquireImmediately(request: LockRequest) {
|
private tryAcquireImmediately(request: LockRequest, signal?: AbortSignal) {
|
||||||
return Effect.gen(this, function* () {
|
|
||||||
const locks = this.tryAcquireManyRows([request]);
|
const locks = this.tryAcquireManyRows([request]);
|
||||||
if (!locks || locks.length === 0) return null;
|
if (!locks || locks.length === 0) return null;
|
||||||
|
|
||||||
const [lock] = locks;
|
const [lock] = locks;
|
||||||
return yield* this.createRelease(lock);
|
const releaseLock = this.createRelease(lock);
|
||||||
});
|
this.releaseIfAborted(releaseLock, signal);
|
||||||
|
|
||||||
|
return releaseLock;
|
||||||
}
|
}
|
||||||
|
|
||||||
private createWaiter(request: LockRequest, waiterId: string) {
|
private createWaiter(request: LockRequest, waiterId: string) {
|
||||||
return Effect.sync(() => {
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
db.transaction((tx) => {
|
db.transaction((tx) => {
|
||||||
this.cleanupExpired(tx, now);
|
this.cleanupExpired(tx, now);
|
||||||
tx.insert(repositoryLockWaitersTable)
|
tx.insert(repositoryLockWaitersTable)
|
||||||
|
|
@ -181,21 +210,14 @@ class RepositoryMutex {
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private deleteWaiter(waiterId: string) {
|
private deleteWaiter(waiterId: string) {
|
||||||
return Effect.sync(() =>
|
db.delete(repositoryLockWaitersTable)
|
||||||
db
|
|
||||||
.delete(repositoryLockWaitersTable)
|
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(eq(repositoryLockWaitersTable.id, waiterId), eq(repositoryLockWaitersTable.ownerId, this.ownerId)),
|
||||||
eq(repositoryLockWaitersTable.id, waiterId),
|
|
||||||
eq(repositoryLockWaitersTable.ownerId, this.ownerId),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.run(),
|
.run();
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private deleteWaiterRow(tx: RepositoryMutexTransaction, waiterId: string): void {
|
private deleteWaiterRow(tx: RepositoryMutexTransaction, waiterId: string): void {
|
||||||
|
|
@ -271,205 +293,73 @@ class RepositoryMutex {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private waitForQueuedLock(request: LockRequest) {
|
private async waitForQueuedLock(request: LockRequest, signal?: AbortSignal) {
|
||||||
|
this.throwIfAborted(signal);
|
||||||
|
|
||||||
const waiterId = this.generateLockId();
|
const waiterId = this.generateLockId();
|
||||||
|
this.createWaiter(request, waiterId);
|
||||||
|
this.startHeartbeat("waiter", waiterId);
|
||||||
|
|
||||||
const attempt = Effect.sync(() => this.tryPromoteWaiter(waiterId)).pipe(
|
try {
|
||||||
Effect.flatMap((attempt) => {
|
while (true) {
|
||||||
|
this.throwIfAborted(signal);
|
||||||
|
|
||||||
|
const attempt = this.tryPromoteWaiter(waiterId);
|
||||||
if (attempt.status === "acquired") {
|
if (attempt.status === "acquired") {
|
||||||
return Effect.succeed(attempt.lock);
|
this.stopHeartbeat(waiterId);
|
||||||
}
|
const releaseLock = this.createRelease(attempt.lock);
|
||||||
|
this.releaseIfAborted(releaseLock, signal);
|
||||||
|
|
||||||
if (attempt.status === "missing") {
|
|
||||||
return Effect.gen(this, function* () {
|
|
||||||
yield* this.createWaiter(request, waiterId);
|
|
||||||
yield* this.startHeartbeat("waiter", waiterId);
|
|
||||||
|
|
||||||
return yield* Effect.fail("retry");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Effect.fail("retry");
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const cleanupAbandonedWaiter = Effect.gen(this, function* () {
|
|
||||||
yield* this.deleteWaiter(waiterId);
|
|
||||||
yield* this.release({ id: waiterId });
|
|
||||||
});
|
|
||||||
|
|
||||||
return Effect.scoped(
|
|
||||||
Effect.gen(this, function* () {
|
|
||||||
const lock = yield* attempt.pipe(
|
|
||||||
Effect.retry(Schedule.spaced(LOCK_POLL_MS)),
|
|
||||||
Effect.onExit((exit) => {
|
|
||||||
if (Exit.isSuccess(exit)) {
|
|
||||||
return Effect.void;
|
|
||||||
}
|
|
||||||
|
|
||||||
return cleanupAbandonedWaiter;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return yield* this.createRelease(lock);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
isLocked(repositoryId: string) {
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
return db.transaction((tx) => {
|
|
||||||
this.cleanupExpired(tx, now);
|
|
||||||
return this.getActiveLocks(tx, repositoryId, now).length > 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private createReleaseMany(locks: AcquiredLock[]) {
|
|
||||||
return Effect.gen(this, function* () {
|
|
||||||
const releases = yield* Effect.all(locks.map((lock) => this.createRelease(lock)));
|
|
||||||
let released = false;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (released) return;
|
|
||||||
|
|
||||||
released = true;
|
|
||||||
for (const release of releases.toReversed()) {
|
|
||||||
release();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private createRelease(lock: AcquiredLock) {
|
|
||||||
return Effect.gen(this, function* () {
|
|
||||||
const heartbeatFiber = yield* this.startHeartbeat("lock", lock.id);
|
|
||||||
let released = false;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (released) return;
|
|
||||||
|
|
||||||
released = true;
|
|
||||||
Effect.runFork(Fiber.interrupt(heartbeatFiber));
|
|
||||||
Effect.runSync(this.release(lock));
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private release(lock: Pick<AcquiredLock, "id">) {
|
|
||||||
return Effect.gen(this, function* () {
|
|
||||||
const releasedLock = yield* Effect.sync(() =>
|
|
||||||
db.transaction((tx) => {
|
|
||||||
const row = tx.query.repositoryLocksTable
|
|
||||||
.findFirst({ where: { AND: [{ id: { eq: lock.id } }, { ownerId: { eq: this.ownerId } }] } })
|
|
||||||
.sync();
|
|
||||||
|
|
||||||
if (!row) return null;
|
|
||||||
|
|
||||||
tx.delete(repositoryLocksTable)
|
|
||||||
.where(
|
|
||||||
and(eq(repositoryLocksTable.id, lock.id), eq(repositoryLocksTable.ownerId, this.ownerId)),
|
|
||||||
)
|
|
||||||
.run();
|
|
||||||
|
|
||||||
return row;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!releasedLock) return;
|
|
||||||
|
|
||||||
const duration = Date.now() - releasedLock.acquiredAt;
|
|
||||||
|
|
||||||
yield* logger.effect.debug(
|
|
||||||
`[Mutex] Released ${releasedLock.type} lock for repo ${releasedLock.repositoryId}: ${releasedLock.operation} (held for ${duration}ms)`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private startHeartbeat(
|
|
||||||
target: "waiter",
|
|
||||||
lockId: string,
|
|
||||||
): Effect.Effect<Fiber.RuntimeFiber<void, never>, never, Scope.Scope>;
|
|
||||||
private startHeartbeat(
|
|
||||||
target: "lock",
|
|
||||||
lockId: string,
|
|
||||||
): Effect.Effect<Fiber.RuntimeFiber<void, never>, never, never>;
|
|
||||||
private startHeartbeat(
|
|
||||||
target: HeartbeatTarget,
|
|
||||||
lockId: string,
|
|
||||||
): Effect.Effect<Fiber.RuntimeFiber<unknown, never>, never, Scope.Scope> {
|
|
||||||
const heartbeat = Effect.gen(this, function* () {
|
|
||||||
const now = Date.now();
|
|
||||||
const values = { heartbeatAt: now, expiresAt: now + LOCK_LEASE_MS };
|
|
||||||
|
|
||||||
if (target === "lock") {
|
|
||||||
yield* Effect.try(() => {
|
|
||||||
db.update(repositoryLocksTable)
|
|
||||||
.set(values)
|
|
||||||
.where(and(eq(repositoryLocksTable.id, lockId), eq(repositoryLocksTable.ownerId, this.ownerId)))
|
|
||||||
.run();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
yield* Effect.try(() => {
|
|
||||||
db.update(repositoryLockWaitersTable)
|
|
||||||
.set(values)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(repositoryLockWaitersTable.id, lockId),
|
|
||||||
eq(repositoryLockWaitersTable.ownerId, this.ownerId),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.run();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).pipe(
|
|
||||||
Effect.catchAll((error) =>
|
|
||||||
logger.effect.warn(`[Mutex] Failed to heartbeat ${target} ${lockId}: ${String(error)}`),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const repeat = heartbeat.pipe(Effect.repeat(Schedule.spaced(LOCK_HEARTBEAT_MS)));
|
|
||||||
|
|
||||||
if (target === "waiter") {
|
|
||||||
// For waiters, we can stop heartbeating when the releaser is dropped, so we use a scoped fiber
|
|
||||||
return repeat.pipe(Effect.forkScoped);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For locks, the heartbeat must outlive the acquire scope.
|
|
||||||
// It is interrupted manually by the returned release function.
|
|
||||||
// TODO: max lifetime for lock heartbeats to prevent leaks if the releaser is never called?
|
|
||||||
return repeat.pipe(Effect.forkDaemon);
|
|
||||||
}
|
|
||||||
|
|
||||||
private acquireSharedEffect(repositoryId: string, operation: string) {
|
|
||||||
return Effect.gen(this, function* () {
|
|
||||||
const request: LockRequest = { repositoryId, type: "shared", operation };
|
|
||||||
const releaseLock = yield* this.tryAcquireImmediately(request);
|
|
||||||
if (releaseLock) return releaseLock;
|
|
||||||
|
|
||||||
yield* logger.effect.debug(`[Mutex] Waiting for shared lock on repo ${repositoryId}: ${operation}`);
|
|
||||||
return yield* this.waitForQueuedLock(request);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private acquireExclusiveEffect(repositoryId: string, operation: string) {
|
|
||||||
return Effect.gen(this, function* () {
|
|
||||||
const request: LockRequest = { repositoryId, type: "exclusive", operation };
|
|
||||||
const releaseLock = yield* this.tryAcquireImmediately(request);
|
|
||||||
if (releaseLock) {
|
|
||||||
yield* logger.effect.debug(`[Mutex] Acquired exclusive lock for repo ${repositoryId}: ${operation}`);
|
|
||||||
return releaseLock;
|
return releaseLock;
|
||||||
}
|
}
|
||||||
|
|
||||||
yield* logger.effect.debug(`[Mutex] Waiting for exclusive lock on repo ${repositoryId}: ${operation}`);
|
if (attempt.status === "missing") {
|
||||||
const queuedReleaseLock = yield* this.waitForQueuedLock(request);
|
this.createWaiter(request, waiterId);
|
||||||
yield* logger.effect.debug(`[Mutex] Acquired exclusive lock for repo ${repositoryId}: ${operation}`);
|
this.startHeartbeat("waiter", waiterId);
|
||||||
return queuedReleaseLock;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private acquireManyEffect(requests: LockRequest[]) {
|
await this.waitForPoll(signal);
|
||||||
return Effect.gen(this, function* () {
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.stopHeartbeat(waiterId);
|
||||||
|
this.deleteWaiter(waiterId);
|
||||||
|
this.release({ id: waiterId });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async acquireShared(repositoryId: string, operation: string, signal?: AbortSignal) {
|
||||||
|
this.throwIfAborted(signal);
|
||||||
|
|
||||||
|
const request: LockRequest = { repositoryId, type: "shared", operation };
|
||||||
|
const releaseLock = this.tryAcquireImmediately(request, signal);
|
||||||
|
if (releaseLock) {
|
||||||
|
return releaseLock;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`[Mutex] Waiting for shared lock on repo ${repositoryId}: ${operation}`);
|
||||||
|
return await this.waitForQueuedLock(request, signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
async acquireExclusive(repositoryId: string, operation: string, signal?: AbortSignal) {
|
||||||
|
this.throwIfAborted(signal);
|
||||||
|
|
||||||
|
const request: LockRequest = { repositoryId, type: "exclusive", operation };
|
||||||
|
const releaseLock = this.tryAcquireImmediately(request, signal);
|
||||||
|
if (releaseLock) {
|
||||||
|
logger.debug(`[Mutex] Acquired exclusive lock for repo ${repositoryId}: ${operation}`);
|
||||||
|
return releaseLock;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`[Mutex] Waiting for exclusive lock on repo ${repositoryId}: ${operation}`);
|
||||||
|
const queuedReleaseLock = await this.waitForQueuedLock(request, signal);
|
||||||
|
logger.debug(`[Mutex] Acquired exclusive lock for repo ${repositoryId}: ${operation}`);
|
||||||
|
return queuedReleaseLock;
|
||||||
|
}
|
||||||
|
|
||||||
|
async acquireMany(requests: LockRequest[], signal?: AbortSignal) {
|
||||||
|
this.throwIfAborted(signal);
|
||||||
|
|
||||||
if (requests.length === 0) {
|
if (requests.length === 0) {
|
||||||
return () => {};
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
@ -483,79 +373,124 @@ class RepositoryMutex {
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedRequests = [...requests].sort((a, b) => a.repositoryId.localeCompare(b.repositoryId));
|
const sortedRequests = [...requests].sort((a, b) => a.repositoryId.localeCompare(b.repositoryId));
|
||||||
|
while (true) {
|
||||||
|
const locks = this.tryAcquireManyRows(sortedRequests);
|
||||||
|
if (locks) {
|
||||||
|
const releaseLocks = this.createReleaseMany(locks);
|
||||||
|
this.releaseIfAborted(releaseLocks, signal);
|
||||||
|
|
||||||
const locks = yield* Effect.sync(() => this.tryAcquireManyRows(sortedRequests)).pipe(
|
return releaseLocks;
|
||||||
Effect.flatMap((locks) => {
|
}
|
||||||
if (locks) return Effect.succeed(locks);
|
|
||||||
return Effect.fail("retry");
|
|
||||||
}),
|
|
||||||
Effect.retry(Schedule.spaced(LOCK_POLL_MS)),
|
|
||||||
);
|
|
||||||
|
|
||||||
return yield* this.createReleaseMany(locks);
|
await this.waitForPoll(signal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isLocked(repositoryId: string) {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
return db.transaction((tx) => {
|
||||||
|
this.cleanupExpired(tx, now);
|
||||||
|
return this.getActiveLocks(tx, repositoryId, now).length > 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private runWithSignal<A, E>(effect: Effect.Effect<A, E>, signal?: AbortSignal) {
|
private createReleaseMany(locks: AcquiredLock[]) {
|
||||||
if (!signal) return Effect.runPromise(effect);
|
const releases = locks.map((lock) => this.createRelease(lock));
|
||||||
|
let released = false;
|
||||||
|
|
||||||
if (signal.aborted) {
|
return () => {
|
||||||
return Promise.reject(this.abortReason(signal));
|
if (released) return;
|
||||||
|
|
||||||
|
released = true;
|
||||||
|
for (const release of releases.toReversed()) {
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise<A>((resolve, reject) => {
|
private createRelease(lock: AcquiredLock) {
|
||||||
const fiber = Effect.runFork(effect);
|
this.startHeartbeat("lock", lock.id);
|
||||||
let settled = false;
|
let released = false;
|
||||||
let aborting = false;
|
|
||||||
|
|
||||||
const complete = (callback: () => void) => {
|
return () => {
|
||||||
if (settled) return;
|
if (released) return;
|
||||||
|
|
||||||
settled = true;
|
released = true;
|
||||||
signal.removeEventListener("abort", onAbort);
|
this.stopHeartbeat(lock.id);
|
||||||
callback();
|
this.release(lock);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private release(lock: Pick<AcquiredLock, "id">) {
|
||||||
|
const releasedLock = db.transaction((tx) => {
|
||||||
|
const row = tx.query.repositoryLocksTable
|
||||||
|
.findFirst({ where: { AND: [{ id: { eq: lock.id } }, { ownerId: { eq: this.ownerId } }] } })
|
||||||
|
.sync();
|
||||||
|
|
||||||
|
if (!row) return null;
|
||||||
|
|
||||||
|
tx.delete(repositoryLocksTable)
|
||||||
|
.where(and(eq(repositoryLocksTable.id, lock.id), eq(repositoryLocksTable.ownerId, this.ownerId)))
|
||||||
|
.run();
|
||||||
|
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!releasedLock) return;
|
||||||
|
|
||||||
|
const duration = Date.now() - releasedLock.acquiredAt;
|
||||||
|
logger.debug(
|
||||||
|
`[Mutex] Released ${releasedLock.type} lock for repo ${releasedLock.repositoryId}: ${releasedLock.operation} (held for ${duration}ms)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private startHeartbeat(target: HeartbeatTarget, lockId: string) {
|
||||||
|
this.stopHeartbeat(lockId);
|
||||||
|
|
||||||
|
const heartbeat = () => {
|
||||||
|
const now = Date.now();
|
||||||
|
const values = { heartbeatAt: now, expiresAt: now + LOCK_LEASE_MS };
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (target === "lock") {
|
||||||
|
db.update(repositoryLocksTable)
|
||||||
|
.set(values)
|
||||||
|
.where(and(eq(repositoryLocksTable.id, lockId), eq(repositoryLocksTable.ownerId, this.ownerId)))
|
||||||
|
.run();
|
||||||
|
} else {
|
||||||
|
db.update(repositoryLockWaitersTable)
|
||||||
|
.set(values)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(repositoryLockWaitersTable.id, lockId),
|
||||||
|
eq(repositoryLockWaitersTable.ownerId, this.ownerId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`[Mutex] Failed to heartbeat ${target} ${lockId}: ${String(error)}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAbort = () => {
|
const timer = setInterval(heartbeat, LOCK_HEARTBEAT_MS);
|
||||||
aborting = true;
|
if (timer && "unref" in timer) {
|
||||||
Effect.runPromise(Fiber.interrupt(fiber)).then(
|
timer.unref();
|
||||||
(exit) =>
|
}
|
||||||
complete(() => {
|
|
||||||
if (Exit.isSuccess(exit)) {
|
this.heartbeatTimers.set(lockId, timer);
|
||||||
resolve(exit.value);
|
}
|
||||||
|
|
||||||
|
private stopHeartbeat(lockId: string) {
|
||||||
|
const timer = this.heartbeatTimers.get(lockId);
|
||||||
|
if (!timer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
reject(this.abortReason(signal));
|
clearInterval(timer);
|
||||||
}),
|
this.heartbeatTimers.delete(lockId);
|
||||||
(error) => complete(() => reject(error)),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
signal.addEventListener("abort", onAbort, { once: true });
|
|
||||||
|
|
||||||
Effect.runPromise(Fiber.join(fiber)).then(
|
|
||||||
(value) => complete(() => resolve(value)),
|
|
||||||
(error) => {
|
|
||||||
if (!aborting) {
|
|
||||||
complete(() => reject(error));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async acquireShared(repositoryId: string, operation: string, signal?: AbortSignal) {
|
|
||||||
return await this.runWithSignal(this.acquireSharedEffect(repositoryId, operation), signal);
|
|
||||||
}
|
|
||||||
|
|
||||||
async acquireExclusive(repositoryId: string, operation: string, signal?: AbortSignal) {
|
|
||||||
return await this.runWithSignal(this.acquireExclusiveEffect(repositoryId, operation), signal);
|
|
||||||
}
|
|
||||||
|
|
||||||
async acquireMany(requests: LockRequest[], signal?: AbortSignal) {
|
|
||||||
return await this.runWithSignal(this.acquireManyEffect(requests), signal);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const repoMutex = getRepositoryMutex();
|
export const repoMutex = new RepositoryMutex();
|
||||||
|
|
|
||||||
|
|
@ -174,22 +174,6 @@ export const auth = betterAuth({
|
||||||
passkey({
|
passkey({
|
||||||
rpID: new URL(config.baseUrl).hostname,
|
rpID: new URL(config.baseUrl).hostname,
|
||||||
rpName: "Zerobyte",
|
rpName: "Zerobyte",
|
||||||
authenticatorSelection: {
|
|
||||||
userVerification: "required",
|
|
||||||
residentKey: "required",
|
|
||||||
},
|
|
||||||
authentication: {
|
|
||||||
afterVerification: async ({ verification }) => {
|
|
||||||
if (verification.authenticationInfo.userVerified) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new APIError("UNAUTHORIZED", {
|
|
||||||
message:
|
|
||||||
"Your passkey was accepted, but it did not confirm your identity with a PIN, biometrics, or screen lock. Please use a verified passkey or sign in with your password.",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
tanstackStartCookies(),
|
tanstackStartCookies(),
|
||||||
...(process.env.NODE_ENV === "test" ? [testUtils()] : []),
|
...(process.env.NODE_ENV === "test" ? [testUtils()] : []),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
import { createServerFn } from "@tanstack/react-start";
|
|
||||||
import { hasActivePasskeyUser } from "~/server/modules/auth/helpers";
|
|
||||||
|
|
||||||
export const getLoginOptions = createServerFn({ method: "GET" }).handler(async () => ({
|
|
||||||
hasPasskeySignIn: await hasActivePasskeyUser(),
|
|
||||||
}));
|
|
||||||
|
|
@ -106,41 +106,6 @@ test("backup progress is delivered to the running backup callback", async () =>
|
||||||
await stopAgentController();
|
await stopAgentController();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("backup events from agents that do not own the active run are ignored", async () => {
|
|
||||||
resetAgentRuntime();
|
|
||||||
controllerMock.sendBackup.mockImplementation(() => Effect.succeed(true));
|
|
||||||
const { agentManager, startAgentController, stopAgentController } = await import("../agents-manager");
|
|
||||||
|
|
||||||
await startAgentController();
|
|
||||||
const resultPromise = agentManager.runBackup("local", {
|
|
||||||
scheduleId: 42,
|
|
||||||
payload: backupPayload,
|
|
||||||
signal: new AbortController().signal,
|
|
||||||
onProgress: vi.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
controllerMock.onEvent?.({
|
|
||||||
type: "backup.completed",
|
|
||||||
agentId: "remote",
|
|
||||||
agentName: "Remote Agent",
|
|
||||||
payload: { jobId: "job-1", scheduleId: "schedule-1", exitCode: 0, result: null },
|
|
||||||
});
|
|
||||||
controllerMock.onEvent?.({
|
|
||||||
type: "backup.completed",
|
|
||||||
agentId: "local",
|
|
||||||
agentName: "Local Agent",
|
|
||||||
payload: { jobId: "job-1", scheduleId: "schedule-1", exitCode: 0, result: null },
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(resultPromise).resolves.toEqual({
|
|
||||||
status: "completed",
|
|
||||||
exitCode: 0,
|
|
||||||
result: null,
|
|
||||||
warningDetails: null,
|
|
||||||
});
|
|
||||||
await stopAgentController();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("backup failed and cancelled events resolve the matching running backup", async () => {
|
test("backup failed and cancelled events resolve the matching running backup", async () => {
|
||||||
resetAgentRuntime();
|
resetAgentRuntime();
|
||||||
controllerMock.sendBackup.mockImplementation(() => Effect.succeed(true));
|
controllerMock.sendBackup.mockImplementation(() => Effect.succeed(true));
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,6 @@ test("websocket lifecycle updates agent connection status", async () => {
|
||||||
expect(agentsServiceMocks.markAgentOnline).toHaveBeenCalledWith(LOCAL_AGENT_ID, expect.any(Number), {
|
expect(agentsServiceMocks.markAgentOnline).toHaveBeenCalledWith(LOCAL_AGENT_ID, expect.any(Number), {
|
||||||
backup: true,
|
backup: true,
|
||||||
protocolVersion: 1,
|
protocolVersion: 1,
|
||||||
protocolCompatible: true,
|
|
||||||
hostname: "host",
|
hostname: "host",
|
||||||
platform: "linux",
|
platform: "linux",
|
||||||
});
|
});
|
||||||
|
|
@ -216,40 +215,6 @@ test("websocket lifecycle updates agent connection status", async () => {
|
||||||
expect(stop).toHaveBeenCalledWith(true);
|
expect(stop).toHaveBeenCalledWith(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("websocket protocol rejection forwards the event and closes the connection", async () => {
|
|
||||||
const serve = vi
|
|
||||||
.spyOn(Bun, "serve")
|
|
||||||
.mockReturnValue(fromPartial({ port: 3001, stop: vi.fn(() => Promise.resolve()) }));
|
|
||||||
const { runtime, onEvent } = await startRuntime(vi.fn());
|
|
||||||
const websocket = serve.mock.calls[0]?.[0].websocket;
|
|
||||||
const socket = createSocket("connection-1");
|
|
||||||
|
|
||||||
await websocket?.open?.(fromPartial(socket));
|
|
||||||
await websocket?.message?.(
|
|
||||||
fromPartial(socket),
|
|
||||||
JSON.stringify({
|
|
||||||
type: "agent.ready",
|
|
||||||
payload: {
|
|
||||||
protocolVersion: 2,
|
|
||||||
hostname: "host",
|
|
||||||
platform: "linux",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
await Effect.runPromise(runtime.stop);
|
|
||||||
|
|
||||||
expect(onEvent).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
type: "agent.protocolRejected",
|
|
||||||
agentId: LOCAL_AGENT_ID,
|
|
||||||
agentName: LOCAL_AGENT_NAME,
|
|
||||||
payload: expect.objectContaining({ reason: "agent_too_new" }),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(agentsServiceMocks.markAgentOffline).toHaveBeenCalledWith(LOCAL_AGENT_ID);
|
|
||||||
expect(socket.close).toHaveBeenCalledWith(1002, "agent_too_new");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("websocket restore events are forwarded with agent metadata", async () => {
|
test("websocket restore events are forwarded with agent metadata", async () => {
|
||||||
const serve = vi
|
const serve = vi
|
||||||
.spyOn(Bun, "serve")
|
.spyOn(Bun, "serve")
|
||||||
|
|
@ -259,8 +224,6 @@ test("websocket restore events are forwarded with agent metadata", async () => {
|
||||||
const socket = createSocket("connection-1");
|
const socket = createSocket("connection-1");
|
||||||
|
|
||||||
await websocket?.open?.(fromPartial(socket));
|
await websocket?.open?.(fromPartial(socket));
|
||||||
await websocket?.message?.(fromPartial(socket), createAgentMessage("agent.ready", readyPayload));
|
|
||||||
onEvent.mockClear();
|
|
||||||
await websocket?.message?.(
|
await websocket?.message?.(
|
||||||
fromPartial(socket),
|
fromPartial(socket),
|
||||||
createAgentMessage("restore.completed", {
|
createAgentMessage("restore.completed", {
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,7 @@ import { Effect, Exit, Fiber, Scope } from "effect";
|
||||||
import { expect, test, vi } from "vitest";
|
import { expect, test, vi } from "vitest";
|
||||||
import waitForExpect from "wait-for-expect";
|
import waitForExpect from "wait-for-expect";
|
||||||
import { fromPartial } from "@total-typescript/shoehorn";
|
import { fromPartial } from "@total-typescript/shoehorn";
|
||||||
import {
|
import { createAgentMessage, type AgentMessage } from "@zerobyte/contracts/agent-protocol";
|
||||||
createAgentMessage,
|
|
||||||
SUPPORTED_AGENT_PROTOCOL_MAX_VERSION,
|
|
||||||
type AgentMessage,
|
|
||||||
} from "@zerobyte/contracts/agent-protocol";
|
|
||||||
import type { Volume } from "@zerobyte/contracts/volumes";
|
import type { Volume } from "@zerobyte/contracts/volumes";
|
||||||
import { LOCAL_AGENT_ID, LOCAL_AGENT_KIND, LOCAL_AGENT_NAME } from "../constants";
|
import { LOCAL_AGENT_ID, LOCAL_AGENT_KIND, LOCAL_AGENT_NAME } from "../constants";
|
||||||
import { createControllerAgentSession } from "../controller/session";
|
import { createControllerAgentSession } from "../controller/session";
|
||||||
|
|
@ -137,19 +133,6 @@ test("invalid inbound messages are ignored", () => {
|
||||||
const onEvent = vi.fn(() => Effect.void);
|
const onEvent = vi.fn(() => Effect.void);
|
||||||
const { session, close } = createSession(onEvent);
|
const { session, close } = createSession(onEvent);
|
||||||
|
|
||||||
Effect.runSync(
|
|
||||||
session.handleMessage(
|
|
||||||
createAgentMessage("agent.ready", {
|
|
||||||
agentId: LOCAL_AGENT_ID,
|
|
||||||
protocolVersion: 1,
|
|
||||||
hostname: "host",
|
|
||||||
platform: "linux",
|
|
||||||
capabilities: { backup: true },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
onEvent.mockClear();
|
|
||||||
|
|
||||||
Effect.runSync(session.handleMessage("not json"));
|
Effect.runSync(session.handleMessage("not json"));
|
||||||
Effect.runSync(session.handleMessage(JSON.stringify({ type: "backup.progress", payload: {} })));
|
Effect.runSync(session.handleMessage(JSON.stringify({ type: "backup.progress", payload: {} })));
|
||||||
|
|
||||||
|
|
@ -210,19 +193,6 @@ test("backup agent messages are forwarded unchanged", () => {
|
||||||
},
|
},
|
||||||
} satisfies Extract<AgentMessage, { type: "backup.progress" }>;
|
} satisfies Extract<AgentMessage, { type: "backup.progress" }>;
|
||||||
|
|
||||||
Effect.runSync(
|
|
||||||
session.handleMessage(
|
|
||||||
createAgentMessage("agent.ready", {
|
|
||||||
agentId: LOCAL_AGENT_ID,
|
|
||||||
protocolVersion: 1,
|
|
||||||
hostname: "host",
|
|
||||||
platform: "linux",
|
|
||||||
capabilities: { backup: true },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
onEvent.mockClear();
|
|
||||||
|
|
||||||
Effect.runSync(session.handleMessage(createAgentMessage(message.type, message.payload)));
|
Effect.runSync(session.handleMessage(createAgentMessage(message.type, message.payload)));
|
||||||
|
|
||||||
expect(onEvent).toHaveBeenCalledWith(
|
expect(onEvent).toHaveBeenCalledWith(
|
||||||
|
|
@ -238,53 +208,6 @@ test("backup agent messages are forwarded unchanged", () => {
|
||||||
close();
|
close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("unsupported agent protocol rejects startup and closes the session", () => {
|
|
||||||
const onEvent = vi.fn(() => Effect.void);
|
|
||||||
const { session, socket } = createSession(onEvent);
|
|
||||||
|
|
||||||
Effect.runSync(
|
|
||||||
session.handleMessage(
|
|
||||||
JSON.stringify({
|
|
||||||
type: "agent.ready",
|
|
||||||
payload: {
|
|
||||||
protocolVersion: SUPPORTED_AGENT_PROTOCOL_MAX_VERSION + 1,
|
|
||||||
hostname: "host",
|
|
||||||
platform: "linux",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(Effect.runSync(session.isReady())).toBe(false);
|
|
||||||
expect(onEvent).toHaveBeenCalledWith({
|
|
||||||
type: "agent.protocolRejected",
|
|
||||||
payload: expect.objectContaining({
|
|
||||||
reason: "agent_too_new",
|
|
||||||
protocolVersion: SUPPORTED_AGENT_PROTOCOL_MAX_VERSION + 1,
|
|
||||||
hostname: "host",
|
|
||||||
platform: "linux",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
expect(socket.close).toHaveBeenCalledWith(1002, "agent_too_new");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("pre-ready non-ready messages reject startup and close the session", () => {
|
|
||||||
const onEvent = vi.fn(() => Effect.void);
|
|
||||||
const { session, socket } = createSession(onEvent);
|
|
||||||
|
|
||||||
Effect.runSync(session.handleMessage(createAgentMessage("heartbeat.pong", { sentAt: 123 })));
|
|
||||||
|
|
||||||
expect(Effect.runSync(session.isReady())).toBe(false);
|
|
||||||
expect(onEvent).toHaveBeenCalledWith({
|
|
||||||
type: "agent.protocolRejected",
|
|
||||||
payload: expect.objectContaining({
|
|
||||||
reason: "unexpected_startup_message",
|
|
||||||
messageType: "heartbeat.pong",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
expect(socket.close).toHaveBeenCalledWith(1002, "unexpected_startup_message");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a dropped backup.cancel closes the session and reports a transport disconnect", async () => {
|
test("a dropped backup.cancel closes the session and reports a transport disconnect", async () => {
|
||||||
const send = vi.fn(() => 0);
|
const send = vi.fn(() => 0);
|
||||||
const socket = createSocket({ send, close: vi.fn() });
|
const socket = createSocket({ send, close: vi.fn() });
|
||||||
|
|
|
||||||
|
|
@ -152,11 +152,6 @@ const getActiveBackupRun = (jobId: string, scheduleId: string, eventName: string
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeBackupRun.agentId !== agentId) {
|
|
||||||
logger.warn(`Ignoring ${eventName} for job ${jobId} from unexpected agent ${agentId}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return activeBackupRun;
|
return activeBackupRun;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -247,10 +242,6 @@ const handleAgentManagerEvent = (event: AgentManagerEvent) => {
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "agent.protocolRejected": {
|
|
||||||
logger.warn(`Rejected agent protocol for ${event.agentName} (${event.agentId}): ${event.payload.reason}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "backup.started": {
|
case "backup.started": {
|
||||||
getActiveBackupRun(event.payload.jobId, event.payload.scheduleId, event.type, event.agentId);
|
getActiveBackupRun(event.payload.jobId, event.payload.scheduleId, event.type, event.agentId);
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { logger } from "@zerobyte/core/node";
|
||||||
import { toMessage } from "@zerobyte/core/utils";
|
import { toMessage } from "@zerobyte/core/utils";
|
||||||
import type {
|
import type {
|
||||||
AgentMessage,
|
AgentMessage,
|
||||||
AgentProtocolRejection,
|
|
||||||
BackupCancelPayload,
|
BackupCancelPayload,
|
||||||
BackupRunPayload,
|
BackupRunPayload,
|
||||||
RestoreCancelPayload,
|
RestoreCancelPayload,
|
||||||
|
|
@ -27,7 +26,6 @@ type AgentEventContext = {
|
||||||
|
|
||||||
export type AgentManagerEvent =
|
export type AgentManagerEvent =
|
||||||
| (AgentEventContext & { type: "agent.disconnected" })
|
| (AgentEventContext & { type: "agent.disconnected" })
|
||||||
| (AgentEventContext & { type: "agent.protocolRejected"; payload: AgentProtocolRejection })
|
|
||||||
| (AgentEventContext & AgentMessage);
|
| (AgentEventContext & AgentMessage);
|
||||||
|
|
||||||
type ControllerAgentSessionHandle = {
|
type ControllerAgentSessionHandle = {
|
||||||
|
|
@ -94,17 +92,11 @@ export function createAgentManagerRuntime(onEvent: (event: AgentManagerEvent) =>
|
||||||
await agentsService.markAgentOnline(agentId, at, {
|
await agentsService.markAgentOnline(agentId, at, {
|
||||||
...event.payload.capabilities,
|
...event.payload.capabilities,
|
||||||
protocolVersion: event.payload.protocolVersion,
|
protocolVersion: event.payload.protocolVersion,
|
||||||
protocolCompatible: true,
|
|
||||||
hostname: event.payload.hostname,
|
hostname: event.payload.hostname,
|
||||||
platform: event.payload.platform,
|
platform: event.payload.platform,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
case "agent.protocolRejected": {
|
|
||||||
return Effect.sync(() =>
|
|
||||||
onEvent({ type: "agent.protocolRejected", agentId, agentName, payload: event.payload }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "heartbeat.pong": {
|
case "heartbeat.pong": {
|
||||||
const at = Date.now();
|
const at = Date.now();
|
||||||
return Effect.promise(() => agentsService.markAgentSeen(agentId, at));
|
return Effect.promise(() => agentsService.markAgentSeen(agentId, at));
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,7 @@ import type { AgentKind } from "../../../db/schema";
|
||||||
import {
|
import {
|
||||||
createControllerMessage,
|
createControllerMessage,
|
||||||
parseAgentMessage,
|
parseAgentMessage,
|
||||||
parseAgentStartupMessage,
|
|
||||||
SUPPORTED_AGENT_PROTOCOL_MAX_VERSION,
|
|
||||||
SUPPORTED_AGENT_PROTOCOL_MIN_VERSION,
|
|
||||||
type AgentMessage,
|
type AgentMessage,
|
||||||
type AgentProtocolRejection,
|
|
||||||
type BackupCancelPayload,
|
type BackupCancelPayload,
|
||||||
type BackupRunPayload,
|
type BackupRunPayload,
|
||||||
type ControllerWireMessage,
|
type ControllerWireMessage,
|
||||||
|
|
@ -31,7 +27,6 @@ type AgentSocket = Bun.ServerWebSocket<AgentConnectionData>;
|
||||||
|
|
||||||
type SessionState = {
|
type SessionState = {
|
||||||
isReady: boolean;
|
isReady: boolean;
|
||||||
protocolVersion: number | null;
|
|
||||||
lastSeenAt: number | null;
|
lastSeenAt: number | null;
|
||||||
lastPongAt: number | null;
|
lastPongAt: number | null;
|
||||||
};
|
};
|
||||||
|
|
@ -40,7 +35,6 @@ type PendingCommand = { deferred: Deferred.Deferred<VolumeCommandResponsePayload
|
||||||
|
|
||||||
export type ControllerAgentSessionEvent =
|
export type ControllerAgentSessionEvent =
|
||||||
| Exclude<AgentMessage, { type: "volume.commandResult" }>
|
| Exclude<AgentMessage, { type: "volume.commandResult" }>
|
||||||
| { type: "agent.protocolRejected"; payload: AgentProtocolRejection }
|
|
||||||
| { type: "agent.disconnected" };
|
| { type: "agent.disconnected" };
|
||||||
|
|
||||||
export type ControllerAgentSession = {
|
export type ControllerAgentSession = {
|
||||||
|
|
@ -65,7 +59,6 @@ export const createControllerAgentSession = (
|
||||||
const pendingCommands = yield* Ref.make(new Map<string, PendingCommand>());
|
const pendingCommands = yield* Ref.make(new Map<string, PendingCommand>());
|
||||||
const state = yield* Ref.make<SessionState>({
|
const state = yield* Ref.make<SessionState>({
|
||||||
isReady: false,
|
isReady: false,
|
||||||
protocolVersion: null,
|
|
||||||
lastSeenAt: null,
|
lastSeenAt: null,
|
||||||
lastPongAt: null,
|
lastPongAt: null,
|
||||||
});
|
});
|
||||||
|
|
@ -190,12 +183,7 @@ export const createControllerAgentSession = (
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case "agent.ready": {
|
case "agent.ready": {
|
||||||
const readyAt = Date.now();
|
const readyAt = Date.now();
|
||||||
yield* updateState((current) => ({
|
yield* updateState((current) => ({ ...current, isReady: true, lastSeenAt: readyAt }));
|
||||||
...current,
|
|
||||||
isReady: true,
|
|
||||||
protocolVersion: message.payload.protocolVersion,
|
|
||||||
lastSeenAt: readyAt,
|
|
||||||
}));
|
|
||||||
yield* logger.effect.info(`Agent "${socket.data.agentName}" (${socket.data.agentId}) is ready`);
|
yield* logger.effect.info(`Agent "${socket.data.agentName}" (${socket.data.agentId}) is ready`);
|
||||||
yield* onEvent(message);
|
yield* onEvent(message);
|
||||||
break;
|
break;
|
||||||
|
|
@ -221,30 +209,10 @@ export const createControllerAgentSession = (
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const rejectStartupMessage = (rejection: AgentProtocolRejection) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
yield* logger.effect.warn(
|
|
||||||
`Rejecting startup message from agent ${socket.data.agentId}: ${rejection.reason}`,
|
|
||||||
);
|
|
||||||
yield* onEvent({ type: "agent.protocolRejected", payload: rejection });
|
|
||||||
yield* Effect.sync(() => socket.close(1002, rejection.reason));
|
|
||||||
yield* closeSession();
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connectionId: socket.data.id,
|
connectionId: socket.data.id,
|
||||||
handleMessage: (data: string) => {
|
handleMessage: (data: string) => {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const currentState = yield* Ref.get(state);
|
|
||||||
|
|
||||||
if (!currentState.isReady) {
|
|
||||||
const startupMessage = parseAgentStartupMessage(data);
|
|
||||||
if (!("success" in startupMessage)) {
|
|
||||||
yield* rejectStartupMessage(startupMessage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = parseAgentMessage(data);
|
const parsed = parseAgentMessage(data);
|
||||||
|
|
||||||
if (parsed === null) {
|
if (parsed === null) {
|
||||||
|
|
@ -253,15 +221,6 @@ export const createControllerAgentSession = (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
if (!currentState.isReady) {
|
|
||||||
yield* rejectStartupMessage({
|
|
||||||
reason: "invalid_agent_ready",
|
|
||||||
supportedProtocolMinVersion: SUPPORTED_AGENT_PROTOCOL_MIN_VERSION,
|
|
||||||
supportedProtocolMaxVersion: SUPPORTED_AGENT_PROTOCOL_MAX_VERSION,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield* logger.effect.warn(
|
yield* logger.effect.warn(
|
||||||
`Invalid agent message from ${socket.data.agentId}: ${parsed.error.message}`,
|
`Invalid agent message from ${socket.data.agentId}: ${parsed.error.message}`,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { db } from "~/server/db/db";
|
import { db } from "~/server/db/db";
|
||||||
import { account, passkey, usersTable } from "~/server/db/schema";
|
import { account, usersTable } from "~/server/db/schema";
|
||||||
import { createUser, randomId, randomSlug } from "~/test/helpers/user-org";
|
import { createUser, randomId, randomSlug } from "~/test/helpers/user-org";
|
||||||
import { hasActivePasskeyUser, userHasCredentialPassword, verifyUserPassword } from "../helpers";
|
import { userHasCredentialPassword, verifyUserPassword } from "../helpers";
|
||||||
|
|
||||||
const { verifyPassword } = vi.hoisted(() => ({
|
const { verifyPassword } = vi.hoisted(() => ({
|
||||||
verifyPassword: vi.fn(async ({ hash }: { hash: string }) => hash === "credential-password-hash"),
|
verifyPassword: vi.fn(async ({ hash }: { hash: string }) => hash === "credential-password-hash"),
|
||||||
|
|
@ -31,24 +30,9 @@ async function createAccount({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createPasskey(userId: string) {
|
|
||||||
await db.insert(passkey).values({
|
|
||||||
id: randomId(),
|
|
||||||
name: "Test passkey",
|
|
||||||
publicKey: randomSlug("public-key"),
|
|
||||||
userId,
|
|
||||||
credentialID: randomSlug("credential"),
|
|
||||||
counter: 0,
|
|
||||||
deviceType: "singleDevice",
|
|
||||||
backedUp: false,
|
|
||||||
transports: "internal",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("verifyUserPassword", () => {
|
describe("verifyUserPassword", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
verifyPassword.mockClear();
|
verifyPassword.mockClear();
|
||||||
await db.delete(passkey);
|
|
||||||
await db.delete(account);
|
await db.delete(account);
|
||||||
await db.delete(usersTable);
|
await db.delete(usersTable);
|
||||||
});
|
});
|
||||||
|
|
@ -80,7 +64,6 @@ describe("verifyUserPassword", () => {
|
||||||
|
|
||||||
describe("userHasCredentialPassword", () => {
|
describe("userHasCredentialPassword", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await db.delete(passkey);
|
|
||||||
await db.delete(account);
|
await db.delete(account);
|
||||||
await db.delete(usersTable);
|
await db.delete(usersTable);
|
||||||
});
|
});
|
||||||
|
|
@ -106,32 +89,3 @@ describe("userHasCredentialPassword", () => {
|
||||||
await expect(userHasCredentialPassword(userId)).resolves.toBe(false);
|
await expect(userHasCredentialPassword(userId)).resolves.toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("hasActivePasskeyUser", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await db.delete(passkey);
|
|
||||||
await db.delete(account);
|
|
||||||
await db.delete(usersTable);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns true when a non-banned user has a passkey", async () => {
|
|
||||||
const userId = await createUser(`${randomSlug("user")}@example.com`);
|
|
||||||
await createPasskey(userId);
|
|
||||||
|
|
||||||
await expect(hasActivePasskeyUser()).resolves.toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns false when only banned users have passkeys", async () => {
|
|
||||||
const userId = await createUser(`${randomSlug("user")}@example.com`);
|
|
||||||
await db.update(usersTable).set({ banned: true }).where(eq(usersTable.id, userId));
|
|
||||||
await createPasskey(userId);
|
|
||||||
|
|
||||||
await expect(hasActivePasskeyUser()).resolves.toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns false when no users have passkeys", async () => {
|
|
||||||
await createUser(`${randomSlug("user")}@example.com`);
|
|
||||||
|
|
||||||
await expect(hasActivePasskeyUser()).resolves.toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { verifyPassword } from "better-auth/crypto";
|
import { verifyPassword } from "better-auth/crypto";
|
||||||
import { db } from "~/server/db/db";
|
import { db } from "~/server/db/db";
|
||||||
import { passkey, usersTable } from "~/server/db/schema";
|
|
||||||
|
|
||||||
type PasswordVerificationBody = {
|
type PasswordVerificationBody = {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|
@ -33,14 +31,3 @@ export const userHasCredentialPassword = async (userId: string) => {
|
||||||
|
|
||||||
return Boolean(userAccount?.password);
|
return Boolean(userAccount?.password);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hasActivePasskeyUser = async () => {
|
|
||||||
const [user] = await db
|
|
||||||
.select({ id: usersTable.id })
|
|
||||||
.from(passkey)
|
|
||||||
.innerJoin(usersTable, eq(passkey.userId, usersTable.id))
|
|
||||||
.where(eq(usersTable.banned, false))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
return Boolean(user);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -208,141 +208,6 @@ describe("repositoriesService repository stats", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("repositoriesService.listSnapshotFiles", () => {
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("limits concurrent restic ls commands per repository", async () => {
|
|
||||||
const repository = await createTestRepository(session.organizationId);
|
|
||||||
let active = 0;
|
|
||||||
let maxActive = 0;
|
|
||||||
let releaseAll = false;
|
|
||||||
let exclusiveAcquired = false;
|
|
||||||
let releaseExclusive: (() => void) | undefined;
|
|
||||||
let exclusivePromise: Promise<() => void> | undefined;
|
|
||||||
const releaseWaiters: Array<() => void> = [];
|
|
||||||
const exclusiveController = new AbortController();
|
|
||||||
|
|
||||||
const releaseWaitingCommands = () => {
|
|
||||||
const waiters = releaseWaiters.splice(0);
|
|
||||||
for (const release of waiters) {
|
|
||||||
release();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolveWithin = async <T>(promise: Promise<T>, timeoutMs: number) => {
|
|
||||||
return await new Promise<T>((resolve, reject) => {
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
reject(new Error(`Expected promise to resolve within ${timeoutMs}ms`));
|
|
||||||
}, timeoutMs);
|
|
||||||
|
|
||||||
promise.then(
|
|
||||||
(value) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
resolve(value);
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
reject(error);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const lsSpy = vi.spyOn(restic, "ls").mockImplementation((_config, snapshotId, _path, options) =>
|
|
||||||
Effect.promise(async () => {
|
|
||||||
active++;
|
|
||||||
maxActive = Math.max(maxActive, active);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!releaseAll) {
|
|
||||||
await new Promise<void>((resolve) => releaseWaiters.push(resolve));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
snapshot: {
|
|
||||||
id: snapshotId,
|
|
||||||
short_id: snapshotId,
|
|
||||||
time: new Date().toISOString(),
|
|
||||||
tree: "tree",
|
|
||||||
paths: ["/"],
|
|
||||||
hostname: "host",
|
|
||||||
struct_type: "snapshot" as const,
|
|
||||||
message_type: "snapshot" as const,
|
|
||||||
},
|
|
||||||
nodes: [],
|
|
||||||
pagination: {
|
|
||||||
offset: options.offset ?? 0,
|
|
||||||
limit: options.limit ?? 500,
|
|
||||||
total: 0,
|
|
||||||
hasMore: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} finally {
|
|
||||||
active--;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const calls = Array.from({ length: 4 }, (_, index) =>
|
|
||||||
withContext({ organizationId: session.organizationId, userId: session.user.id }, () =>
|
|
||||||
repositoriesService.listSnapshotFiles(repository.shortId, `snapshot-${index}`, "/", {
|
|
||||||
offset: 0,
|
|
||||||
limit: 100,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await waitForExpect(() => {
|
|
||||||
expect(releaseWaiters).toHaveLength(2);
|
|
||||||
});
|
|
||||||
expect(maxActive).toBe(2);
|
|
||||||
|
|
||||||
exclusivePromise = repoMutex
|
|
||||||
.acquireExclusive(repository.id, "delete", exclusiveController.signal)
|
|
||||||
.then((release) => {
|
|
||||||
exclusiveAcquired = true;
|
|
||||||
releaseExclusive = release;
|
|
||||||
return release;
|
|
||||||
});
|
|
||||||
|
|
||||||
releaseWaitingCommands();
|
|
||||||
|
|
||||||
releaseExclusive = await resolveWithin(exclusivePromise, 2000);
|
|
||||||
expect(exclusiveAcquired).toBe(true);
|
|
||||||
expect(active).toBe(0);
|
|
||||||
|
|
||||||
releaseExclusive();
|
|
||||||
releaseExclusive = undefined;
|
|
||||||
|
|
||||||
await waitForExpect(() => {
|
|
||||||
expect(releaseWaiters).toHaveLength(2);
|
|
||||||
});
|
|
||||||
expect(maxActive).toBe(2);
|
|
||||||
|
|
||||||
releaseWaitingCommands();
|
|
||||||
await Promise.all(calls);
|
|
||||||
} finally {
|
|
||||||
if (releaseExclusive) {
|
|
||||||
releaseExclusive();
|
|
||||||
} else {
|
|
||||||
exclusiveController.abort();
|
|
||||||
}
|
|
||||||
releaseAll = true;
|
|
||||||
releaseWaitingCommands();
|
|
||||||
await Promise.allSettled(calls);
|
|
||||||
if (exclusivePromise) {
|
|
||||||
await Promise.allSettled([exclusivePromise]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(lsSpy).toHaveBeenCalledTimes(4);
|
|
||||||
expect(maxActive).toBeLessThanOrEqual(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("repositoriesService.dumpSnapshot", () => {
|
describe("repositoriesService.dumpSnapshot", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ import type { ParsedTask, TaskInput } from "../tasks/tasks.schemas";
|
||||||
import { Effect } from "effect";
|
import { Effect } from "effect";
|
||||||
|
|
||||||
const runningDoctors = new Map<string, AbortController>();
|
const runningDoctors = new Map<string, AbortController>();
|
||||||
const lsLimiters = new Map<string, Effect.Semaphore>();
|
|
||||||
const RESTORE_TASK_RESOURCE_TYPE = "repository";
|
const RESTORE_TASK_RESOURCE_TYPE = "repository";
|
||||||
|
|
||||||
type RestoreTaskInput = Extract<TaskInput, { kind: "restore" }>;
|
type RestoreTaskInput = Extract<TaskInput, { kind: "restore" }>;
|
||||||
|
|
@ -100,15 +99,6 @@ const updateActiveRestoreTask = (restoreId: string, eventName: string, update: (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLsLimiter = (repositoryId: string) => {
|
|
||||||
let limiter = lsLimiters.get(repositoryId);
|
|
||||||
if (!limiter) {
|
|
||||||
limiter = Effect.runSync(Effect.makeSemaphore(2));
|
|
||||||
lsLimiters.set(repositoryId, limiter);
|
|
||||||
}
|
|
||||||
return limiter;
|
|
||||||
};
|
|
||||||
|
|
||||||
const findActiveRestoreTask = (
|
const findActiveRestoreTask = (
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
repositoryShortId: string,
|
repositoryShortId: string,
|
||||||
|
|
@ -479,10 +469,6 @@ const listSnapshotFiles = async (
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const limiter = getLsLimiter(repository.id);
|
|
||||||
await runEffectPromise(limiter.take(1));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const releaseLock = await repoMutex.acquireShared(repository.id, `ls:${snapshotId}`);
|
const releaseLock = await repoMutex.acquireShared(repository.id, `ls:${snapshotId}`);
|
||||||
try {
|
try {
|
||||||
const result = await runEffectPromise(
|
const result = await runEffectPromise(
|
||||||
|
|
@ -514,9 +500,6 @@ const listSnapshotFiles = async (
|
||||||
} finally {
|
} finally {
|
||||||
releaseLock();
|
releaseLock();
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
await runEffectPromise(limiter.release(1));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const restoreSnapshot = async (
|
const restoreSnapshot = async (
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,18 @@ set -euo pipefail
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
TARGET_HOST="192.168.2.41"
|
TARGET_HOST="192.168.2.41"
|
||||||
TARGET="root@$TARGET_HOST"
|
TARGET="root@$TARGET_HOST"
|
||||||
|
FIXTURE_UID="1000"
|
||||||
|
FIXTURE_GID="1000"
|
||||||
|
|
||||||
ARTIFACTS_DIR="$SCRIPT_DIR/artifacts/$TARGET_HOST"
|
ARTIFACTS_DIR="$SCRIPT_DIR/artifacts/$TARGET_HOST"
|
||||||
|
KEY_PATH="$ARTIFACTS_DIR/zerobyte-sftp-ed25519"
|
||||||
KNOWN_HOSTS_PATH="$ARTIFACTS_DIR/known_hosts"
|
KNOWN_HOSTS_PATH="$ARTIFACTS_DIR/known_hosts"
|
||||||
CONFIG_PATH="$ARTIFACTS_DIR/config.generated.json"
|
CONFIG_PATH="$ARTIFACTS_DIR/config.generated.json"
|
||||||
|
|
||||||
|
SMB_PASSWORD_FILE="$ARTIFACTS_DIR/smb-password.txt"
|
||||||
SFTP_PASSWORD_FILE="$ARTIFACTS_DIR/sftp-password.txt"
|
SFTP_PASSWORD_FILE="$ARTIFACTS_DIR/sftp-password.txt"
|
||||||
|
WEBDAV_PASSWORD_FILE="$ARTIFACTS_DIR/webdav-password.txt"
|
||||||
|
RESTIC_PASSWORD_FILE="$ARTIFACTS_DIR/restic-password.txt"
|
||||||
|
|
||||||
read_or_create_secret() {
|
read_or_create_secret() {
|
||||||
local file_path="$1"
|
local file_path="$1"
|
||||||
|
|
@ -26,12 +32,31 @@ read_or_create_secret() {
|
||||||
mkdir -p "$ARTIFACTS_DIR"
|
mkdir -p "$ARTIFACTS_DIR"
|
||||||
chmod 700 "$ARTIFACTS_DIR"
|
chmod 700 "$ARTIFACTS_DIR"
|
||||||
|
|
||||||
|
SMB_PASSWORD="$(read_or_create_secret "$SMB_PASSWORD_FILE")"
|
||||||
SFTP_PASSWORD="$(read_or_create_secret "$SFTP_PASSWORD_FILE")"
|
SFTP_PASSWORD="$(read_or_create_secret "$SFTP_PASSWORD_FILE")"
|
||||||
|
WEBDAV_PASSWORD="$(read_or_create_secret "$WEBDAV_PASSWORD_FILE")"
|
||||||
|
RESTIC_PASSWORD="$(read_or_create_secret "$RESTIC_PASSWORD_FILE")"
|
||||||
|
|
||||||
ssh "$TARGET" bash -s -- "$SFTP_PASSWORD" <<'REMOTE'
|
if [[ ! -f "$KEY_PATH" || ! -f "$KEY_PATH.pub" ]]; then
|
||||||
|
ssh-keygen -q -t ed25519 -N "" -C "zerobyte-backend-integration@$TARGET_HOST" -f "$KEY_PATH"
|
||||||
|
chmod 600 "$KEY_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
PUBLIC_KEY_BASE64="$(base64 <"$KEY_PATH.pub" | tr -d '\n')"
|
||||||
|
|
||||||
|
ssh "$TARGET" bash -s -- "$FIXTURE_UID" "$FIXTURE_GID" "$SMB_PASSWORD" "$SFTP_PASSWORD" "$WEBDAV_PASSWORD" "$RESTIC_PASSWORD" "$PUBLIC_KEY_BASE64" <<'REMOTE'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
sftp_password="$1"
|
fixture_uid="$1"
|
||||||
|
fixture_gid="$2"
|
||||||
|
smb_password="$3"
|
||||||
|
sftp_password="$4"
|
||||||
|
webdav_password="$5"
|
||||||
|
restic_password="$6"
|
||||||
|
public_key="$(printf '%s' "$7" | base64 -d)"
|
||||||
|
repo_path="/srv/zerobyte-backend-integration/restic-repo"
|
||||||
|
repo_password_fingerprint_path="$repo_path/.zerobyte-password-sha256"
|
||||||
|
repo_password_fingerprint="$(printf '%s' "$restic_password" | sha256sum | cut -d' ' -f1)"
|
||||||
legacy_sshd_dir="/etc/ssh/zerobyte-backend-integration-legacy"
|
legacy_sshd_dir="/etc/ssh/zerobyte-backend-integration-legacy"
|
||||||
|
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
@ -41,19 +66,98 @@ write_file() {
|
||||||
cat >"$file_path"
|
cat >"$file_path"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initialize_restic_repo() {
|
||||||
|
local password_file
|
||||||
|
|
||||||
|
rm -rf "$repo_path"
|
||||||
|
install -d -o zerobyte-sftp -g zerobyte-sftp -m 0700 "$repo_path"
|
||||||
|
|
||||||
|
password_file="$(mktemp)"
|
||||||
|
printf '%s\n' "$restic_password" >"$password_file"
|
||||||
|
chown zerobyte-sftp:zerobyte-sftp "$password_file"
|
||||||
|
chmod 0600 "$password_file"
|
||||||
|
su -s /bin/sh -c "restic init --repo '$repo_path' --password-file '$password_file'" zerobyte-sftp
|
||||||
|
rm -f "$password_file"
|
||||||
|
|
||||||
|
printf '%s\n' "$repo_password_fingerprint" >"$repo_password_fingerprint_path"
|
||||||
|
chmod 0600 "$repo_password_fingerprint_path"
|
||||||
|
}
|
||||||
|
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y openssh-server
|
apt-get install -y apache2 apache2-utils nfs-kernel-server openssh-server restic rpcbind samba
|
||||||
|
|
||||||
id -u zerobyte-sftp >/dev/null 2>&1 || useradd --create-home --home-dir /home/zerobyte-sftp --shell /bin/bash zerobyte-sftp
|
id -u zerobyte-sftp >/dev/null 2>&1 || useradd --create-home --home-dir /home/zerobyte-sftp --shell /bin/bash zerobyte-sftp
|
||||||
|
id -u zerobyte-smb >/dev/null 2>&1 || useradd --create-home --home-dir /home/zerobyte-smb --shell /bin/bash zerobyte-smb
|
||||||
|
|
||||||
install -d -m 0755 /srv/zerobyte-backend-integration/fixtures/case-a/docs
|
install -d -m 0755 /srv/zerobyte-backend-integration/fixtures/case-a/docs
|
||||||
printf 'hello from zerobyte integration\n' >/srv/zerobyte-backend-integration/fixtures/case-a/hello.txt
|
printf 'hello from zerobyte integration\n' >/srv/zerobyte-backend-integration/fixtures/case-a/hello.txt
|
||||||
printf 'fixture documentation\n' >/srv/zerobyte-backend-integration/fixtures/case-a/docs/readme.md
|
printf 'fixture documentation\n' >/srv/zerobyte-backend-integration/fixtures/case-a/docs/readme.md
|
||||||
|
chown -R "$fixture_uid:$fixture_gid" /srv/zerobyte-backend-integration/fixtures
|
||||||
find /srv/zerobyte-backend-integration/fixtures -type d -exec chmod 0755 {} +
|
find /srv/zerobyte-backend-integration/fixtures -type d -exec chmod 0755 {} +
|
||||||
find /srv/zerobyte-backend-integration/fixtures -type f -exec chmod 0644 {} +
|
find /srv/zerobyte-backend-integration/fixtures -type f -exec chmod 0644 {} +
|
||||||
|
|
||||||
|
install -d -o zerobyte-sftp -g zerobyte-sftp -m 0700 /home/zerobyte-sftp
|
||||||
|
install -d -o zerobyte-sftp -g zerobyte-sftp -m 0700 /home/zerobyte-sftp/.ssh
|
||||||
|
printf '%s\n' "$public_key" >/home/zerobyte-sftp/.ssh/authorized_keys
|
||||||
|
chown zerobyte-sftp:zerobyte-sftp /home/zerobyte-sftp/.ssh/authorized_keys
|
||||||
|
chmod 0600 /home/zerobyte-sftp/.ssh/authorized_keys
|
||||||
|
|
||||||
|
printf '%s\n%s\n' "$smb_password" "$smb_password" | smbpasswd -a -s zerobyte-smb >/dev/null
|
||||||
|
smbpasswd -e zerobyte-smb >/dev/null
|
||||||
printf 'zerobyte-sftp:%s\n' "$sftp_password" | chpasswd
|
printf 'zerobyte-sftp:%s\n' "$sftp_password" | chpasswd
|
||||||
passwd -u zerobyte-sftp >/dev/null 2>&1 || true
|
passwd -u zerobyte-sftp >/dev/null 2>&1 || true
|
||||||
|
htpasswd -bc /etc/apache2/zerobyte-backend-integration.htpasswd zerobyte-webdav "$webdav_password" >/dev/null
|
||||||
|
|
||||||
|
if [[ ! -f "$repo_path/config" ]]; then
|
||||||
|
initialize_restic_repo
|
||||||
|
elif [[ ! -f "$repo_password_fingerprint_path" ]] || [[ "$(cat "$repo_password_fingerprint_path")" != "$repo_password_fingerprint" ]]; then
|
||||||
|
initialize_restic_repo
|
||||||
|
fi
|
||||||
|
|
||||||
|
write_file /etc/exports <<'EOF'
|
||||||
|
/srv/zerobyte-backend-integration/fixtures *(ro,sync,no_subtree_check,insecure)
|
||||||
|
EOF
|
||||||
|
exportfs -ra
|
||||||
|
systemctl unmask rpcbind rpcbind.socket >/dev/null 2>&1
|
||||||
|
systemctl start rpcbind.socket
|
||||||
|
systemctl start rpcbind
|
||||||
|
systemctl start proc-fs-nfsd.mount
|
||||||
|
systemctl restart nfs-kernel-server
|
||||||
|
|
||||||
|
write_file /etc/samba/smb.conf <<'EOF'
|
||||||
|
[zerobyte-backend-integration]
|
||||||
|
path = /srv/zerobyte-backend-integration/fixtures
|
||||||
|
browseable = yes
|
||||||
|
read only = yes
|
||||||
|
guest ok = no
|
||||||
|
valid users = zerobyte-smb
|
||||||
|
EOF
|
||||||
|
|
||||||
|
install -d -o www-data -g www-data -m 0755 /var/lib/dav
|
||||||
|
a2enmod dav dav_fs auth_basic >/dev/null
|
||||||
|
printf 'ServerName localhost\n' >/etc/apache2/conf-available/zerobyte-backend-integration-servername.conf
|
||||||
|
a2enconf zerobyte-backend-integration-servername >/dev/null
|
||||||
|
write_file /etc/apache2/sites-available/zerobyte-backend-integration-dav.conf <<'EOF'
|
||||||
|
Alias /zerobyte-backend-integration /srv/zerobyte-backend-integration/fixtures
|
||||||
|
|
||||||
|
DAVLockDB /var/lib/dav/lockdb
|
||||||
|
|
||||||
|
<Location /zerobyte-backend-integration>
|
||||||
|
DAV On
|
||||||
|
AuthType Basic
|
||||||
|
AuthName "Zerobyte Backend Integration WebDAV"
|
||||||
|
AuthUserFile /etc/apache2/zerobyte-backend-integration.htpasswd
|
||||||
|
Require valid-user
|
||||||
|
</Location>
|
||||||
|
|
||||||
|
<Directory /srv/zerobyte-backend-integration/fixtures>
|
||||||
|
Options Indexes FollowSymLinks
|
||||||
|
AllowOverride None
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
EOF
|
||||||
|
a2ensite zerobyte-backend-integration-dav >/dev/null
|
||||||
|
apache2ctl configtest
|
||||||
|
|
||||||
install -d -m 0700 "$legacy_sshd_dir"
|
install -d -m 0700 "$legacy_sshd_dir"
|
||||||
if [[ ! -f "$legacy_sshd_dir/ssh_host_rsa_key" ]]; then
|
if [[ ! -f "$legacy_sshd_dir/ssh_host_rsa_key" ]]; then
|
||||||
|
|
@ -109,6 +213,8 @@ EOF
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable --now zerobyte-backend-integration-legacy-sshd.service
|
systemctl enable --now zerobyte-backend-integration-legacy-sshd.service
|
||||||
|
|
||||||
|
systemctl restart apache2
|
||||||
|
systemctl restart smbd
|
||||||
systemctl restart ssh
|
systemctl restart ssh
|
||||||
systemctl restart zerobyte-backend-integration-legacy-sshd.service
|
systemctl restart zerobyte-backend-integration-legacy-sshd.service
|
||||||
systemctl is-active --quiet zerobyte-backend-integration-legacy-sshd.service
|
systemctl is-active --quiet zerobyte-backend-integration-legacy-sshd.service
|
||||||
|
|
@ -128,7 +234,13 @@ if ! ssh-keyscan -T 5 -p 2222 "$TARGET_HOST" >>"$KNOWN_HOSTS_PATH" 2>/dev/null;
|
||||||
fi
|
fi
|
||||||
|
|
||||||
INTEGRATION_HOST="$TARGET_HOST" \
|
INTEGRATION_HOST="$TARGET_HOST" \
|
||||||
|
FIXTURE_UID="$FIXTURE_UID" \
|
||||||
|
FIXTURE_GID="$FIXTURE_GID" \
|
||||||
|
SMB_PASSWORD="$SMB_PASSWORD" \
|
||||||
SFTP_PASSWORD="$SFTP_PASSWORD" \
|
SFTP_PASSWORD="$SFTP_PASSWORD" \
|
||||||
|
WEBDAV_PASSWORD="$WEBDAV_PASSWORD" \
|
||||||
|
RESTIC_PASSWORD="$RESTIC_PASSWORD" \
|
||||||
|
SFTP_KEY_PATH="$KEY_PATH" \
|
||||||
KNOWN_HOSTS_PATH="$KNOWN_HOSTS_PATH" \
|
KNOWN_HOSTS_PATH="$KNOWN_HOSTS_PATH" \
|
||||||
CONFIG_PATH="$CONFIG_PATH" \
|
CONFIG_PATH="$CONFIG_PATH" \
|
||||||
bun run "$SCRIPT_DIR/write-generated-config.ts"
|
bun run "$SCRIPT_DIR/write-generated-config.ts"
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,36 @@ function getRequiredEnv(name: string) {
|
||||||
return process.env[name]!;
|
return process.env[name]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRequiredNumberEnv(name: string) {
|
||||||
|
return Number.parseInt(getRequiredEnv(name), 10);
|
||||||
|
}
|
||||||
|
|
||||||
const host = getRequiredEnv("INTEGRATION_HOST");
|
const host = getRequiredEnv("INTEGRATION_HOST");
|
||||||
|
const fixtureUid = getRequiredNumberEnv("FIXTURE_UID");
|
||||||
|
const fixtureGid = getRequiredNumberEnv("FIXTURE_GID");
|
||||||
|
const smbPassword = getRequiredEnv("SMB_PASSWORD");
|
||||||
const sftpPassword = getRequiredEnv("SFTP_PASSWORD");
|
const sftpPassword = getRequiredEnv("SFTP_PASSWORD");
|
||||||
|
const webdavPassword = getRequiredEnv("WEBDAV_PASSWORD");
|
||||||
|
const resticPassword = getRequiredEnv("RESTIC_PASSWORD");
|
||||||
|
const privateKey = fs.readFileSync(getRequiredEnv("SFTP_KEY_PATH"), "utf8");
|
||||||
const knownHosts = fs.readFileSync(getRequiredEnv("KNOWN_HOSTS_PATH"), "utf8");
|
const knownHosts = fs.readFileSync(getRequiredEnv("KNOWN_HOSTS_PATH"), "utf8");
|
||||||
const configPath = getRequiredEnv("CONFIG_PATH");
|
const configPath = getRequiredEnv("CONFIG_PATH");
|
||||||
|
|
||||||
const fileText = "hello from zerobyte integration\n";
|
const fileText = "hello from zerobyte integration\n";
|
||||||
const readmeText = "fixture documentation\n";
|
const readmeText = "fixture documentation\n";
|
||||||
|
|
||||||
|
const nfsEntries = [
|
||||||
|
{ path: "hello.txt", type: "file", uid: fixtureUid, gid: fixtureGid, mode: "0644", text: fileText },
|
||||||
|
{ path: "docs", type: "directory", uid: fixtureUid, gid: fixtureGid, mode: "0755" },
|
||||||
|
{ path: "docs/readme.md", type: "file", uid: fixtureUid, gid: fixtureGid, mode: "0644", text: readmeText },
|
||||||
|
];
|
||||||
|
|
||||||
|
const smbEntries = [
|
||||||
|
{ path: "hello.txt", type: "file", uid: fixtureUid, gid: fixtureGid, mode: "0644", text: fileText },
|
||||||
|
{ path: "docs", type: "directory", uid: fixtureUid, gid: fixtureGid, mode: "1755" },
|
||||||
|
{ path: "docs/readme.md", type: "file", uid: fixtureUid, gid: fixtureGid, mode: "0644", text: readmeText },
|
||||||
|
];
|
||||||
|
|
||||||
const contentOnlyEntries = [
|
const contentOnlyEntries = [
|
||||||
{ path: "hello.txt", type: "file", text: fileText },
|
{ path: "hello.txt", type: "file", text: fileText },
|
||||||
{ path: "docs", type: "directory" },
|
{ path: "docs", type: "directory" },
|
||||||
|
|
@ -21,6 +43,71 @@ const contentOnlyEntries = [
|
||||||
const config = {
|
const config = {
|
||||||
version: 1,
|
version: 1,
|
||||||
scenarios: [
|
scenarios: [
|
||||||
|
{
|
||||||
|
id: "nfs-local-repo",
|
||||||
|
volume: {
|
||||||
|
backend: "nfs",
|
||||||
|
server: host,
|
||||||
|
exportPath: "/srv/zerobyte-backend-integration/fixtures",
|
||||||
|
port: 2049,
|
||||||
|
version: "4.1",
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
repository: { backend: "local", path: "repo-nfs" },
|
||||||
|
fixtureRoot: "case-a",
|
||||||
|
expectedEntries: nfsEntries,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "smb-local-repo",
|
||||||
|
volume: {
|
||||||
|
backend: "smb",
|
||||||
|
server: host,
|
||||||
|
share: "zerobyte-backend-integration",
|
||||||
|
username: "zerobyte-smb",
|
||||||
|
password: smbPassword,
|
||||||
|
mapToContainerUidGid: false,
|
||||||
|
vers: "3.0",
|
||||||
|
port: 445,
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
repository: { backend: "local", path: "repo-smb" },
|
||||||
|
fixtureRoot: "case-a",
|
||||||
|
expectedEntries: smbEntries,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sftp-local-repo",
|
||||||
|
volume: {
|
||||||
|
backend: "sftp",
|
||||||
|
host,
|
||||||
|
port: 22,
|
||||||
|
username: "zerobyte-sftp",
|
||||||
|
privateKey,
|
||||||
|
path: "/srv/zerobyte-backend-integration/fixtures",
|
||||||
|
readOnly: true,
|
||||||
|
skipHostKeyCheck: false,
|
||||||
|
knownHosts,
|
||||||
|
},
|
||||||
|
repository: { backend: "local", path: "repo-sftp-volume" },
|
||||||
|
fixtureRoot: "case-a",
|
||||||
|
expectedEntries: contentOnlyEntries,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sftp-password-local-repo",
|
||||||
|
volume: {
|
||||||
|
backend: "sftp",
|
||||||
|
host,
|
||||||
|
port: 22,
|
||||||
|
username: "zerobyte-sftp",
|
||||||
|
password: sftpPassword,
|
||||||
|
path: "/srv/zerobyte-backend-integration/fixtures",
|
||||||
|
readOnly: true,
|
||||||
|
skipHostKeyCheck: false,
|
||||||
|
knownHosts,
|
||||||
|
},
|
||||||
|
repository: { backend: "local", path: "repo-sftp-password-volume" },
|
||||||
|
fixtureRoot: "case-a",
|
||||||
|
expectedEntries: contentOnlyEntries,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "sftp-legacy-rsa-hostkey-local-repo",
|
id: "sftp-legacy-rsa-hostkey-local-repo",
|
||||||
volume: {
|
volume: {
|
||||||
|
|
@ -39,6 +126,47 @@ const config = {
|
||||||
fixtureRoot: "case-a",
|
fixtureRoot: "case-a",
|
||||||
expectedEntries: contentOnlyEntries,
|
expectedEntries: contentOnlyEntries,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "webdav-local-repo",
|
||||||
|
volume: {
|
||||||
|
backend: "webdav",
|
||||||
|
server: host,
|
||||||
|
path: "/zerobyte-backend-integration",
|
||||||
|
username: "zerobyte-webdav",
|
||||||
|
password: webdavPassword,
|
||||||
|
port: 80,
|
||||||
|
readOnly: true,
|
||||||
|
ssl: false,
|
||||||
|
},
|
||||||
|
repository: { backend: "local", path: "repo-webdav" },
|
||||||
|
fixtureRoot: "case-a",
|
||||||
|
expectedEntries: contentOnlyEntries,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "nfs-sftp-repository",
|
||||||
|
volume: {
|
||||||
|
backend: "nfs",
|
||||||
|
server: host,
|
||||||
|
exportPath: "/srv/zerobyte-backend-integration/fixtures",
|
||||||
|
port: 2049,
|
||||||
|
version: "4.1",
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
repository: {
|
||||||
|
backend: "sftp",
|
||||||
|
host,
|
||||||
|
port: 22,
|
||||||
|
user: "zerobyte-sftp",
|
||||||
|
path: "/srv/zerobyte-backend-integration/restic-repo",
|
||||||
|
privateKey,
|
||||||
|
skipHostKeyCheck: false,
|
||||||
|
knownHosts,
|
||||||
|
isExistingRepository: true,
|
||||||
|
customPassword: resticPassword,
|
||||||
|
},
|
||||||
|
fixtureRoot: "case-a",
|
||||||
|
expectedEntries: nfsEntries,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
2
app/test/integration/.gitignore
vendored
2
app/test/integration/.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
||||||
artifacts/*
|
|
||||||
!artifacts/.gitignore
|
|
||||||
2
app/test/integration/artifacts/.gitignore
vendored
2
app/test/integration/artifacts/.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
||||||
*
|
|
||||||
!.gitignore
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
ARG BASE_IMAGE=zerobyte-integration-runtime-base:latest
|
|
||||||
FROM ${BASE_IMAGE}
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENV NODE_ENV=test
|
|
||||||
|
|
||||||
COPY ./package.json ./bun.lock ./
|
|
||||||
COPY ./packages/core/package.json ./packages/core/package.json
|
|
||||||
COPY ./packages/contracts/package.json ./packages/contracts/package.json
|
|
||||||
COPY ./apps/agent/package.json ./apps/agent/package.json
|
|
||||||
COPY ./apps/docs/package.json ./apps/docs/package.json
|
|
||||||
|
|
||||||
RUN VITE_GIT_HOOKS=0 bun install --frozen-lockfile
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
CMD ["sh", "app/test/integration/infra/entrypoint.sh"]
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
services:
|
|
||||||
rustfs:
|
|
||||||
image: rustfs/rustfs:latest
|
|
||||||
command: ["/data"]
|
|
||||||
environment:
|
|
||||||
RUSTFS_ADDRESS: ":9000"
|
|
||||||
RUSTFS_ACCESS_KEY: "rustfsadmin"
|
|
||||||
RUSTFS_SECRET_KEY: "rustfsadmin"
|
|
||||||
RUSTFS_CONSOLE_ENABLE: "false"
|
|
||||||
volumes:
|
|
||||||
- rustfs-data:/data
|
|
||||||
|
|
||||||
rustfs-setup:
|
|
||||||
image: minio/mc:latest
|
|
||||||
depends_on:
|
|
||||||
- rustfs
|
|
||||||
entrypoint: ["/bin/sh", "-c"]
|
|
||||||
command:
|
|
||||||
- |
|
|
||||||
until mc alias set rustfs http://rustfs:9000 rustfsadmin rustfsadmin --api S3v4 --path on; do
|
|
||||||
echo "Waiting for RustFS..."
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
mc mb --ignore-existing --region us-east-1 rustfs/zerobyte-integration
|
|
||||||
|
|
||||||
sftp:
|
|
||||||
build:
|
|
||||||
context: ./sftp
|
|
||||||
environment:
|
|
||||||
SFTP_USER: "zerobyte-sftp"
|
|
||||||
SFTP_PASSWORD: "zerobyte-sftp-password"
|
|
||||||
SFTP_PUBLIC_KEY_PATH: "/run/zerobyte/sftp/id_ed25519.pub"
|
|
||||||
volumes:
|
|
||||||
- ../artifacts/sftp/id_ed25519.pub:/run/zerobyte/sftp/id_ed25519.pub:ro
|
|
||||||
- sftp-data:/srv/zerobyte-integration
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "ssh-keyscan", "-T", "2", "localhost"]
|
|
||||||
interval: 1s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 30
|
|
||||||
|
|
||||||
webdav:
|
|
||||||
build:
|
|
||||||
context: ./webdav
|
|
||||||
environment:
|
|
||||||
WEBDAV_USER: "zerobyte-webdav"
|
|
||||||
WEBDAV_PASSWORD: "zerobyte-webdav-password"
|
|
||||||
healthcheck:
|
|
||||||
test:
|
|
||||||
[
|
|
||||||
"CMD",
|
|
||||||
"curl",
|
|
||||||
"-fsS",
|
|
||||||
"-u",
|
|
||||||
"zerobyte-webdav:zerobyte-webdav-password",
|
|
||||||
"http://localhost/zerobyte-integration/case-a/hello.txt",
|
|
||||||
]
|
|
||||||
interval: 1s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 30
|
|
||||||
|
|
||||||
smb:
|
|
||||||
build:
|
|
||||||
context: ./smb
|
|
||||||
environment:
|
|
||||||
SMB_USER: "zerobyte-smb"
|
|
||||||
SMB_PASSWORD: "zerobyte-smb-password"
|
|
||||||
SMB_SHARE: "zerobyte-integration"
|
|
||||||
healthcheck:
|
|
||||||
test:
|
|
||||||
[
|
|
||||||
"CMD",
|
|
||||||
"smbclient",
|
|
||||||
"//localhost/zerobyte-integration",
|
|
||||||
"-U",
|
|
||||||
"zerobyte-smb%zerobyte-smb-password",
|
|
||||||
"-c",
|
|
||||||
"get case-a/hello.txt /tmp/zerobyte-smb-healthcheck.txt",
|
|
||||||
]
|
|
||||||
interval: 1s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 30
|
|
||||||
|
|
||||||
nfs:
|
|
||||||
build:
|
|
||||||
context: ./nfs
|
|
||||||
privileged: true
|
|
||||||
volumes:
|
|
||||||
- nfs-data:/srv/zerobyte-integration
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "sh", "-c", "exportfs -v | grep -q /srv/zerobyte-integration/fixtures"]
|
|
||||||
interval: 1s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 30
|
|
||||||
|
|
||||||
integration:
|
|
||||||
build:
|
|
||||||
context: ../../../..
|
|
||||||
dockerfile: app/test/integration/infra/Dockerfile
|
|
||||||
args:
|
|
||||||
BASE_IMAGE: zerobyte-integration-runtime-base:latest
|
|
||||||
depends_on:
|
|
||||||
rustfs-setup:
|
|
||||||
condition: service_completed_successfully
|
|
||||||
sftp:
|
|
||||||
condition: service_healthy
|
|
||||||
webdav:
|
|
||||||
condition: service_healthy
|
|
||||||
smb:
|
|
||||||
condition: service_healthy
|
|
||||||
nfs:
|
|
||||||
condition: service_healthy
|
|
||||||
cap_add:
|
|
||||||
- SYS_ADMIN
|
|
||||||
devices:
|
|
||||||
- /dev/fuse:/dev/fuse
|
|
||||||
volumes:
|
|
||||||
- ../artifacts:/app/app/test/integration/artifacts
|
|
||||||
- ../artifacts/sftp/id_ed25519:/run/zerobyte/sftp/id_ed25519:ro
|
|
||||||
environment:
|
|
||||||
LOG_LEVEL: "error"
|
|
||||||
SFTP_PRIVATE_KEY_PATH: "/run/zerobyte/sftp/id_ed25519"
|
|
||||||
SKIP_VOLUME_MOUNT_INTEGRATION_TESTS: "${SKIP_VOLUME_MOUNT_INTEGRATION_TESTS:-false}"
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
rustfs-data:
|
|
||||||
sftp-data:
|
|
||||||
nfs-data:
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
#!/usr/bin/env sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
if [ -w /etc/fuse.conf ] && ! grep -q '^user_allow_other$' /etc/fuse.conf; then
|
|
||||||
printf '\nuser_allow_other\n' >>/etc/fuse.conf
|
|
||||||
fi
|
|
||||||
|
|
||||||
bun app/test/integration/src/write-rclone-config.ts
|
|
||||||
exec bunx --bun vitest run --config app/test/integration/vitest.config.ts
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
FROM alpine:3.21
|
|
||||||
|
|
||||||
RUN apk add --no-cache nfs-utils
|
|
||||||
|
|
||||||
COPY entrypoint.sh /usr/local/bin/zerobyte-nfs-entrypoint
|
|
||||||
RUN chmod +x /usr/local/bin/zerobyte-nfs-entrypoint
|
|
||||||
|
|
||||||
EXPOSE 2049
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/zerobyte-nfs-entrypoint"]
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
FIXTURE_ROOT="/srv/zerobyte-integration/fixtures"
|
|
||||||
|
|
||||||
install -d -m 0755 "$FIXTURE_ROOT/case-a/docs"
|
|
||||||
printf 'hello from zerobyte integration\n' >"$FIXTURE_ROOT/case-a/hello.txt"
|
|
||||||
printf 'fixture documentation\n' >"$FIXTURE_ROOT/case-a/docs/readme.md"
|
|
||||||
find "$FIXTURE_ROOT" -type d -exec chmod 0755 {} +
|
|
||||||
find "$FIXTURE_ROOT" -type f -exec chmod 0644 {} +
|
|
||||||
|
|
||||||
mkdir -p /run/rpc_pipefs /proc/fs/nfsd
|
|
||||||
mountpoint -q /proc/fs/nfsd || mount -t nfsd nfsd /proc/fs/nfsd
|
|
||||||
mountpoint -q /run/rpc_pipefs || mount -t rpc_pipefs rpc_pipefs /run/rpc_pipefs
|
|
||||||
printf '1\n' >/proc/fs/nfsd/nfsv4gracetime
|
|
||||||
printf '1\n' >/proc/fs/nfsd/nfsv4leasetime
|
|
||||||
|
|
||||||
cat >/etc/exports <<EOF
|
|
||||||
$FIXTURE_ROOT *(ro,sync,no_subtree_check,insecure,fsid=0)
|
|
||||||
EOF
|
|
||||||
|
|
||||||
rpcbind -w
|
|
||||||
rpc.mountd --no-udp --foreground &
|
|
||||||
exportfs -ra
|
|
||||||
rpc.nfsd -V 4 -V 4.1 8
|
|
||||||
|
|
||||||
trap 'exportfs -ua; rpc.nfsd 0' INT TERM
|
|
||||||
while kill -0 "$(pidof rpc.mountd)" 2>/dev/null; do
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
FROM alpine:3.21
|
|
||||||
|
|
||||||
RUN apk add --no-cache openssh-client openssh-server
|
|
||||||
|
|
||||||
COPY entrypoint.sh /usr/local/bin/zerobyte-sftp-entrypoint
|
|
||||||
RUN chmod +x /usr/local/bin/zerobyte-sftp-entrypoint
|
|
||||||
|
|
||||||
EXPOSE 22
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/zerobyte-sftp-entrypoint"]
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
SFTP_USER="${SFTP_USER:-zerobyte-sftp}"
|
|
||||||
SFTP_PASSWORD="${SFTP_PASSWORD:-zerobyte-sftp-password}"
|
|
||||||
SFTP_PUBLIC_KEY_PATH="${SFTP_PUBLIC_KEY_PATH:-/run/zerobyte/sftp/id_ed25519.pub}"
|
|
||||||
SERVICE_ROOT="/srv/zerobyte-integration"
|
|
||||||
|
|
||||||
ssh-keygen -A
|
|
||||||
install -d -m 0755 /run/sshd
|
|
||||||
|
|
||||||
if ! id "$SFTP_USER" >/dev/null 2>&1; then
|
|
||||||
addgroup -S "$SFTP_USER"
|
|
||||||
adduser -S -D -h "/home/$SFTP_USER" -s /bin/sh -G "$SFTP_USER" "$SFTP_USER"
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s:%s\n' "$SFTP_USER" "$SFTP_PASSWORD" | chpasswd
|
|
||||||
|
|
||||||
install -d -o "$SFTP_USER" -g "$SFTP_USER" -m 0700 "/home/$SFTP_USER/.ssh"
|
|
||||||
install -o "$SFTP_USER" -g "$SFTP_USER" -m 0600 "$SFTP_PUBLIC_KEY_PATH" "/home/$SFTP_USER/.ssh/authorized_keys"
|
|
||||||
|
|
||||||
install -d -o "$SFTP_USER" -g "$SFTP_USER" -m 0755 "$SERVICE_ROOT/fixtures/case-a/docs"
|
|
||||||
install -d -o "$SFTP_USER" -g "$SFTP_USER" -m 0755 "$SERVICE_ROOT/repos/sftp"
|
|
||||||
printf 'hello from zerobyte integration\n' >"$SERVICE_ROOT/fixtures/case-a/hello.txt"
|
|
||||||
printf 'fixture documentation\n' >"$SERVICE_ROOT/fixtures/case-a/docs/readme.md"
|
|
||||||
chown -R "$SFTP_USER:$SFTP_USER" "$SERVICE_ROOT"
|
|
||||||
|
|
||||||
cat >/etc/ssh/sshd_config <<EOF
|
|
||||||
Port 22
|
|
||||||
HostKey /etc/ssh/ssh_host_ed25519_key
|
|
||||||
HostKey /etc/ssh/ssh_host_rsa_key
|
|
||||||
PermitRootLogin no
|
|
||||||
PasswordAuthentication yes
|
|
||||||
PubkeyAuthentication yes
|
|
||||||
KbdInteractiveAuthentication no
|
|
||||||
Subsystem sftp internal-sftp
|
|
||||||
|
|
||||||
Match User $SFTP_USER
|
|
||||||
ForceCommand internal-sftp
|
|
||||||
AllowTcpForwarding no
|
|
||||||
X11Forwarding no
|
|
||||||
PasswordAuthentication yes
|
|
||||||
PubkeyAuthentication yes
|
|
||||||
EOF
|
|
||||||
|
|
||||||
exec /usr/sbin/sshd -D -e
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
FROM alpine:3.21
|
|
||||||
|
|
||||||
RUN apk add --no-cache samba samba-common-tools samba-client
|
|
||||||
|
|
||||||
COPY entrypoint.sh /usr/local/bin/zerobyte-smb-entrypoint
|
|
||||||
RUN chmod +x /usr/local/bin/zerobyte-smb-entrypoint
|
|
||||||
|
|
||||||
EXPOSE 445
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/zerobyte-smb-entrypoint"]
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
SMB_USER="${SMB_USER:-zerobyte-smb}"
|
|
||||||
SMB_PASSWORD="${SMB_PASSWORD:-zerobyte-smb-password}"
|
|
||||||
SMB_SHARE="${SMB_SHARE:-zerobyte-integration}"
|
|
||||||
FIXTURE_ROOT="/srv/zerobyte-integration/fixtures"
|
|
||||||
|
|
||||||
adduser -D -H -s /sbin/nologin "$SMB_USER" 2>/dev/null || true
|
|
||||||
|
|
||||||
install -d -m 0755 "$FIXTURE_ROOT/case-a/docs"
|
|
||||||
printf 'hello from zerobyte integration\n' >"$FIXTURE_ROOT/case-a/hello.txt"
|
|
||||||
printf 'fixture documentation\n' >"$FIXTURE_ROOT/case-a/docs/readme.md"
|
|
||||||
chown -R "$SMB_USER:$SMB_USER" "$FIXTURE_ROOT"
|
|
||||||
find "$FIXTURE_ROOT" -type d -exec chmod 0755 {} +
|
|
||||||
find "$FIXTURE_ROOT" -type f -exec chmod 0644 {} +
|
|
||||||
|
|
||||||
printf '%s\n%s\n' "$SMB_PASSWORD" "$SMB_PASSWORD" | smbpasswd -a -s "$SMB_USER" >/dev/null
|
|
||||||
smbpasswd -e "$SMB_USER" >/dev/null
|
|
||||||
|
|
||||||
cat >/etc/samba/smb.conf <<EOF
|
|
||||||
[global]
|
|
||||||
server role = standalone server
|
|
||||||
server string = Zerobyte Integration SMB
|
|
||||||
map to guest = Never
|
|
||||||
log file = /dev/stdout
|
|
||||||
max log size = 0
|
|
||||||
load printers = no
|
|
||||||
printing = bsd
|
|
||||||
disable spoolss = yes
|
|
||||||
bind interfaces only = yes
|
|
||||||
interfaces = lo eth0
|
|
||||||
smb ports = 445
|
|
||||||
|
|
||||||
[$SMB_SHARE]
|
|
||||||
path = $FIXTURE_ROOT
|
|
||||||
browseable = yes
|
|
||||||
read only = yes
|
|
||||||
guest ok = no
|
|
||||||
valid users = $SMB_USER
|
|
||||||
EOF
|
|
||||||
|
|
||||||
testparm -s >/dev/null
|
|
||||||
exec smbd --foreground --no-process-group --debug-stdout
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
FROM alpine:3.21
|
|
||||||
|
|
||||||
RUN apk add --no-cache apache2 apache2-utils apache2-webdav curl
|
|
||||||
|
|
||||||
COPY entrypoint.sh /usr/local/bin/zerobyte-webdav-entrypoint
|
|
||||||
RUN chmod +x /usr/local/bin/zerobyte-webdav-entrypoint
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/zerobyte-webdav-entrypoint"]
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
WEBDAV_USER="${WEBDAV_USER:-zerobyte-webdav}"
|
|
||||||
WEBDAV_PASSWORD="${WEBDAV_PASSWORD:-zerobyte-webdav-password}"
|
|
||||||
SERVICE_ROOT="/srv/zerobyte-integration"
|
|
||||||
FIXTURE_ROOT="$SERVICE_ROOT/fixtures"
|
|
||||||
LOCK_ROOT="/var/lib/zerobyte-webdav"
|
|
||||||
|
|
||||||
install -d -m 0755 "$FIXTURE_ROOT/case-a/docs"
|
|
||||||
printf 'hello from zerobyte integration\n' >"$FIXTURE_ROOT/case-a/hello.txt"
|
|
||||||
printf 'fixture documentation\n' >"$FIXTURE_ROOT/case-a/docs/readme.md"
|
|
||||||
chown -R apache:apache "$SERVICE_ROOT"
|
|
||||||
|
|
||||||
install -d -o apache -g apache -m 0755 "$LOCK_ROOT"
|
|
||||||
htpasswd -bc /etc/apache2/zerobyte-webdav.htpasswd "$WEBDAV_USER" "$WEBDAV_PASSWORD" >/dev/null
|
|
||||||
|
|
||||||
cat >/etc/apache2/conf.d/zerobyte-webdav.conf <<EOF
|
|
||||||
ServerName localhost
|
|
||||||
ErrorLog /proc/self/fd/2
|
|
||||||
CustomLog /proc/self/fd/1 combined
|
|
||||||
|
|
||||||
DAVLockDB $LOCK_ROOT/lockdb
|
|
||||||
Alias /zerobyte-integration $FIXTURE_ROOT
|
|
||||||
|
|
||||||
<Location /zerobyte-integration>
|
|
||||||
DAV On
|
|
||||||
AuthType Basic
|
|
||||||
AuthName "Zerobyte Integration WebDAV"
|
|
||||||
AuthUserFile /etc/apache2/zerobyte-webdav.htpasswd
|
|
||||||
Require valid-user
|
|
||||||
</Location>
|
|
||||||
|
|
||||||
<Directory $FIXTURE_ROOT>
|
|
||||||
Options Indexes FollowSymLinks
|
|
||||||
AllowOverride None
|
|
||||||
Require all granted
|
|
||||||
</Directory>
|
|
||||||
EOF
|
|
||||||
|
|
||||||
httpd -t
|
|
||||||
exec httpd -D FOREGROUND
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
script_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
repo_root="$(cd "$script_dir/../../.." && pwd)"
|
|
||||||
base_image="zerobyte-integration-runtime-base:latest"
|
|
||||||
compose_project="zerobyte-integration-$(basename "$repo_root" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9_-')"
|
|
||||||
artifacts_dir="$script_dir/artifacts"
|
|
||||||
sftp_artifacts_dir="$artifacts_dir/sftp"
|
|
||||||
compose_file="$script_dir/infra/docker-compose.yml"
|
|
||||||
docker_output_log="$artifacts_dir/docker-output.log"
|
|
||||||
compose=(docker compose -f "$compose_file" -p "$compose_project")
|
|
||||||
|
|
||||||
mkdir -p "$artifacts_dir"
|
|
||||||
if [[ -d "$artifacts_dir/runs" ]]; then
|
|
||||||
chmod -R u+rwX "$artifacts_dir/runs" || true
|
|
||||||
fi
|
|
||||||
rm -rf "$artifacts_dir/runs"
|
|
||||||
rm -rf "$sftp_artifacts_dir"
|
|
||||||
rm -f "$artifacts_dir/compose.log"
|
|
||||||
rm -f "$docker_output_log"
|
|
||||||
mkdir -p "$artifacts_dir/runs"
|
|
||||||
mkdir -p "$sftp_artifacts_dir"
|
|
||||||
|
|
||||||
ssh-keygen -q -t ed25519 -N "" -f "$sftp_artifacts_dir/id_ed25519"
|
|
||||||
chmod 600 "$sftp_artifacts_dir/id_ed25519"
|
|
||||||
chmod 644 "$sftp_artifacts_dir/id_ed25519.pub"
|
|
||||||
|
|
||||||
docker build --progress quiet --target runtime-tools -t "$base_image" "$repo_root" >"$docker_output_log" 2>&1
|
|
||||||
|
|
||||||
exit_code=0
|
|
||||||
"${compose[@]}" up --build --no-color --detach rustfs >>"$docker_output_log" 2>&1 || exit_code=$?
|
|
||||||
|
|
||||||
if [[ "$exit_code" -eq 0 ]]; then
|
|
||||||
"${compose[@]}" up --build --no-color --abort-on-container-exit --exit-code-from rustfs-setup rustfs-setup >>"$docker_output_log" 2>&1 || exit_code=$?
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$exit_code" -eq 0 ]]; then
|
|
||||||
volume_services=(sftp webdav smb)
|
|
||||||
if [[ "${SKIP_VOLUME_MOUNT_INTEGRATION_TESTS:-false}" != "true" ]]; then
|
|
||||||
volume_services+=(nfs)
|
|
||||||
fi
|
|
||||||
"${compose[@]}" up --build --no-color --detach --wait "${volume_services[@]}" >>"$docker_output_log" 2>&1 || exit_code=$?
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$exit_code" -eq 0 ]]; then
|
|
||||||
"${compose[@]}" run --rm --no-deps --build integration 2>>"$docker_output_log" || exit_code=$?
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$exit_code" -ne 0 ]]; then
|
|
||||||
"${compose[@]}" logs --no-color >"$artifacts_dir/compose.log" || true
|
|
||||||
echo "Integration Docker logs: $artifacts_dir/compose.log" >&2
|
|
||||||
echo "Integration Docker command output: $docker_output_log" >&2
|
|
||||||
fi
|
|
||||||
|
|
||||||
"${compose[@]}" down --volumes --remove-orphans >>"$docker_output_log" 2>&1 || true
|
|
||||||
|
|
||||||
exit "$exit_code"
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
export const RUSTFS_ENDPOINT = "http://rustfs:9000";
|
|
||||||
export const RUSTFS_ACCESS_KEY_ID = "rustfsadmin";
|
|
||||||
export const RUSTFS_SECRET_ACCESS_KEY = "rustfsadmin";
|
|
||||||
export const RUSTFS_BUCKET = "zerobyte-integration";
|
|
||||||
|
|
||||||
export const RCLONE_REMOTE = "e2e-rustfs";
|
|
||||||
export const RCLONE_CONFIG_DIR = "/root/.config/rclone";
|
|
||||||
export const RCLONE_CONFIG_FILE = path.join(RCLONE_CONFIG_DIR, "rclone.conf");
|
|
||||||
|
|
||||||
export const INTEGRATION_ORGANIZATION_ID = "integration-suite";
|
|
||||||
export const INTEGRATION_ARTIFACTS_DIR = path.resolve(import.meta.dirname, "../artifacts");
|
|
||||||
export const INTEGRATION_RUNS_DIR = path.join(INTEGRATION_ARTIFACTS_DIR, "runs");
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
import fs from "node:fs/promises";
|
|
||||||
import path from "node:path";
|
|
||||||
import { expect } from "vitest";
|
|
||||||
import type { ScenarioFixture } from "./fixture";
|
|
||||||
|
|
||||||
type ResticLsNode = {
|
|
||||||
path: string;
|
|
||||||
type: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const nodeByPath = (nodes: ResticLsNode[]) => new Map(nodes.map((node) => [path.normalize(node.path), node]));
|
|
||||||
|
|
||||||
export const assertSnapshotContainsFixture = (sourceRoot: string, nodes: ResticLsNode[], fixture: ScenarioFixture) => {
|
|
||||||
const nodesByPath = nodeByPath(nodes);
|
|
||||||
|
|
||||||
for (const entry of fixture.entries) {
|
|
||||||
const node = nodesByPath.get(path.join(sourceRoot, entry.relativePath));
|
|
||||||
expect(node?.type).toBe(entry.type === "directory" ? "dir" : entry.type);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const assertRestoredFixture = async (restoreRoot: string, fixture: ScenarioFixture) => {
|
|
||||||
for (const entry of fixture.entries) {
|
|
||||||
const entryPath = path.join(restoreRoot, entry.relativePath);
|
|
||||||
if (entry.type === "file") {
|
|
||||||
await expect(fs.readFile(entryPath, "utf8")).resolves.toBe(entry.content);
|
|
||||||
}
|
|
||||||
if (entry.type === "directory") {
|
|
||||||
const stats = await fs.stat(entryPath);
|
|
||||||
expect(stats.isDirectory()).toBe(true);
|
|
||||||
}
|
|
||||||
if (entry.type === "symlink") {
|
|
||||||
await expect(fs.readlink(entryPath)).resolves.toBe(entry.target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const assertFixtureSourceExists = async (fixture: ScenarioFixture) => {
|
|
||||||
const stats = await fs.stat(fixture.sourceRoot);
|
|
||||||
expect(stats.isDirectory()).toBe(true);
|
|
||||||
await assertRestoredFixture(fixture.sourceRoot, fixture);
|
|
||||||
};
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
import crypto from "node:crypto";
|
|
||||||
import fs from "node:fs/promises";
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
export type FixtureEntry =
|
|
||||||
| {
|
|
||||||
type: "file";
|
|
||||||
relativePath: string;
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "directory";
|
|
||||||
relativePath: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "symlink";
|
|
||||||
relativePath: string;
|
|
||||||
target: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ScenarioFixture = {
|
|
||||||
sourceRoot: string;
|
|
||||||
entries: FixtureEntry[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createScenarioFixture = async (workspace: string, scenarioId: string): Promise<ScenarioFixture> => {
|
|
||||||
const sourceRoot = path.join(workspace, "source");
|
|
||||||
const randomSuffix = crypto.randomBytes(8).toString("hex");
|
|
||||||
|
|
||||||
const fixture: ScenarioFixture = {
|
|
||||||
sourceRoot,
|
|
||||||
entries: [
|
|
||||||
{
|
|
||||||
type: "file",
|
|
||||||
relativePath: "regular.txt",
|
|
||||||
content: `regular fixture for ${scenarioId} (${randomSuffix})\n`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "directory",
|
|
||||||
relativePath: "nested",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "file",
|
|
||||||
relativePath: "nested/deep.txt",
|
|
||||||
content: `nested fixture for ${scenarioId} (${randomSuffix})\n`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "symlink",
|
|
||||||
relativePath: "regular-link",
|
|
||||||
target: "regular.txt",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const entry of fixture.entries) {
|
|
||||||
const entryPath = path.join(sourceRoot, entry.relativePath);
|
|
||||||
if (entry.type === "file") {
|
|
||||||
await fs.mkdir(path.dirname(entryPath), { recursive: true });
|
|
||||||
await fs.writeFile(entryPath, entry.content);
|
|
||||||
}
|
|
||||||
if (entry.type === "directory") {
|
|
||||||
await fs.mkdir(entryPath, { recursive: true });
|
|
||||||
}
|
|
||||||
if (entry.type === "symlink") {
|
|
||||||
await fs.mkdir(path.dirname(entryPath), { recursive: true });
|
|
||||||
await fs.symlink(entry.target, entryPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fixture;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createStaticVolumeFixture = (sourceRoot: string): ScenarioFixture => ({
|
|
||||||
sourceRoot,
|
|
||||||
entries: [
|
|
||||||
{
|
|
||||||
type: "file",
|
|
||||||
relativePath: "hello.txt",
|
|
||||||
content: "hello from zerobyte integration\n",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "directory",
|
|
||||||
relativePath: "docs",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "file",
|
|
||||||
relativePath: "docs/readme.md",
|
|
||||||
content: "fixture documentation\n",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import type { BackendConfig } from "@zerobyte/contracts/volumes";
|
|
||||||
|
|
||||||
export const NFS_HOST = "nfs";
|
|
||||||
export const NFS_PORT = 2049;
|
|
||||||
export const NFS_EXPORT_PATH = "/";
|
|
||||||
|
|
||||||
export const buildNfsVolumeConfig = (): BackendConfig => ({
|
|
||||||
backend: "nfs",
|
|
||||||
server: NFS_HOST,
|
|
||||||
exportPath: NFS_EXPORT_PATH,
|
|
||||||
port: NFS_PORT,
|
|
||||||
version: "4.1",
|
|
||||||
readOnly: true,
|
|
||||||
});
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import path from "node:path";
|
|
||||||
import { createRestic } from "@zerobyte/core/restic/server";
|
|
||||||
import { RCLONE_CONFIG_FILE } from "../constants";
|
|
||||||
|
|
||||||
export const createIntegrationRestic = (workspace: string, resticPassword: string) => {
|
|
||||||
return createRestic({
|
|
||||||
resolveSecret: async (value: string) => value,
|
|
||||||
getOrganizationResticPassword: async () => resticPassword,
|
|
||||||
resticCacheDir: path.join(workspace, "restic-cache"),
|
|
||||||
resticPassFile: path.join(workspace, "restic.pass"),
|
|
||||||
defaultExcludes: [],
|
|
||||||
rcloneConfigFile: RCLONE_CONFIG_FILE,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
import fs from "node:fs/promises";
|
|
||||||
import type { BackendConfig } from "@zerobyte/contracts/volumes";
|
|
||||||
import { safeExec } from "@zerobyte/core/node";
|
|
||||||
import type { RepositoryConfig } from "@zerobyte/core/restic";
|
|
||||||
|
|
||||||
export const SFTP_HOST = "sftp";
|
|
||||||
export const SFTP_PORT = 22;
|
|
||||||
export const SFTP_USERNAME = "zerobyte-sftp";
|
|
||||||
export const SFTP_PASSWORD = "zerobyte-sftp-password";
|
|
||||||
export const SFTP_FIXTURE_ROOT = "/srv/zerobyte-integration/fixtures";
|
|
||||||
export const SFTP_REPOSITORY_ROOT = "/srv/zerobyte-integration/repos";
|
|
||||||
|
|
||||||
export const readSftpPrivateKey = async () => {
|
|
||||||
const privateKeyPath = process.env.SFTP_PRIVATE_KEY_PATH;
|
|
||||||
if (!privateKeyPath) {
|
|
||||||
throw new Error("SFTP_PRIVATE_KEY_PATH is required for SFTP integration tests");
|
|
||||||
}
|
|
||||||
|
|
||||||
return fs.readFile(privateKeyPath, "utf8");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const scanSftpKnownHosts = async () => {
|
|
||||||
const result = await safeExec({
|
|
||||||
command: "ssh-keyscan",
|
|
||||||
args: ["-T", "5", SFTP_HOST],
|
|
||||||
timeout: 10_000,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.exitCode !== 0 || result.stdout.trim().length === 0) {
|
|
||||||
throw new Error(`Failed to scan SFTP known hosts: ${result.stderr || result.stdout}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.stdout;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const buildSftpPrivateKeyVolumeConfig = ({
|
|
||||||
privateKey,
|
|
||||||
knownHosts,
|
|
||||||
}: {
|
|
||||||
privateKey: string;
|
|
||||||
knownHosts: string;
|
|
||||||
}): BackendConfig => ({
|
|
||||||
backend: "sftp",
|
|
||||||
host: SFTP_HOST,
|
|
||||||
port: SFTP_PORT,
|
|
||||||
username: SFTP_USERNAME,
|
|
||||||
privateKey,
|
|
||||||
path: SFTP_FIXTURE_ROOT,
|
|
||||||
readOnly: true,
|
|
||||||
skipHostKeyCheck: false,
|
|
||||||
knownHosts,
|
|
||||||
allowLegacySshRsa: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const buildSftpPasswordVolumeConfig = ({ knownHosts }: { knownHosts: string }): BackendConfig => ({
|
|
||||||
backend: "sftp",
|
|
||||||
host: SFTP_HOST,
|
|
||||||
port: SFTP_PORT,
|
|
||||||
username: SFTP_USERNAME,
|
|
||||||
password: SFTP_PASSWORD,
|
|
||||||
path: SFTP_FIXTURE_ROOT,
|
|
||||||
readOnly: true,
|
|
||||||
skipHostKeyCheck: false,
|
|
||||||
knownHosts,
|
|
||||||
allowLegacySshRsa: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const buildSftpRepositoryConfig = ({
|
|
||||||
privateKey,
|
|
||||||
knownHosts,
|
|
||||||
path,
|
|
||||||
}: {
|
|
||||||
privateKey: string;
|
|
||||||
knownHosts: string;
|
|
||||||
path: string;
|
|
||||||
}): RepositoryConfig => ({
|
|
||||||
backend: "sftp",
|
|
||||||
host: SFTP_HOST,
|
|
||||||
port: SFTP_PORT,
|
|
||||||
user: SFTP_USERNAME,
|
|
||||||
path,
|
|
||||||
privateKey,
|
|
||||||
skipHostKeyCheck: false,
|
|
||||||
knownHosts,
|
|
||||||
allowLegacySshRsa: false,
|
|
||||||
});
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import type { BackendConfig } from "@zerobyte/contracts/volumes";
|
|
||||||
|
|
||||||
export const SMB_HOST = "smb";
|
|
||||||
export const SMB_PORT = 445;
|
|
||||||
export const SMB_SHARE = "zerobyte-integration";
|
|
||||||
export const SMB_USERNAME = "zerobyte-smb";
|
|
||||||
export const SMB_PASSWORD = "zerobyte-smb-password";
|
|
||||||
|
|
||||||
export const buildSmbVolumeConfig = (): BackendConfig => ({
|
|
||||||
backend: "smb",
|
|
||||||
server: SMB_HOST,
|
|
||||||
share: SMB_SHARE,
|
|
||||||
username: SMB_USERNAME,
|
|
||||||
password: SMB_PASSWORD,
|
|
||||||
mapToContainerUidGid: false,
|
|
||||||
vers: "3.0",
|
|
||||||
port: SMB_PORT,
|
|
||||||
readOnly: true,
|
|
||||||
});
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import type { BackendConfig } from "@zerobyte/contracts/volumes";
|
|
||||||
|
|
||||||
export const WEBDAV_HOST = "webdav";
|
|
||||||
export const WEBDAV_PORT = 80;
|
|
||||||
export const WEBDAV_USERNAME = "zerobyte-webdav";
|
|
||||||
export const WEBDAV_PASSWORD = "zerobyte-webdav-password";
|
|
||||||
export const WEBDAV_FIXTURE_ROOT = "/zerobyte-integration";
|
|
||||||
|
|
||||||
export const buildWebdavVolumeConfig = (): BackendConfig => ({
|
|
||||||
backend: "webdav",
|
|
||||||
server: WEBDAV_HOST,
|
|
||||||
path: WEBDAV_FIXTURE_ROOT,
|
|
||||||
username: WEBDAV_USERNAME,
|
|
||||||
password: WEBDAV_PASSWORD,
|
|
||||||
port: WEBDAV_PORT,
|
|
||||||
readOnly: true,
|
|
||||||
ssl: false,
|
|
||||||
});
|
|
||||||
|
|
@ -1,145 +0,0 @@
|
||||||
import crypto from "node:crypto";
|
|
||||||
import fs from "node:fs/promises";
|
|
||||||
import path from "node:path";
|
|
||||||
import { Effect } from "effect";
|
|
||||||
import type { RepositoryConfig } from "@zerobyte/core/restic";
|
|
||||||
import { expect, test } from "vitest";
|
|
||||||
import {
|
|
||||||
INTEGRATION_ORGANIZATION_ID,
|
|
||||||
INTEGRATION_RUNS_DIR,
|
|
||||||
RCLONE_REMOTE,
|
|
||||||
RUSTFS_ACCESS_KEY_ID,
|
|
||||||
RUSTFS_BUCKET,
|
|
||||||
RUSTFS_ENDPOINT,
|
|
||||||
RUSTFS_SECRET_ACCESS_KEY,
|
|
||||||
} from "./constants";
|
|
||||||
import { assertFixtureSourceExists, assertRestoredFixture, assertSnapshotContainsFixture } from "./helpers/assertions";
|
|
||||||
import { createScenarioFixture } from "./helpers/fixture";
|
|
||||||
import { createIntegrationRestic } from "./helpers/restic";
|
|
||||||
import {
|
|
||||||
buildSftpRepositoryConfig,
|
|
||||||
readSftpPrivateKey,
|
|
||||||
scanSftpKnownHosts,
|
|
||||||
SFTP_REPOSITORY_ROOT,
|
|
||||||
} from "./helpers/sftp";
|
|
||||||
|
|
||||||
type RepositoryScenario = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
createRepositoryConfig: (prefix: string) => RepositoryConfig | Promise<RepositoryConfig>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const scenarios: RepositoryScenario[] = [
|
|
||||||
{
|
|
||||||
id: "direct-s3",
|
|
||||||
name: "direct S3 repository",
|
|
||||||
createRepositoryConfig: (prefix) => ({
|
|
||||||
backend: "s3",
|
|
||||||
endpoint: RUSTFS_ENDPOINT,
|
|
||||||
bucket: `${RUSTFS_BUCKET}/${prefix}`,
|
|
||||||
accessKeyId: RUSTFS_ACCESS_KEY_ID,
|
|
||||||
secretAccessKey: RUSTFS_SECRET_ACCESS_KEY,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "rclone-s3",
|
|
||||||
name: "rclone repository over RustFS S3",
|
|
||||||
createRepositoryConfig: (prefix) => ({
|
|
||||||
backend: "rclone",
|
|
||||||
remote: RCLONE_REMOTE,
|
|
||||||
path: `${RUSTFS_BUCKET}/${prefix}`,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "sftp",
|
|
||||||
name: "SFTP repository",
|
|
||||||
createRepositoryConfig: async (prefix) =>
|
|
||||||
buildSftpRepositoryConfig({
|
|
||||||
privateKey: await readSftpPrivateKey(),
|
|
||||||
knownHosts: await scanSftpKnownHosts(),
|
|
||||||
path: `${SFTP_REPOSITORY_ROOT}/${prefix}`,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
test.concurrent.each(scenarios)("$name can backup, list, and restore fixture data", async (scenario) => {
|
|
||||||
const runId = crypto.randomUUID();
|
|
||||||
const repositoryPrefix = `${scenario.id}/${runId}`;
|
|
||||||
const workspace = path.join(INTEGRATION_RUNS_DIR, `${scenario.id}-${runId}`);
|
|
||||||
const restoreTarget = path.join(workspace, "restore");
|
|
||||||
const backupTag = `zerobyte-integration-${scenario.id}-${runId}`;
|
|
||||||
const resticPassword = `zerobyte-integration-${scenario.id}-${runId}-${crypto.randomBytes(16).toString("hex")}`;
|
|
||||||
const repositoryConfig = await scenario.createRepositoryConfig(repositoryPrefix);
|
|
||||||
const restic = createIntegrationRestic(workspace, resticPassword);
|
|
||||||
|
|
||||||
let passed = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fs.mkdir(workspace, { recursive: true });
|
|
||||||
const fixture = await createScenarioFixture(workspace, scenario.id);
|
|
||||||
await assertFixtureSourceExists(fixture);
|
|
||||||
|
|
||||||
const initResult = await Effect.runPromise(
|
|
||||||
restic.init(repositoryConfig, {
|
|
||||||
organizationId: INTEGRATION_ORGANIZATION_ID,
|
|
||||||
timeoutMs: 120_000,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(initResult.success).toBe(true);
|
|
||||||
expect(initResult.error).toBeNull();
|
|
||||||
|
|
||||||
const backupResult = await Effect.runPromise(
|
|
||||||
restic.backup(repositoryConfig, fixture.sourceRoot, {
|
|
||||||
organizationId: INTEGRATION_ORGANIZATION_ID,
|
|
||||||
tags: [backupTag],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(backupResult.exitCode).toBe(0);
|
|
||||||
expect(backupResult.warningDetails).toBeNull();
|
|
||||||
expect(backupResult.result?.snapshot_id).toEqual(expect.any(String));
|
|
||||||
|
|
||||||
const snapshotId = backupResult.result?.snapshot_id;
|
|
||||||
if (!snapshotId) {
|
|
||||||
throw new Error("Restic backup completed without a snapshot id");
|
|
||||||
}
|
|
||||||
|
|
||||||
const snapshots = await Effect.runPromise(
|
|
||||||
restic.snapshots(repositoryConfig, {
|
|
||||||
organizationId: INTEGRATION_ORGANIZATION_ID,
|
|
||||||
tags: [backupTag],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const snapshot = snapshots.find(
|
|
||||||
(candidate) => candidate.id === snapshotId || candidate.short_id === snapshotId,
|
|
||||||
);
|
|
||||||
expect(snapshot).toBeDefined();
|
|
||||||
expect(snapshot?.paths).toContain(fixture.sourceRoot);
|
|
||||||
|
|
||||||
const lsResult = await Effect.runPromise(
|
|
||||||
restic.ls(repositoryConfig, snapshotId, undefined, {
|
|
||||||
organizationId: INTEGRATION_ORGANIZATION_ID,
|
|
||||||
limit: 100,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
assertSnapshotContainsFixture(fixture.sourceRoot, lsResult.nodes, fixture);
|
|
||||||
|
|
||||||
await fs.rm(fixture.sourceRoot, { recursive: true, force: true });
|
|
||||||
|
|
||||||
await Effect.runPromise(
|
|
||||||
restic.restore(repositoryConfig, snapshotId, restoreTarget, {
|
|
||||||
organizationId: INTEGRATION_ORGANIZATION_ID,
|
|
||||||
basePath: fixture.sourceRoot,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
await assertRestoredFixture(restoreTarget, fixture);
|
|
||||||
|
|
||||||
passed = true;
|
|
||||||
} finally {
|
|
||||||
if (passed) {
|
|
||||||
await fs.rm(workspace, { recursive: true, force: true });
|
|
||||||
} else {
|
|
||||||
console.error(`Integration scenario artifacts retained in ${workspace}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,200 +0,0 @@
|
||||||
import crypto from "node:crypto";
|
|
||||||
import fs from "node:fs/promises";
|
|
||||||
import path from "node:path";
|
|
||||||
import type { RepositoryConfig } from "@zerobyte/core/restic";
|
|
||||||
import { Effect } from "effect";
|
|
||||||
import { expect, test } from "vitest";
|
|
||||||
import { makeNfsBackend } from "../../../../apps/agent/src/volume-host/backends/nfs";
|
|
||||||
import { makeSmbBackend } from "../../../../apps/agent/src/volume-host/backends/smb";
|
|
||||||
import { makeSftpBackend } from "../../../../apps/agent/src/volume-host/backends/sftp";
|
|
||||||
import { makeWebdavBackend } from "../../../../apps/agent/src/volume-host/backends/webdav";
|
|
||||||
import type { VolumeBackend } from "../../../../apps/agent/src/volume-host/types";
|
|
||||||
import { INTEGRATION_ORGANIZATION_ID, INTEGRATION_RUNS_DIR } from "./constants";
|
|
||||||
import { assertFixtureSourceExists, assertRestoredFixture, assertSnapshotContainsFixture } from "./helpers/assertions";
|
|
||||||
import { createStaticVolumeFixture } from "./helpers/fixture";
|
|
||||||
import { buildNfsVolumeConfig } from "./helpers/nfs";
|
|
||||||
import { createIntegrationRestic } from "./helpers/restic";
|
|
||||||
import {
|
|
||||||
buildSftpPasswordVolumeConfig,
|
|
||||||
buildSftpPrivateKeyVolumeConfig,
|
|
||||||
readSftpPrivateKey,
|
|
||||||
scanSftpKnownHosts,
|
|
||||||
} from "./helpers/sftp";
|
|
||||||
import { buildSmbVolumeConfig } from "./helpers/smb";
|
|
||||||
import { buildWebdavVolumeConfig } from "./helpers/webdav";
|
|
||||||
|
|
||||||
type VolumeScenario = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
createBackend: (mountPath: string) => Promise<VolumeBackend>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const scenarios: VolumeScenario[] = [
|
|
||||||
{
|
|
||||||
id: "sftp-local-repo",
|
|
||||||
name: "SFTP volume with private key auth and local repository",
|
|
||||||
createBackend: async (mountPath) => {
|
|
||||||
const knownHosts = await scanSftpKnownHosts();
|
|
||||||
const privateKey = await readSftpPrivateKey();
|
|
||||||
const config = buildSftpPrivateKeyVolumeConfig({ privateKey, knownHosts });
|
|
||||||
return makeSftpBackend(config, mountPath);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "sftp-password-local-repo",
|
|
||||||
name: "SFTP volume with password auth and local repository",
|
|
||||||
createBackend: async (mountPath) => {
|
|
||||||
const knownHosts = await scanSftpKnownHosts();
|
|
||||||
const config = buildSftpPasswordVolumeConfig({ knownHosts });
|
|
||||||
return makeSftpBackend(config, mountPath);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "webdav-local-repo",
|
|
||||||
name: "WebDAV volume with local repository",
|
|
||||||
createBackend: async (mountPath) => makeWebdavBackend(buildWebdavVolumeConfig(), mountPath),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "smb-local-repo",
|
|
||||||
name: "SMB volume with local repository",
|
|
||||||
createBackend: async (mountPath) => makeSmbBackend(buildSmbVolumeConfig(), mountPath),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "nfs-local-repo",
|
|
||||||
name: "NFS volume with local repository",
|
|
||||||
createBackend: async (mountPath) => makeNfsBackend(buildNfsVolumeConfig(), mountPath),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const volumeMountTest = process.env.SKIP_VOLUME_MOUNT_INTEGRATION_TESTS === "true" ? test.skip : test;
|
|
||||||
|
|
||||||
const makeDirectoriesWritable = async (root: string): Promise<void> => {
|
|
||||||
await fs.chmod(root, 0o700).catch(() => {});
|
|
||||||
|
|
||||||
const entries = await fs.readdir(root, { withFileTypes: true }).catch((error: NodeJS.ErrnoException) => {
|
|
||||||
if (error.code === "ENOENT") return [];
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
entries.map(async (entry) => {
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
await makeDirectoriesWritable(path.join(root, entry.name));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
volumeMountTest.concurrent.each(scenarios)("$name can backup and restore static fixture data", async (scenario) => {
|
|
||||||
const runId = crypto.randomUUID();
|
|
||||||
const workspace = path.join(INTEGRATION_RUNS_DIR, `${scenario.id}-${runId}`);
|
|
||||||
const mountPath = path.join(workspace, "mount");
|
|
||||||
const repositoryPath = path.join(workspace, "repo");
|
|
||||||
const restoreTarget = path.join(workspace, "restore");
|
|
||||||
const backupTag = `zerobyte-integration-${scenario.id}-${runId}`;
|
|
||||||
const resticPassword = `zerobyte-integration-${scenario.id}-${runId}-${crypto.randomBytes(16).toString("hex")}`;
|
|
||||||
const repositoryConfig: RepositoryConfig = { backend: "local", path: repositoryPath };
|
|
||||||
const restic = createIntegrationRestic(workspace, resticPassword);
|
|
||||||
|
|
||||||
let backend: VolumeBackend | undefined;
|
|
||||||
let passed = false;
|
|
||||||
let unmountFailed = false;
|
|
||||||
let cleanupError: Error | undefined;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fs.mkdir(workspace, { recursive: true });
|
|
||||||
|
|
||||||
const initResult = await Effect.runPromise(
|
|
||||||
restic.init(repositoryConfig, {
|
|
||||||
organizationId: INTEGRATION_ORGANIZATION_ID,
|
|
||||||
timeoutMs: 120_000,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(initResult.success).toBe(true);
|
|
||||||
expect(initResult.error).toBeNull();
|
|
||||||
|
|
||||||
backend = await scenario.createBackend(mountPath);
|
|
||||||
|
|
||||||
const mountResult = await backend.mount();
|
|
||||||
expect(mountResult.status).toBe("mounted");
|
|
||||||
|
|
||||||
const healthResult = await backend.checkHealth();
|
|
||||||
expect(healthResult.status).toBe("mounted");
|
|
||||||
|
|
||||||
const fixture = createStaticVolumeFixture(path.join(mountPath, "case-a"));
|
|
||||||
await assertFixtureSourceExists(fixture);
|
|
||||||
|
|
||||||
const backupResult = await Effect.runPromise(
|
|
||||||
restic.backup(repositoryConfig, fixture.sourceRoot, {
|
|
||||||
organizationId: INTEGRATION_ORGANIZATION_ID,
|
|
||||||
tags: [backupTag],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(backupResult.exitCode).toBe(0);
|
|
||||||
expect(backupResult.warningDetails).toBeNull();
|
|
||||||
expect(backupResult.result?.snapshot_id).toEqual(expect.any(String));
|
|
||||||
|
|
||||||
const snapshotId = backupResult.result?.snapshot_id;
|
|
||||||
if (!snapshotId) {
|
|
||||||
throw new Error("Restic backup completed without a snapshot id");
|
|
||||||
}
|
|
||||||
|
|
||||||
const snapshots = await Effect.runPromise(
|
|
||||||
restic.snapshots(repositoryConfig, {
|
|
||||||
organizationId: INTEGRATION_ORGANIZATION_ID,
|
|
||||||
tags: [backupTag],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const snapshot = snapshots.find(
|
|
||||||
(candidate) => candidate.id === snapshotId || candidate.short_id === snapshotId,
|
|
||||||
);
|
|
||||||
expect(snapshot).toBeDefined();
|
|
||||||
expect(snapshot?.paths).toContain(fixture.sourceRoot);
|
|
||||||
|
|
||||||
const lsResult = await Effect.runPromise(
|
|
||||||
restic.ls(repositoryConfig, snapshotId, undefined, {
|
|
||||||
organizationId: INTEGRATION_ORGANIZATION_ID,
|
|
||||||
limit: 100,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
assertSnapshotContainsFixture(fixture.sourceRoot, lsResult.nodes, fixture);
|
|
||||||
|
|
||||||
const unmountResult = await backend.unmount();
|
|
||||||
expect(unmountResult.status).toBe("unmounted");
|
|
||||||
backend = undefined;
|
|
||||||
|
|
||||||
await Effect.runPromise(
|
|
||||||
restic.restore(repositoryConfig, snapshotId, restoreTarget, {
|
|
||||||
organizationId: INTEGRATION_ORGANIZATION_ID,
|
|
||||||
basePath: fixture.sourceRoot,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
await assertRestoredFixture(restoreTarget, fixture);
|
|
||||||
|
|
||||||
passed = true;
|
|
||||||
} finally {
|
|
||||||
if (backend) {
|
|
||||||
const unmountResult = await backend.unmount();
|
|
||||||
if (unmountResult.status === "error") {
|
|
||||||
unmountFailed = true;
|
|
||||||
console.error(`Failed to unmount ${scenario.id} volume at ${mountPath}: ${unmountResult.error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (passed && !unmountFailed) {
|
|
||||||
await makeDirectoriesWritable(workspace);
|
|
||||||
await fs.rm(workspace, { recursive: true, force: true });
|
|
||||||
} else {
|
|
||||||
console.error(`Integration scenario artifacts retained in ${workspace}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (passed && unmountFailed) {
|
|
||||||
cleanupError = new Error(`Failed to unmount ${scenario.id} volume at ${mountPath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleanupError) {
|
|
||||||
throw cleanupError;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import fs from "node:fs/promises";
|
|
||||||
import {
|
|
||||||
RCLONE_CONFIG_DIR,
|
|
||||||
RCLONE_CONFIG_FILE,
|
|
||||||
RCLONE_REMOTE,
|
|
||||||
RUSTFS_ACCESS_KEY_ID,
|
|
||||||
RUSTFS_ENDPOINT,
|
|
||||||
RUSTFS_SECRET_ACCESS_KEY,
|
|
||||||
} from "./constants";
|
|
||||||
|
|
||||||
const rcloneConfig = `[${RCLONE_REMOTE}]
|
|
||||||
type = s3
|
|
||||||
provider = Minio
|
|
||||||
env_auth = false
|
|
||||||
access_key_id = ${RUSTFS_ACCESS_KEY_ID}
|
|
||||||
secret_access_key = ${RUSTFS_SECRET_ACCESS_KEY}
|
|
||||||
endpoint = ${RUSTFS_ENDPOINT}
|
|
||||||
force_path_style = true
|
|
||||||
acl = private
|
|
||||||
`;
|
|
||||||
|
|
||||||
await fs.mkdir(RCLONE_CONFIG_DIR, { recursive: true });
|
|
||||||
await fs.writeFile(RCLONE_CONFIG_FILE, rcloneConfig, { mode: 0o600 });
|
|
||||||
await fs.chmod(RCLONE_CONFIG_FILE, 0o600);
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
import path from "node:path";
|
|
||||||
import { defineConfig } from "vitest/config";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
"~": path.resolve(import.meta.dirname, "../.."),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
test: {
|
|
||||||
server: {
|
|
||||||
deps: {
|
|
||||||
inline: ["zod"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
name: "integration",
|
|
||||||
environment: "node",
|
|
||||||
include: ["app/test/integration/src/**/*.test.ts"],
|
|
||||||
testTimeout: 300_000,
|
|
||||||
hookTimeout: 300_000,
|
|
||||||
maxConcurrency: 4,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { Effect, Fiber, Queue, Ref } from "effect";
|
import { Effect, Fiber, Queue, Ref } from "effect";
|
||||||
import {
|
import {
|
||||||
AGENT_PROTOCOL_VERSION,
|
|
||||||
createAgentMessage,
|
createAgentMessage,
|
||||||
parseControllerMessage,
|
parseControllerMessage,
|
||||||
type AgentWireMessage,
|
type AgentWireMessage,
|
||||||
|
|
@ -142,7 +141,7 @@ export const createControllerSession = (ws: WebSocket): ControllerSession => {
|
||||||
offerOutbound(
|
offerOutbound(
|
||||||
createAgentMessage("agent.ready", {
|
createAgentMessage("agent.ready", {
|
||||||
agentId: "",
|
agentId: "",
|
||||||
protocolVersion: AGENT_PROTOCOL_VERSION,
|
protocolVersion: 1,
|
||||||
hostname: resolveResticHostname(),
|
hostname: resolveResticHostname(),
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
capabilities: { backup: true, restore: true, volume: true, restic: true },
|
capabilities: { backup: true, restore: true, volume: true, restic: true },
|
||||||
|
|
|
||||||
|
|
@ -30,16 +30,6 @@ Invited SSO users behave differently: Zerobyte creates their membership in the i
|
||||||
Zerobyte also has an instance-wide user role named `admin` for global tasks such as registration control and user management. That global role is separate from organization roles like `member`, `admin`, and `owner`.
|
Zerobyte also has an instance-wide user role named `admin` for global tasks such as registration control and user management. That global role is separate from organization roles like `member`, `admin`, and `owner`.
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
## Current permission model
|
|
||||||
|
|
||||||
Zerobyte does not currently provide fine-grained RBAC for backup operations. Authenticated organization members are trusted operators for the organization's repositories, volumes, backup schedules, restores, and notification destinations.
|
|
||||||
|
|
||||||
The `member`, `admin`, and `owner` roles mainly control organization-management actions. For example, standard members cannot manage members, SSO settings, invitations, or recovery-key downloads, but they can still operate backup resources inside the organization.
|
|
||||||
|
|
||||||
<Callout type="warn">
|
|
||||||
Only add users to an organization if you are comfortable with them managing that organization's backup resources and using the storage, volume, and notification capabilities configured for the Zerobyte instance.
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
## Invitations
|
## Invitations
|
||||||
|
|
||||||
Organization invitations carry four important pieces of data:
|
Organization invitations carry four important pieces of data:
|
||||||
|
|
|
||||||
|
|
@ -78,10 +78,6 @@ In practice, an **eligible local account** means an account that already belongs
|
||||||
|
|
||||||
The invitation UI lets you assign `member`, `admin`, or `owner` at invite time. The accepted membership is created with that invited role.
|
The invitation UI lets you assign `member`, `admin`, or `owner` at invite time. The accepted membership is created with that invited role.
|
||||||
|
|
||||||
<Callout type="warn">
|
|
||||||
Zerobyte does not currently provide fine-grained RBAC for backup operations. Invited members can operate backup resources inside the organization. Roles mainly restrict organization-management actions such as SSO settings, invitations, member management, and recovery-key downloads.
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
## Auto-linking semantics
|
## Auto-linking semantics
|
||||||
|
|
||||||
- Auto-linking is stored **per provider**, not globally.
|
- Auto-linking is stored **per provider**, not globally.
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ Mount the provisioning file and set `PROVISIONING_PATH`:
|
||||||
```yaml docker-compose.yml
|
```yaml docker-compose.yml
|
||||||
services:
|
services:
|
||||||
zerobyte:
|
zerobyte:
|
||||||
image: ghcr.io/nicotsx/zerobyte:v0.38
|
image: ghcr.io/nicotsx/zerobyte:v0.37
|
||||||
environment:
|
environment:
|
||||||
- PROVISIONING_PATH=/config/provisioning.json
|
- PROVISIONING_PATH=/config/provisioning.json
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ If you run Traefik as your reverse proxy, add labels to the Zerobyte service in
|
||||||
```yaml docker-compose.yml
|
```yaml docker-compose.yml
|
||||||
services:
|
services:
|
||||||
zerobyte:
|
zerobyte:
|
||||||
image: ghcr.io/nicotsx/zerobyte:v0.38
|
image: ghcr.io/nicotsx/zerobyte:v0.37
|
||||||
container_name: zerobyte
|
container_name: zerobyte
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ services:
|
||||||
- "4096:4096"
|
- "4096:4096"
|
||||||
|
|
||||||
zerobyte:
|
zerobyte:
|
||||||
image: ghcr.io/nicotsx/zerobyte:v0.38
|
image: ghcr.io/nicotsx/zerobyte:v0.37
|
||||||
container_name: zerobyte
|
container_name: zerobyte
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# Uncomment if you need remote mounts (NFS/SMB/WebDAV):
|
# Uncomment if you need remote mounts (NFS/SMB/WebDAV):
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ Create a `docker-compose.yml` file with the following configuration:
|
||||||
```yaml docker-compose.yml
|
```yaml docker-compose.yml
|
||||||
services:
|
services:
|
||||||
zerobyte:
|
zerobyte:
|
||||||
image: ghcr.io/nicotsx/zerobyte:v0.38
|
image: ghcr.io/nicotsx/zerobyte:v0.37
|
||||||
container_name: zerobyte
|
container_name: zerobyte
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
cap_add:
|
cap_add:
|
||||||
|
|
@ -182,7 +182,7 @@ If you only need to back up locally mounted directories and don't require remote
|
||||||
```yaml docker-compose.yml
|
```yaml docker-compose.yml
|
||||||
services:
|
services:
|
||||||
zerobyte:
|
zerobyte:
|
||||||
image: ghcr.io/nicotsx/zerobyte:v0.38
|
image: ghcr.io/nicotsx/zerobyte:v0.37
|
||||||
container_name: zerobyte
|
container_name: zerobyte
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
|
|
@ -259,7 +259,7 @@ If you use provisioning, Zerobyte can resolve secrets from environment variables
|
||||||
```yaml docker-compose.yml
|
```yaml docker-compose.yml
|
||||||
services:
|
services:
|
||||||
zerobyte:
|
zerobyte:
|
||||||
image: ghcr.io/nicotsx/zerobyte:v0.38
|
image: ghcr.io/nicotsx/zerobyte:v0.37
|
||||||
container_name: zerobyte
|
container_name: zerobyte
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
cap_add:
|
cap_add:
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ Update your `docker-compose.yml` to mount the directory you want to backup:
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
zerobyte:
|
zerobyte:
|
||||||
image: ghcr.io/nicotsx/zerobyte:v0.38
|
image: ghcr.io/nicotsx/zerobyte:v0.37
|
||||||
container_name: zerobyte
|
container_name: zerobyte
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# ... other configuration ...
|
# ... other configuration ...
|
||||||
|
|
|
||||||
|
|
@ -19,21 +19,21 @@
|
||||||
"@phosphor-icons/react": "^2.1.10",
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
"@tailwindcss/vite": "^4.3.0",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
"@tanstack/react-devtools": "^0.10.5",
|
"@tanstack/react-devtools": "^0.10.5",
|
||||||
"@tanstack/react-router": "^1.170.10",
|
"@tanstack/react-router": "^1.170.8",
|
||||||
"@tanstack/react-router-devtools": "^1.167.0",
|
"@tanstack/react-router-devtools": "^1.167.0",
|
||||||
"@tanstack/react-router-ssr-query": "^1.167.1",
|
"@tanstack/react-router-ssr-query": "^1.167.0",
|
||||||
"@tanstack/react-start": "^1.168.18",
|
"@tanstack/react-start": "^1.168.13",
|
||||||
"@tanstack/router-plugin": "^1.168.13",
|
"@tanstack/router-plugin": "^1.168.11",
|
||||||
"@types/mdx": "^2.0.13",
|
"@types/mdx": "^2.0.13",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"fumadocs-core": "^16.9.3",
|
"fumadocs-core": "^16.9.1",
|
||||||
"fumadocs-mdx": "^15.0.10",
|
"fumadocs-mdx": "^14.3.2",
|
||||||
"fumadocs-ui": "^16.9.3",
|
"fumadocs-ui": "^16.9.1",
|
||||||
"lucide-react": "^1.17.0",
|
"lucide-react": "^1.16.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
"shadcn": "^4.8.3",
|
"shadcn": "^4.8.0",
|
||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.0",
|
||||||
"tw-animate-css": "^1.4.0"
|
"tw-animate-css": "^1.4.0"
|
||||||
|
|
@ -52,7 +52,7 @@
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"vite": "^8.0.14",
|
"vite": "^8.0.14",
|
||||||
"vite-plugin-killer-instincts": "^1.1.0",
|
"vite-plugin-killer-instincts": "^1.1.0",
|
||||||
"vite-plus": "^0.1.23",
|
"vite-plus": "^0.1.22",
|
||||||
"vitest": "^4.1.7",
|
"vitest": "^4.1.7",
|
||||||
"wrangler": "^4.95.0"
|
"wrangler": "^4.95.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
367
bun.lock
367
bun.lock
|
|
@ -5,14 +5,14 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "zerobyte",
|
"name": "zerobyte",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@better-auth/passkey": "^1.6.12",
|
"@better-auth/passkey": "^1.6.11",
|
||||||
"@better-auth/sso": "^1.6.12",
|
"@better-auth/sso": "^1.6.11",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@hono/standard-validator": "^0.2.2",
|
"@hono/standard-validator": "^0.2.2",
|
||||||
"@hookform/resolvers": "^5.4.0",
|
"@hookform/resolvers": "^5.4.0",
|
||||||
"@inquirer/prompts": "^8.5.1",
|
"@inquirer/prompts": "^8.5.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
|
@ -31,20 +31,20 @@
|
||||||
"@tanstack/react-hotkeys": "^0.10.0",
|
"@tanstack/react-hotkeys": "^0.10.0",
|
||||||
"@tanstack/react-query": "^5.100.14",
|
"@tanstack/react-query": "^5.100.14",
|
||||||
"@tanstack/react-query-devtools": "^5.100.14",
|
"@tanstack/react-query-devtools": "^5.100.14",
|
||||||
"@tanstack/react-router": "^1.170.10",
|
"@tanstack/react-router": "^1.170.8",
|
||||||
"@tanstack/react-router-ssr-query": "^1.167.1",
|
"@tanstack/react-router-ssr-query": "^1.167.0",
|
||||||
"@tanstack/react-start": "^1.168.18",
|
"@tanstack/react-start": "^1.168.13",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@zerobyte/contracts": "workspace:*",
|
"@zerobyte/contracts": "workspace:*",
|
||||||
"@zerobyte/core": "workspace:*",
|
"@zerobyte/core": "workspace:*",
|
||||||
"better-auth": "^1.6.12",
|
"better-auth": "^1.6.11",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"commander": "^15.0.0",
|
"commander": "^14.0.3",
|
||||||
"consola": "^3.4.2",
|
"consola": "^3.4.2",
|
||||||
"content-disposition": "^2.0.1",
|
"content-disposition": "^2.0.0",
|
||||||
"cron-parser": "^5.5.0",
|
"cron-parser": "^5.5.0",
|
||||||
"date-fns": "^4.4.0",
|
"date-fns": "^4.3.0",
|
||||||
"dither-plugin": "^1.1.1",
|
"dither-plugin": "^1.1.1",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"drizzle-orm": "^1.0.0-rc.4-5d5b77c",
|
"drizzle-orm": "^1.0.0-rc.4-5d5b77c",
|
||||||
|
|
@ -55,12 +55,11 @@
|
||||||
"hono-rate-limiter": "^0.5.3",
|
"hono-rate-limiter": "^0.5.3",
|
||||||
"http-errors-enhanced": "^4.0.2",
|
"http-errors-enhanced": "^4.0.2",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"ipaddr.js": "^2.4.0",
|
"lucide-react": "^1.16.0",
|
||||||
"lucide-react": "^1.17.0",
|
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
"react-hook-form": "^7.77.0",
|
"react-hook-form": "^7.76.1",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"recharts": "3.8.1",
|
"recharts": "3.8.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
|
|
@ -72,7 +71,7 @@
|
||||||
"zod": "^4.4.3",
|
"zod": "^4.4.3",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@better-auth/utils": "0.4.1",
|
"@better-auth/utils": "0.4.0",
|
||||||
"@effect/language-service": "^0.86.2",
|
"@effect/language-service": "^0.86.2",
|
||||||
"@faker-js/faker": "^10.4.0",
|
"@faker-js/faker": "^10.4.0",
|
||||||
"@hey-api/openapi-ts": "^0.97.3",
|
"@hey-api/openapi-ts": "^0.97.3",
|
||||||
|
|
@ -95,7 +94,7 @@
|
||||||
"@vitejs/plugin-react": "^6.0.2",
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
"babel-plugin-react-compiler": "^1.0.0",
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"drizzle-kit": "^1.0.0-rc.4-ca0f029",
|
"drizzle-kit": "^1.0.0-rc.4-5d5b77c",
|
||||||
"fast-check": "^4.8.0",
|
"fast-check": "^4.8.0",
|
||||||
"happy-dom": "^20.9.0",
|
"happy-dom": "^20.9.0",
|
||||||
"msw": "^2.14.6",
|
"msw": "^2.14.6",
|
||||||
|
|
@ -105,7 +104,7 @@
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"vite": "^8.0.14",
|
"vite": "^8.0.14",
|
||||||
"vite-plus": "^0.1.23",
|
"vite-plus": "^0.1.22",
|
||||||
"vitest": "^4.1.7",
|
"vitest": "^4.1.7",
|
||||||
"wait-for-expect": "^4.0.0",
|
"wait-for-expect": "^4.0.0",
|
||||||
},
|
},
|
||||||
|
|
@ -132,21 +131,21 @@
|
||||||
"@phosphor-icons/react": "^2.1.10",
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
"@tailwindcss/vite": "^4.3.0",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
"@tanstack/react-devtools": "^0.10.5",
|
"@tanstack/react-devtools": "^0.10.5",
|
||||||
"@tanstack/react-router": "^1.170.10",
|
"@tanstack/react-router": "^1.170.8",
|
||||||
"@tanstack/react-router-devtools": "^1.167.0",
|
"@tanstack/react-router-devtools": "^1.167.0",
|
||||||
"@tanstack/react-router-ssr-query": "^1.167.1",
|
"@tanstack/react-router-ssr-query": "^1.167.0",
|
||||||
"@tanstack/react-start": "^1.168.18",
|
"@tanstack/react-start": "^1.168.13",
|
||||||
"@tanstack/router-plugin": "^1.168.13",
|
"@tanstack/router-plugin": "^1.168.11",
|
||||||
"@types/mdx": "^2.0.13",
|
"@types/mdx": "^2.0.13",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"fumadocs-core": "^16.9.3",
|
"fumadocs-core": "^16.9.1",
|
||||||
"fumadocs-mdx": "^15.0.10",
|
"fumadocs-mdx": "^14.3.2",
|
||||||
"fumadocs-ui": "^16.9.3",
|
"fumadocs-ui": "^16.9.1",
|
||||||
"lucide-react": "^1.17.0",
|
"lucide-react": "^1.16.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
"shadcn": "^4.8.3",
|
"shadcn": "^4.8.0",
|
||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
|
|
@ -165,7 +164,7 @@
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"vite": "^8.0.14",
|
"vite": "^8.0.14",
|
||||||
"vite-plugin-killer-instincts": "^1.1.0",
|
"vite-plugin-killer-instincts": "^1.1.0",
|
||||||
"vite-plus": "^0.1.23",
|
"vite-plus": "^0.1.22",
|
||||||
"vitest": "^4.1.7",
|
"vitest": "^4.1.7",
|
||||||
"wrangler": "^4.95.0",
|
"wrangler": "^4.95.0",
|
||||||
},
|
},
|
||||||
|
|
@ -270,25 +269,25 @@
|
||||||
|
|
||||||
"@base-ui/utils": ["@base-ui/utils@0.2.9", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-x/PDDCYzoqPpjrdyb3VcyylTI2IjUXEtYDGi5foh7KsnmNJIIaVwA2GLgDH1dps1GgXiJbA60hM+AyuTfQzIvw=="],
|
"@base-ui/utils": ["@base-ui/utils@0.2.9", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-x/PDDCYzoqPpjrdyb3VcyylTI2IjUXEtYDGi5foh7KsnmNJIIaVwA2GLgDH1dps1GgXiJbA60hM+AyuTfQzIvw=="],
|
||||||
|
|
||||||
"@better-auth/core": ["@better-auth/core@1.6.12", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-6mXtYSYfo6TvHHCZAZmfjvIQQtBDWzWzwy9iIWPEoede2lP2SuJzkfIQNuTtIGzZcn7a9iuzIm1jWDBzfnBARg=="],
|
"@better-auth/core": ["@better-auth/core@1.6.11", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-LrwidLCV8azdMGjvtwp30nj9tIv1BwI3VhtC0UaGSjQkAVWw4bN42I8qwbxRziPeSQoj+zUVkOpxZzAWBDARtQ=="],
|
||||||
|
|
||||||
"@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.12", "", { "peerDependencies": { "@better-auth/core": "^1.6.12", "@better-auth/utils": "0.4.1", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-g0sKQstvXHH70s+TjAXo86cNyWV60ahhJm1sow27RyW41U10vfBehOFinU3GPESyxl/fEr9D27rk3jdl6E3l3A=="],
|
"@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-4jpkETIGZOHCf7BK4jnu22fdN6jjomH0/HhEzkaWy3+Eppi5PYlHTF/460jrTmA3Xc+Vqwp9t282ymHiEPypGw=="],
|
||||||
|
|
||||||
"@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.12", "", { "peerDependencies": { "@better-auth/core": "^1.6.12", "@better-auth/utils": "0.4.1", "kysely": "^0.28.17 || ^0.29.0" }, "optionalPeers": ["kysely"] }, "sha512-KhPwPmLj+MoTVGV6goPfCYf/7Fuiy2Q37GEWhvQdoFjkYKbGo995OoghBVNBnAYOakYvTYjG0JebCfiETBVX3g=="],
|
"@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "kysely": "^0.28.17" }, "optionalPeers": ["kysely"] }, "sha512-/g8M9RfIjdcZDnbstSUvQiINkvdNlCeZr248zwqx2/PVksQI1MhQofbzUn3RnQnbPKp0EPwpX/dR3oudRFenUg=="],
|
||||||
|
|
||||||
"@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.12", "", { "peerDependencies": { "@better-auth/core": "^1.6.12", "@better-auth/utils": "0.4.1" } }, "sha512-flblsePBCcB0DA6hewAOupxyypNTQczZvkNYvRrsVlBDIh0+vHBU/dTjoDmuQnZ3egTdFNnMeC+VrNnqt/GFUg=="],
|
"@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0" } }, "sha512-hpdfw0BBf8MuzLkIdmbcUZICbY9r/bhLO2RxSnkzT5+/O+0I0u2I8+m0YUP7vNllP/ZCKASHOYgXPLO75Z0f9Q=="],
|
||||||
|
|
||||||
"@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.12", "", { "peerDependencies": { "@better-auth/core": "^1.6.12", "@better-auth/utils": "0.4.1", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-IeiHZN9PtIyiqYgTDlrmm8sYI++5p1OI49uWB7LHg2+touiaNUGe0uWYymQpw1zq1e8FJxKlwvOc5vw6nGrI6g=="],
|
"@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-3Tor8rSv8vSEIMEaV2PFpPEuVhqc1gNoZ6eGvoh3LwExXXuj8madew6ob+H1pH7Aphn3Ar5PQ08AguT8TbwFAA=="],
|
||||||
|
|
||||||
"@better-auth/passkey": ["@better-auth/passkey@1.6.12", "", { "dependencies": { "@simplewebauthn/browser": "^13.2.2", "@simplewebauthn/server": "^13.2.3", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "^1.6.12", "@better-auth/utils": "0.4.1", "@better-fetch/fetch": "1.1.21", "better-auth": "^1.6.12", "better-call": "1.3.5", "nanostores": "^1.0.1" } }, "sha512-Mn7U49zFt7uaphLEWfc67wpT5P5BPTmyro7Y85ERLdpX5rwf8ltR/sAu9ZFzd9EfwAHnChttFSmjYlz0JnzX7w=="],
|
"@better-auth/passkey": ["@better-auth/passkey@1.6.11", "", { "dependencies": { "@simplewebauthn/browser": "^13.2.2", "@simplewebauthn/server": "^13.2.3", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "better-auth": "^1.6.11", "better-call": "1.3.5", "nanostores": "^1.0.1" } }, "sha512-QjL+OyiKRSHFRhSp2CSe7u5jnRL5G+Eh4bW9eV4WFZQ+2a/S+113kHQxPqxhy3Onb5cQhkT5Bhyz7cxKNDJTPw=="],
|
||||||
|
|
||||||
"@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.12", "", { "peerDependencies": { "@better-auth/core": "^1.6.12", "@better-auth/utils": "0.4.1", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-+GvU8vZ3aJUHDBuR5PxtU5OpPQS2T9ND7s2JYm63bD6rnYztLwEo8bwHL3BvsTwSvCjFHZCtsn1A+6qyoOzTMw=="],
|
"@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-Pw+7q7zTp+VSci1V+CYMvuxIbAeVMZLe4lRo46LJoAKMHfjFl5T/ycsyFvWs/DkWC7n9gZZzRDEbHp0I5FiKKw=="],
|
||||||
|
|
||||||
"@better-auth/sso": ["@better-auth/sso@1.6.12", "", { "dependencies": { "fast-xml-parser": "^5.8.0", "jose": "^6.1.3", "samlify": "~2.10.2", "tldts": "^6.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "^1.6.12", "@better-auth/utils": "0.4.1", "@better-fetch/fetch": "1.1.21", "better-auth": "^1.6.12", "better-call": "1.3.5" } }, "sha512-MsdGpkU9Glon4wrBhns7NQ+qkTByC6BQQ7RYakqG67jAHN3dERhWoiGNwWyJlXOPNZKjUinniGkpQngwYj/Vtg=="],
|
"@better-auth/sso": ["@better-auth/sso@1.6.11", "", { "dependencies": { "fast-xml-parser": "^5.5.7", "jose": "^6.1.3", "samlify": "~2.10.2", "tldts": "^6.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "better-auth": "^1.6.11", "better-call": "1.3.5" } }, "sha512-lJHmoCayp9Woh/MPKTHDfGq7k1oQbU2yz5tIOZXl/pzrgLxV7fMGo9aJCyabHkw3GHMjBes4byC6aakHYzpZIg=="],
|
||||||
|
|
||||||
"@better-auth/telemetry": ["@better-auth/telemetry@1.6.12", "", { "peerDependencies": { "@better-auth/core": "^1.6.12", "@better-auth/utils": "0.4.1", "@better-fetch/fetch": "1.1.21" } }, "sha512-g59qLPq9SROyku0X5tiZpXXiVrsbjB1QA6OctOt9svzj7NjCFBoCAO9QlBiOTUolo0l9CF6fLlc85PoBkY5RtA=="],
|
"@better-auth/telemetry": ["@better-auth/telemetry@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21" } }, "sha512-hsjDHc8MZbm6/AHeNdtywrWedXevnBjmdvnHTcZub+rTVjOv+Td0roI8USKuC6uUibmrl//2rJfVCsGbopihNA=="],
|
||||||
|
|
||||||
"@better-auth/utils": ["@better-auth/utils@0.4.1", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-SZBPRPF3z0nBvE5ygOkxae35wnnXPRShmqFo78S+qslLeFoPu/pMgnXAuNKFMMybac3tiLaVg1e3MQW5MC+1iA=="],
|
"@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
|
||||||
|
|
||||||
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="],
|
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="],
|
||||||
|
|
||||||
|
|
@ -484,35 +483,35 @@
|
||||||
|
|
||||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
||||||
|
|
||||||
"@inquirer/ansi": ["@inquirer/ansi@2.0.7", "", {}, "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q=="],
|
"@inquirer/ansi": ["@inquirer/ansi@2.0.6", "", {}, "sha512-I/INw4sHGlVZ/afZOckpLiDP9SmbMl1g/GCqeHjLw1Afw/0PlRs2tRFgTGWmdI0hoNuWZn3y2iHNmG1vyECyQQ=="],
|
||||||
|
|
||||||
"@inquirer/checkbox": ["@inquirer/checkbox@5.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/core": "^11.2.1", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw=="],
|
"@inquirer/checkbox": ["@inquirer/checkbox@5.2.0", "", { "dependencies": { "@inquirer/ansi": "^2.0.6", "@inquirer/core": "^11.2.0", "@inquirer/figures": "^2.0.6", "@inquirer/type": "^4.0.6" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-1HJt+3fqxblp/GQjdntSyoSHYBc0e3CzXVgjFpKA6qFLd9FHBBqwN8Co0xYH6t2JVUZrtFwZ4bBiwptkiLxyOg=="],
|
||||||
|
|
||||||
"@inquirer/confirm": ["@inquirer/confirm@6.0.13", "", { "dependencies": { "@inquirer/core": "^11.1.10", "@inquirer/type": "^4.0.5" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-wkGPC7yJ5WJk1DJ5SX7fzk+gfj4BM8cf5dDDi71B/551xHrdsZVRJOC0WyikXd0pEsb/9cLniuE4atbsMqmFkw=="],
|
"@inquirer/confirm": ["@inquirer/confirm@6.0.13", "", { "dependencies": { "@inquirer/core": "^11.1.10", "@inquirer/type": "^4.0.5" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-wkGPC7yJ5WJk1DJ5SX7fzk+gfj4BM8cf5dDDi71B/551xHrdsZVRJOC0WyikXd0pEsb/9cLniuE4atbsMqmFkw=="],
|
||||||
|
|
||||||
"@inquirer/core": ["@inquirer/core@11.1.10", "", { "dependencies": { "@inquirer/ansi": "^2.0.5", "@inquirer/figures": "^2.0.5", "@inquirer/type": "^4.0.5", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-a4Q5BXHQAHa9eO202sTaFCHFYVB3x5fauDuThEAdZ9gfn76pSxiKU7wWcEH0N1O0XmQvNfQNU6QXpiRxmYQx+A=="],
|
"@inquirer/core": ["@inquirer/core@11.1.10", "", { "dependencies": { "@inquirer/ansi": "^2.0.5", "@inquirer/figures": "^2.0.5", "@inquirer/type": "^4.0.5", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-a4Q5BXHQAHa9eO202sTaFCHFYVB3x5fauDuThEAdZ9gfn76pSxiKU7wWcEH0N1O0XmQvNfQNU6QXpiRxmYQx+A=="],
|
||||||
|
|
||||||
"@inquirer/editor": ["@inquirer/editor@5.2.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/external-editor": "^3.0.2", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-NFLxWmTPc2WORINoffcbVwB2+tujeNZ6Cu50HbXvTbk0fHSmhA5PgCckxXuyUniQvX7bhxkZi0nODfJIGsnCdA=="],
|
"@inquirer/editor": ["@inquirer/editor@5.2.0", "", { "dependencies": { "@inquirer/core": "^11.2.0", "@inquirer/external-editor": "^3.0.1", "@inquirer/type": "^4.0.6" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-/m+sgRmzSdK6HDtVnl3PmI6MnZC4O+LLezedoJcrX7mINhTjjb0hlC7aEDGZXkFTB4b5uQ0q59AhYTah88KbNg=="],
|
||||||
|
|
||||||
"@inquirer/expand": ["@inquirer/expand@5.1.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g=="],
|
"@inquirer/expand": ["@inquirer/expand@5.1.0", "", { "dependencies": { "@inquirer/core": "^11.2.0", "@inquirer/type": "^4.0.6" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-fR7g4BVnIcs+4NApF6C5byflNM/EULxSxsv/2Jvg+gmop0R6eBIPvZqE6RYnTy1tQTFnf9wyHkwNoQSZbofaGA=="],
|
||||||
|
|
||||||
"@inquirer/external-editor": ["@inquirer/external-editor@3.0.2", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-1X+KN2PinUcy/aS3f3OBSKJaWk+jBikMry4Qblj5ysqN4T+8BeA1rNJdTSOlQ2iOmvKg7ZsR4tDQvl1p0PLgKg=="],
|
"@inquirer/external-editor": ["@inquirer/external-editor@3.0.1", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-tam+Gwjsxg2sx3iUVPkAnhKT/yrk2rd2NAa7XJU/J8OYpU0ifXsnp12xlvzp/DCpWBXVv+vLQsqnpAWwUcWD5Q=="],
|
||||||
|
|
||||||
"@inquirer/figures": ["@inquirer/figures@2.0.7", "", {}, "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw=="],
|
"@inquirer/figures": ["@inquirer/figures@2.0.6", "", {}, "sha512-dsZgQtH2t5Q6ah3aPbZbeEZAxsD9qQu0DXf01AltuEfRTm+NoLN6+rLVbr+4edeEbNCp/wBNM6mALRWtsQpfkw=="],
|
||||||
|
|
||||||
"@inquirer/input": ["@inquirer/input@5.1.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-3wuHoDBBtU+o0TB4uP2R+ACdNrn6SgdZBc1YUaiU9GcfXNaTNUgSM/Ft7eQ3gYWP2H1MiT6glycmfWYpmPzVKw=="],
|
"@inquirer/input": ["@inquirer/input@5.1.0", "", { "dependencies": { "@inquirer/core": "^11.2.0", "@inquirer/type": "^4.0.6" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-sVZCz6P6e8tW5g2bSFel1oLpa6jK/u7BexFfrgTqR8syIdnHqy+iopnlSbYBZMsCK52chLjhGNBxt0eRqhsghw=="],
|
||||||
|
|
||||||
"@inquirer/number": ["@inquirer/number@4.1.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA=="],
|
"@inquirer/number": ["@inquirer/number@4.1.0", "", { "dependencies": { "@inquirer/core": "^11.2.0", "@inquirer/type": "^4.0.6" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-VMXB/XejCbaSTf9Xucl7dqjzzsaGsrs6XwSYXPbGZ2QbSuq/Gz8XamhSi9ClRubNXZlGry9xVg1tKkJdTDgCtQ=="],
|
||||||
|
|
||||||
"@inquirer/password": ["@inquirer/password@5.1.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg=="],
|
"@inquirer/password": ["@inquirer/password@5.1.0", "", { "dependencies": { "@inquirer/ansi": "^2.0.6", "@inquirer/core": "^11.2.0", "@inquirer/type": "^4.0.6" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5tqRuKCDIUxdPxTI/CuLnh914kz+WMPmURHKnZgui9gk43ebudEsdu4EwSn1CPSi5R+17YpBG+ba/YqTnRAcJA=="],
|
||||||
|
|
||||||
"@inquirer/prompts": ["@inquirer/prompts@8.5.1", "", { "dependencies": { "@inquirer/checkbox": "^5.2.1", "@inquirer/confirm": "^6.1.1", "@inquirer/editor": "^5.2.1", "@inquirer/expand": "^5.1.1", "@inquirer/input": "^5.1.1", "@inquirer/number": "^4.1.1", "@inquirer/password": "^5.1.1", "@inquirer/rawlist": "^5.3.1", "@inquirer/search": "^4.2.1", "@inquirer/select": "^5.2.1" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-EwbBgs5e3a5MwQglQ736lx0UY/bbirddCbwe32AJMzQVSeAQK1jmwtDu8lwG9Izp+cdPhZ9NvuEt8aFmav6iLw=="],
|
"@inquirer/prompts": ["@inquirer/prompts@8.5.0", "", { "dependencies": { "@inquirer/checkbox": "^5.2.0", "@inquirer/confirm": "^6.1.0", "@inquirer/editor": "^5.2.0", "@inquirer/expand": "^5.1.0", "@inquirer/input": "^5.1.0", "@inquirer/number": "^4.1.0", "@inquirer/password": "^5.1.0", "@inquirer/rawlist": "^5.3.0", "@inquirer/search": "^4.2.0", "@inquirer/select": "^5.2.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-pLjXOnY4y3R1mgyHP3pXD/8eXejp+L/dde/0N2NLKgKfMstqhNZrpvs7Wkzbl9FYFQh10LRQ7QZwq+cz9rrhyw=="],
|
||||||
|
|
||||||
"@inquirer/rawlist": ["@inquirer/rawlist@5.3.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og=="],
|
"@inquirer/rawlist": ["@inquirer/rawlist@5.3.0", "", { "dependencies": { "@inquirer/core": "^11.2.0", "@inquirer/type": "^4.0.6" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-p+vAeTAD+cGXjGleP1F5LXrX2ISxNDZm+lqeBpnJausNLSZskZZkcggwhomqP8Igx9oIjnoeOrw98xvdFvdm2w=="],
|
||||||
|
|
||||||
"@inquirer/search": ["@inquirer/search@4.2.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g=="],
|
"@inquirer/search": ["@inquirer/search@4.2.0", "", { "dependencies": { "@inquirer/core": "^11.2.0", "@inquirer/figures": "^2.0.6", "@inquirer/type": "^4.0.6" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-ByURoSGIaSl5O5Q0AmYmVmUsXbMUcBGNoA3FRL7TOyiA22IeFHymJKRkuILbOIlJwqnBk7AnPpseodyFUBzg+g=="],
|
||||||
|
|
||||||
"@inquirer/select": ["@inquirer/select@5.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/core": "^11.2.1", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw=="],
|
"@inquirer/select": ["@inquirer/select@5.2.0", "", { "dependencies": { "@inquirer/ansi": "^2.0.6", "@inquirer/core": "^11.2.0", "@inquirer/figures": "^2.0.6", "@inquirer/type": "^4.0.6" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-6IzkcmEbEXfgVbxZ2d1UyJFbCBoc6dTofulFmrYuomIp88HXiVqRbqbg4/mbfZhvnNo6xYmnYo2AEmDof6fQkg=="],
|
||||||
|
|
||||||
"@inquirer/type": ["@inquirer/type@4.0.5", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q=="],
|
"@inquirer/type": ["@inquirer/type@4.0.5", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q=="],
|
||||||
|
|
||||||
|
|
@ -642,9 +641,9 @@
|
||||||
|
|
||||||
"@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.120.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ys+upfqNtSu58huAhJMBKl3XCkGzyVFBlMlGPzHeFKgpFF/OdgNs1MMf8oaJIbgMH8ZxgGF7qfue39eJohmKIg=="],
|
"@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.120.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ys+upfqNtSu58huAhJMBKl3XCkGzyVFBlMlGPzHeFKgpFF/OdgNs1MMf8oaJIbgMH8ZxgGF7qfue39eJohmKIg=="],
|
||||||
|
|
||||||
"@oxc-project/runtime": ["@oxc-project/runtime@0.133.0", "", {}, "sha512-PkvjA1Lq5++V5S1E6Patr92ZVcieE6EalDr1VJTqv4BnjZdOUC4W3p8k1wMXSd5/2aFP4b/A6N5sg2Bkzcr9vQ=="],
|
"@oxc-project/runtime": ["@oxc-project/runtime@0.129.0", "", {}, "sha512-0+S67blQakgeNqoKGozOUp5rQBrz2ynXZ2QIINXZPiafsD0YL0UogB9hAWc1S7k6VSNwKYC/N7MqT0V6IzpHkQ=="],
|
||||||
|
|
||||||
"@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="],
|
"@oxc-project/types": ["@oxc-project/types@0.129.0", "", {}, "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg=="],
|
||||||
|
|
||||||
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.52.0", "", { "os": "android", "cpu": "arm" }, "sha512-17EMSJnQ9g+upVHrAUYDMfH5lvRKQ9Nvg8WtEoH72oDr1VpWz+7/o3tD97U1EToen2YAQ/68JmtDYkQUi20dfQ=="],
|
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.52.0", "", { "os": "android", "cpu": "arm" }, "sha512-17EMSJnQ9g+upVHrAUYDMfH5lvRKQ9Nvg8WtEoH72oDr1VpWz+7/o3tD97U1EToen2YAQ/68JmtDYkQUi20dfQ=="],
|
||||||
|
|
||||||
|
|
@ -684,55 +683,55 @@
|
||||||
|
|
||||||
"@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-q5xL7oeXkZdEtNZWBdvehJcmt+GRu9l2bK40yJs1jJXlqq+r0Hygb1rTjq+FM2o/2xyt4cufH6KRplHp3Jjsvw=="],
|
"@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-q5xL7oeXkZdEtNZWBdvehJcmt+GRu9l2bK40yJs1jJXlqq+r0Hygb1rTjq+FM2o/2xyt4cufH6KRplHp3Jjsvw=="],
|
||||||
|
|
||||||
"@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@0.23.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gOs9PVr2wEg4ox9z0aJo+RKhhImW86YL5N6yav8BK/rgPsIrwN/igSZ+pbRr723NFvUNKde9fgMhRA6JrXAOZw=="],
|
"@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@0.22.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4150Lpgc1YM09GcjA6GSrra1JoPjC7aOpfywLjWEY4vW0Sd1qKzqHF1WRaiw0/qUZ40OATYdv3aRd7ipPkWQbw=="],
|
||||||
|
|
||||||
"@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@0.23.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-kjJ8B+7n4tB9VJdxS5A9GdJt6/bYpzbu4lXp2uO1S3sRmCB5gDEABlGoiePNApRWaW+xqL4b4xgiE727jSLhuA=="],
|
"@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@0.22.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-vFWcPWYOgZs4HWcgS1EjUZg33NLcNfEYU49KGImmCfZWkflENrmBYV4HN/C0YeAPum6ZZ/goPSvQrB/cOD+NfA=="],
|
||||||
|
|
||||||
"@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@0.23.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-6dCZuKNu135seMXilkRk9SpCx6i1XgmiipYGalLij5WVRX6ZYS8c4xI7preN/zv9fCXhsQclTIMDu2Y/cytTjw=="],
|
"@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@0.22.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-6LiUpP0Zir3+29FvBm7Y28q/dBjSHqTZ5MhG1Ckw4fGhI4cAvbcwXaKvbjx1TP7rRmBNOoq/M5xdpHjTb+GAew=="],
|
||||||
|
|
||||||
"@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@0.23.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3bdilnyA7kmSTjK27rvjIjSxL5SIg3wt7vwNiRkouWB83ytssyKnuGvxSYJxgMEmFpSutzaBzcCUM2jDtPGcgA=="],
|
"@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@0.22.1", "", { "os": "linux", "cpu": "x64" }, "sha512-fuX1hEQfpHauUbXADsfqVhRzrUrGabzGXbj5wsp2vKhV5uk/Rze8Mba9GdjFGECzvXudMGqHqxB4r6jGRdhxVA=="],
|
||||||
|
|
||||||
"@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@0.23.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-j+OEp44SVYiQ+ZD+uttsX7u6L9SvmbbQ77SO1pSFCcJlsVMeCk8qZsjhKfGKuT/jIA+ipOJMVs/+pqUfObBWNw=="],
|
"@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@0.22.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SZidAj+jrbZf9ZjBEYW0tiNZ+KasqB2zgW26qdiPpQSF/DzURnPmXz651IeA9YsmbVdHGIooEHUmev6QJdquA=="],
|
||||||
|
|
||||||
"@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@0.23.0", "", { "os": "win32", "cpu": "x64" }, "sha512-5MyjFuqf+g8OUPJBSGWHJtmoWnzFJYyOg4To9WMQshZYEWig/vtu7JtJ03VWnzHv9LJkAUeApY0gVCOywFR/iQ=="],
|
"@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@0.22.1", "", { "os": "win32", "cpu": "x64" }, "sha512-QweSk9H5lFh5Y+WUf2Kq/OAN88V6+62ZwGhP38gqdRotI90luXSMkruFTj7Q2rYrzH4ZVNaSqx7NY8JpSfIzqg=="],
|
||||||
|
|
||||||
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.67.0", "", { "os": "android", "cpu": "arm" }, "sha512-VrSi571rDv1N8HaEDM+DEX8nmT0y9jJo8tzzW13vsOWTx59xQczCIJx68n2zWOXRT5YKZsOZXp4qkHN/10x4mw=="],
|
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.63.0", "", { "os": "android", "cpu": "arm" }, "sha512-A9xLtQt7i0OA1PoB/meog6kikXI9CdwEp7ZwQqmgnpKn3G3b1orvTDy8CQ6T7w1HvDrgWGB78PkFKcWgibcTCg=="],
|
||||||
|
|
||||||
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.67.0", "", { "os": "android", "cpu": "arm64" }, "sha512-l6+NdYxMoRohix5r5bbigW16LPicceCwGcQ6LKKuE1kUdjgFfQolJjrJsQYPFetIs78Gxj/G/f5TEGoTCwj9nQ=="],
|
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.63.0", "", { "os": "android", "cpu": "arm64" }, "sha512-SQo+ZMvdR9l3CxZp5W5gFNxSiDxclY6lOzzNpKYLF8asESpm3Pwumx0gER5T7aHLF1/2BAAtLD3DiDkdgy4V1A=="],
|
||||||
|
|
||||||
"@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.67.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jOzXxS1AxFxhImLIRbtGIMrEwaXcgMw3gR57WB1cRk8ai+vpr6726kxXqVvlNsrXtJ/FrmOm8RxlC0m8SW24Qg=="],
|
"@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.63.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6W82XjJDTmMnjg30427l0dufpnyLoq7wEukKdM6/g2VIybRVuQiBVh43EA4b+UxZ3+tLcKm+Or/pXGNgLCEU8g=="],
|
||||||
|
|
||||||
"@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.67.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-3DFAVY94OqjIZHXIPz37yGRSWwOFTAqChQ64/M69GYLawzP0KiwdhDNfqdKKYT0bTR/DNxmMnQsj3ns+8+X/Lg=="],
|
"@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.63.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-CnWd/YCuVG5W1BYkjJEVbJG11o526O9qAwBEQM+nh8K19CRFUkFdROXCyYkGmroHEYQe4vgQ6+lh3550Lp35Xw=="],
|
||||||
|
|
||||||
"@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.67.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-e4dDKZuLu8TR9DEBssWSDahlPgZBwojTTHZUvnjBRJfJJbpxYCjfjKfi0Z1+CSLMiJBwI2yCDtRM1XJQaARjmg=="],
|
"@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.63.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-a4eZAqrmtajqcxfdAzC+l7g3PaE3V8hpAYqqeD3fTxLXOMFdK3eNTZrU80n4dDEVm0JXy1aL5PqvqWldBl6zYA=="],
|
||||||
|
|
||||||
"@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.67.0", "", { "os": "linux", "cpu": "arm" }, "sha512-BKytFdcQzbITV3xlnzDUDTEDtbUMCCiC4EaNTDZ4FyT8gdNvBC4gfiLucXp/sQl0XU3p7syTlorUWVVVBZab2g=="],
|
"@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.63.0", "", { "os": "linux", "cpu": "arm" }, "sha512-tYUtU9TdbU3uXF5D62g5zXJ13iniFGhXQx5vp9cyEjGdbSAY3VdFBSaldYvyoDmgMZ0ZYuwQP1Y4t2Fhejwa0w=="],
|
||||||
|
|
||||||
"@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.67.0", "", { "os": "linux", "cpu": "arm" }, "sha512-XYAv0esBDX7BpTzRDjVX2Vdj+zndd8ll2dFQiaeQ6zTZr7A8GRDTN7fH3FP3jU+O0vCDx85oH/EtG7BzPgAXuw=="],
|
"@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.63.0", "", { "os": "linux", "cpu": "arm" }, "sha512-I5r3twFf776UZg9dmRo2xbrKt00tTkORXEVe0ctg4vdTkQvJAjiCHxnbAU2HL1AiJ9cqADA76MAliuilsAWnvg=="],
|
||||||
|
|
||||||
"@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.67.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-zizRMjA0i6u/2B0evgda04iycu+MoNuf1pBy6Eh+1CjC5wMEG7qN5zdDKTCvFc0KSYSDM9QTG3gjZHirgtQuKg=="],
|
"@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.63.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-t7ltUkg6FFh4b564QyGir8xIj/QZbXu8FlcRkcyW9+ztr/mfRHlvUOFd95pJCXi9s/L5DrUeWWgpXRS+V+6igQ=="],
|
||||||
|
|
||||||
"@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.67.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-zB/Tf6sUjmmvvbva9Gj3JTJ8rJ9t4I8/U0o6vSRtd0DRIsIuyegBwJAzhSUFQHdMijIRJkW0exs/yBhpw2S20w=="],
|
"@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.63.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Q5mmZy/XWjuYFUuQyYjOvZ5U/JkKEwnpir6hGxhh6HcdP0V/BKxLo8dqkfF/t7r7AguB17dfS/8+go5AQDRR6g=="],
|
||||||
|
|
||||||
"@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.67.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-kgU40Gt74CK0TCsF51KZymkIwN9U0BajKsMijB52zPqOeZU9NAHkA/NSQkZDHEaCakx42DxhXkODiAqf2b4Gug=="],
|
"@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.63.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-uBGtuZ0TzLB4x5wVa82HGNvYqY8buwDhyCnCP0R0gkk9szqVsP0MeTtD5HX7EsEuFIt+aYmYxuxeVxs3nTSwtQ=="],
|
||||||
|
|
||||||
"@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.67.0", "", { "os": "linux", "cpu": "none" }, "sha512-tOYhkk/iaG9aD3FvGpBFd1Lrw0x0RaVoJBxjUkfNzS50rC5NS5BteNCwgr8A2zCdADrIIoze6D7u6U5Ic++/iQ=="],
|
"@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.63.0", "", { "os": "linux", "cpu": "none" }, "sha512-h4s6FwxE+9MeA181o0dnDwHP32Y/bG8EiB/vrD6Ib+AMt6haigDc/0bUtI/sLmQDBMJnUfaCmtSSrEAqjtEVrA=="],
|
||||||
|
|
||||||
"@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.67.0", "", { "os": "linux", "cpu": "none" }, "sha512-sEtywrPb+0b+tHYl1SDCrw903fiC4eyKoNqzP3v+f2JT3Xcv4NEYG+P8rj+eEnX7IWhqV/xj8/JmcmVj21CXaA=="],
|
"@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.63.0", "", { "os": "linux", "cpu": "none" }, "sha512-2EaNcCBR8Mcjl5ARtuN3BdEpVkX7KpjSjMGZ/mJMIeaXgTtdz5ytg2VwygMSStA/k0ixfvZFoZOfjDEcouV5vQ=="],
|
||||||
|
|
||||||
"@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.67.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-BvR8Moa0zCLxroOx4vZaZN9nUfwAUpSTwjZdxZyKy4bv3PrzrXrxKR/ZQ0L9wNSvlPhnMJeZfa3q5w6ZCTuN6Q=="],
|
"@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.63.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4hlf/fd7TrYYl3QrWWD0GocqJefwMu3cHQhmi2FvEB/YOvFb5DZN3SMBaPi7B1TM5DeypkEtrVib674q1KKPg=="],
|
||||||
|
|
||||||
"@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.67.0", "", { "os": "linux", "cpu": "x64" }, "sha512-mm2cxM6fksOpq6l0uFws8BUGKAR4dNa/cZCn37Npq7PFbhD5HDJqWfnoIvTaeRKMy5XdS2tO0MA0qbHDrnXAAA=="],
|
"@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.63.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Vgq9rkRVcPcjbcH+ihYTfpeR7vCXfqpd+z5ItTGc0yYUV59L5ceHYN1iV4H9bKGV7Rn5hkVc7x3mSvHegduENA=="],
|
||||||
|
|
||||||
"@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.67.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WmbMuLapKyDlobMkXAaAL0Y+Uczh4LETfIfQsUpbId4Ip8Ai82/jqeYTOoUCkuuhBFapgqP253+d83tLKOksJg=="],
|
"@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.63.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3/Lkq/ncooA61rorrC+ZQed1Bc4VpGj+WnGsp58zmxKgvZ2vhreu+dcVyr3mX8NUpq7mfZ4gDDTou/yrF1Pd7A=="],
|
||||||
|
|
||||||
"@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.67.0", "", { "os": "none", "cpu": "arm64" }, "sha512-9g/PqxYJelzzTAOR5Y+RiRqdeydhEuXv2KxNeFcAKQ7UsvnWSY1OP4MsuPMbTO2Pf70tz7mFhl1j13H3fyh+8g=="],
|
"@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.63.0", "", { "os": "none", "cpu": "arm64" }, "sha512-0/EdD/6hDkx5Mfd769PTjvEM8mZ/6Dfukp1dBCL/2PjlIVGEtYdNZyok6ChqYPsT9JcFnlQnUeQzO0/1L/oC9w=="],
|
||||||
|
|
||||||
"@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.67.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-2VhwE6Gatb0vJGnN0TBuQMbKCOiZlSQ/zJvVWYLK4a9d4iDiJOen/yVQkGpmsJ90MuH66fzi0kEKI0jRQMDxGA=="],
|
"@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.63.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-wb0CUkN8ngwPiRQBjD1Cj0LsHeNvm+Xt6YBHDMtj2DVQVD6Oj8Ri7g6BD+KICf6LaBqZlmzOvy6nF9E/8yyGOg=="],
|
||||||
|
|
||||||
"@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.67.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-EQ3VExXfeM1InbE5+JjufhZZTWy+kHUwgt3yZR7gQ47Je/mE0WspQPan0OJznh493L5anM210YNJtH1PXjTSFg=="],
|
"@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.63.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-BX5iq+ovdNlVYhSn5qPMUIT0uwAwt2lmEnCnzK+Gkhw4DovIvhGb96OFhV8yzQNUnQxn/xGkOR+X+BLrLDNm8w=="],
|
||||||
|
|
||||||
"@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.67.0", "", { "os": "win32", "cpu": "x64" }, "sha512-bw24y+/1MHS4QDkons3YyHkPT9uCMoLHHgQhb+mb8NOjTYwub1CZ+K9Ngr8aO5DMrDrkqHwTzlTwFP2vS8Y/ZQ=="],
|
"@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.63.0", "", { "os": "win32", "cpu": "x64" }, "sha512-QeN/WELOfsXMeYwxvfgQrl6CbVftYUCZsGXHjXQd5Trccm8+i4gmtxaOui4xbJQaiDlviF8F3yLSBloQUeFsfA=="],
|
||||||
|
|
||||||
"@oxlint/plugins": ["@oxlint/plugins@1.61.0", "", {}, "sha512-nkOyZEF1vH527CkdQtOp1HMrVFEM4ResURvI2JFeGoup+h+43J/k/FgdOR9b9Isxg+Yae7qVDa7y3nssE8b3TQ=="],
|
"@oxlint/plugins": ["@oxlint/plugins@1.61.0", "", {}, "sha512-nkOyZEF1vH527CkdQtOp1HMrVFEM4ResURvI2JFeGoup+h+43J/k/FgdOR9b9Isxg+Yae7qVDa7y3nssE8b3TQ=="],
|
||||||
|
|
||||||
|
|
@ -914,19 +913,19 @@
|
||||||
|
|
||||||
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
|
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
|
||||||
|
|
||||||
"@shikijs/core": ["@shikijs/core@4.1.0", "", { "dependencies": { "@shikijs/primitive": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ=="],
|
"@shikijs/core": ["@shikijs/core@4.0.2", "", { "dependencies": { "@shikijs/primitive": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw=="],
|
||||||
|
|
||||||
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-YquhawCUgaBfhsS72e2Y/dI59gCBNPHu3fEO/tvLaXrTssxZrY5ddjtNLTwndrMgPo8b3IscE+xoICDzpTmlFQ=="],
|
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag=="],
|
||||||
|
|
||||||
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-axLpjVs45YBvvINa+dJF+NPW+KtFkNXsFr4SDw2BMj9GdeMnGxVB9PQb2xXlJYovslt/nz6giedAyOANkfc7hg=="],
|
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg=="],
|
||||||
|
|
||||||
"@shikijs/langs": ["@shikijs/langs@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-nwOMruEkbgdZfQ/b8CgpNBVOpvG1k0N5tbmgiFeqsan401+x3ILqlzZJowSla4Agmq4hG2Uf2wh5jLTEhR8VSg=="],
|
"@shikijs/langs": ["@shikijs/langs@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg=="],
|
||||||
|
|
||||||
"@shikijs/primitive": ["@shikijs/primitive@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw=="],
|
"@shikijs/primitive": ["@shikijs/primitive@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw=="],
|
||||||
|
|
||||||
"@shikijs/themes": ["@shikijs/themes@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-emCcTnUM7yO2wltYbaxm+yLvcCI4+h8XBKc4KmJ7EZUXoSGjcCHifkI//R4OFit9ewpg7H2/9tjOuXrT2v/Knw=="],
|
"@shikijs/themes": ["@shikijs/themes@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA=="],
|
||||||
|
|
||||||
"@shikijs/types": ["@shikijs/types@4.1.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA=="],
|
"@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="],
|
||||||
|
|
||||||
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||||
|
|
||||||
|
|
@ -1020,45 +1019,45 @@
|
||||||
|
|
||||||
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.100.14", "", { "dependencies": { "@tanstack/query-devtools": "5.100.14" }, "peerDependencies": { "@tanstack/react-query": "^5.100.14", "react": "^18 || ^19" } }, "sha512-JkP5VDgKOw3t/QSA1OABRHEqx8BuNs5MfvZRooNqdvN57SzTuGq3fKR1a2IH5rqa5HDLUm+FOXUEnB9ueHiLzg=="],
|
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.100.14", "", { "dependencies": { "@tanstack/query-devtools": "5.100.14" }, "peerDependencies": { "@tanstack/react-query": "^5.100.14", "react": "^18 || ^19" } }, "sha512-JkP5VDgKOw3t/QSA1OABRHEqx8BuNs5MfvZRooNqdvN57SzTuGq3fKR1a2IH5rqa5HDLUm+FOXUEnB9ueHiLzg=="],
|
||||||
|
|
||||||
"@tanstack/react-router": ["@tanstack/react-router@1.170.10", "", { "dependencies": { "@tanstack/history": "1.162.0", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.171.8", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-gVmWYq0ucWr+OB97Nud0YhKa9NOipB7/QrWI7wRZJJWEL0qUS8WPqAs0vA1f3IBXZpXmf8xxzf/tl5cmo4tlmA=="],
|
"@tanstack/react-router": ["@tanstack/react-router@1.170.8", "", { "dependencies": { "@tanstack/history": "1.162.0", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.171.6", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-Qw2ju6jjnIsMpuW+VrnHZWHuugqs592PWsnI56sG28qNhg14CgRLahOcNajfuJR9P4MxKGP94WVzmFKSYUz/ig=="],
|
||||||
|
|
||||||
"@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.167.0", "", { "dependencies": { "@tanstack/router-devtools-core": "1.168.0" }, "peerDependencies": { "@tanstack/react-router": "^1.170.0", "@tanstack/router-core": "^1.170.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-nGw095EG7IHx0h5NtlEmzf6vcCTaFNPWdTSuDKazajhN0ct/v/TkekJ9J6KYUCeV1a8/2ZmToc58M+0rrOyn7w=="],
|
"@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.167.0", "", { "dependencies": { "@tanstack/router-devtools-core": "1.168.0" }, "peerDependencies": { "@tanstack/react-router": "^1.170.0", "@tanstack/router-core": "^1.170.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-nGw095EG7IHx0h5NtlEmzf6vcCTaFNPWdTSuDKazajhN0ct/v/TkekJ9J6KYUCeV1a8/2ZmToc58M+0rrOyn7w=="],
|
||||||
|
|
||||||
"@tanstack/react-router-ssr-query": ["@tanstack/react-router-ssr-query@1.167.1", "", { "dependencies": { "@tanstack/router-ssr-query-core": "1.169.1" }, "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/react-query": ">=5.90.0", "@tanstack/react-router": ">=1.127.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-W9j5JPnBikyafvuUfykFfHIWod58OAbAAa5leNkXBcoDoocghMmu6w9uZOmUZvAWT7CSvgj5tBUtF7CM2OoHXQ=="],
|
"@tanstack/react-router-ssr-query": ["@tanstack/react-router-ssr-query@1.167.0", "", { "dependencies": { "@tanstack/router-ssr-query-core": "1.169.0" }, "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/react-query": ">=5.90.0", "@tanstack/react-router": ">=1.127.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-lJC/qySnlB0RaPCwCS4BQbdsDwyaPP2C8tzuEUsrwPTxm8TVYonYv3sptoSVhY0C2f8i5041X8gbRL7+lSY8BQ=="],
|
||||||
|
|
||||||
"@tanstack/react-start": ["@tanstack/react-start@1.168.18", "", { "dependencies": { "@tanstack/react-router": "1.170.10", "@tanstack/react-start-client": "1.168.7", "@tanstack/react-start-rsc": "0.1.17", "@tanstack/react-start-server": "1.167.13", "@tanstack/router-utils": "1.162.1", "@tanstack/start-client-core": "1.170.6", "@tanstack/start-plugin-core": "1.171.10", "@tanstack/start-server-core": "1.169.8", "pathe": "^2.0.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "@vitejs/plugin-rsc": "*", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "@vitejs/plugin-rsc", "vite"] }, "sha512-IRYbUPgUToyt1W9KJJB3oG/OUmqYOfHW521cPe5pZhJe3LkaTdtu7KpHsW4p6z+su8CfN7n9oofb0vY36lXRkg=="],
|
"@tanstack/react-start": ["@tanstack/react-start@1.168.13", "", { "dependencies": { "@tanstack/react-router": "1.170.8", "@tanstack/react-start-client": "1.168.4", "@tanstack/react-start-rsc": "0.1.13", "@tanstack/react-start-server": "1.167.9", "@tanstack/router-utils": "1.162.1", "@tanstack/start-client-core": "1.170.4", "@tanstack/start-plugin-core": "1.171.6", "@tanstack/start-server-core": "1.169.4", "pathe": "^2.0.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "@vitejs/plugin-rsc": "*", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "@vitejs/plugin-rsc", "vite"] }, "sha512-E2pHQ92NiND1/HiD5Ax71xFXxiRZ2reOfU5W4BqxUL5plap3p8xSw1c6L8Np1E60vsxknuPCYRZESKkRy/LkOA=="],
|
||||||
|
|
||||||
"@tanstack/react-start-client": ["@tanstack/react-start-client@1.168.7", "", { "dependencies": { "@tanstack/react-router": "1.170.10", "@tanstack/router-core": "1.171.8", "@tanstack/start-client-core": "1.170.6" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-ldvWrNvXb/EXAY+8uj0XbkVlP0Uh6Ddm63NNNKWf5V2BcQ6KRxiZgANXLWlKRczmnklZiwN2eWFexEJFEzetLQ=="],
|
"@tanstack/react-start-client": ["@tanstack/react-start-client@1.168.4", "", { "dependencies": { "@tanstack/react-router": "1.170.8", "@tanstack/router-core": "1.171.6", "@tanstack/start-client-core": "1.170.4" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-PDJ7xEuUKrlBiQz2PrVN9pD2ErmWeFpckYW1WUE8JCAeVi8U7C6rQNTQe4hQxBhycRfRdD53M6UfdWdQODIxyg=="],
|
||||||
|
|
||||||
"@tanstack/react-start-rsc": ["@tanstack/react-start-rsc@0.1.17", "", { "dependencies": { "@tanstack/react-router": "1.170.10", "@tanstack/react-start-server": "1.167.13", "@tanstack/router-core": "1.171.8", "@tanstack/router-utils": "1.162.1", "@tanstack/start-client-core": "1.170.6", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-plugin-core": "1.171.10", "@tanstack/start-server-core": "1.169.8", "@tanstack/start-storage-context": "1.167.10", "pathe": "^2.0.3" }, "peerDependencies": { "@rspack/core": ">=2.0.0-0", "@vitejs/plugin-rsc": ">=0.5.20", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "react-server-dom-rspack": ">=0.0.2" }, "optionalPeers": ["@rspack/core", "@vitejs/plugin-rsc", "react-server-dom-rspack"] }, "sha512-qOgOccz24i7mRnTzARSB6sWJa4kSon3AVyQ7NowJ1/o2VquHpblu7OY2sdxSbOqU0e3t6fSgqu08RTym47H1Fw=="],
|
"@tanstack/react-start-rsc": ["@tanstack/react-start-rsc@0.1.13", "", { "dependencies": { "@tanstack/react-router": "1.170.8", "@tanstack/react-start-server": "1.167.9", "@tanstack/router-core": "1.171.6", "@tanstack/router-utils": "1.162.1", "@tanstack/start-client-core": "1.170.4", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-plugin-core": "1.171.6", "@tanstack/start-server-core": "1.169.4", "@tanstack/start-storage-context": "1.167.8", "pathe": "^2.0.3" }, "peerDependencies": { "@rspack/core": ">=2.0.0-0", "@vitejs/plugin-rsc": ">=0.5.20", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "react-server-dom-rspack": ">=0.0.2" }, "optionalPeers": ["@rspack/core", "@vitejs/plugin-rsc", "react-server-dom-rspack"] }, "sha512-nl5pKkxy1RnRxOLjy/c3g/RKdQSQYWzK5iuLlsRaO9TbLuMhQlNAn255xQgVXG56G9xCtDg8/nD0ZycxSlSkWA=="],
|
||||||
|
|
||||||
"@tanstack/react-start-server": ["@tanstack/react-start-server@1.167.13", "", { "dependencies": { "@tanstack/history": "1.162.0", "@tanstack/react-router": "1.170.10", "@tanstack/router-core": "1.171.8", "@tanstack/start-client-core": "1.170.6", "@tanstack/start-server-core": "1.169.8" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-u/nfkW9M79HRx45uJipEi6txjfTJhYTFUirBSm7jqZfId7RRDfV+j38fipGhbIbCjCkHd6hPbUzJAnQFoM0uqg=="],
|
"@tanstack/react-start-server": ["@tanstack/react-start-server@1.167.9", "", { "dependencies": { "@tanstack/history": "1.162.0", "@tanstack/react-router": "1.170.8", "@tanstack/router-core": "1.171.6", "@tanstack/start-client-core": "1.170.4", "@tanstack/start-server-core": "1.169.4" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-a1SGeeoIEg411vEN6DThB2Bm5tiYBb0tCC/RaG8BSjRVtsY6kxD9cP1+LOpZwjRSgfdyqtSbe1v78ZDB9z0/uw=="],
|
||||||
|
|
||||||
"@tanstack/react-store": ["@tanstack/react-store@0.11.0", "", { "dependencies": { "@tanstack/store": "0.11.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-tX4YXh3PDkmpvGQWkWqKpzs/MSqbtuwY9dWdWhtV9Q50PmO+jOkUKIWIX4G85dwt7lxdHLXsiaEKPdKmC8F41w=="],
|
"@tanstack/react-store": ["@tanstack/react-store@0.11.0", "", { "dependencies": { "@tanstack/store": "0.11.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-tX4YXh3PDkmpvGQWkWqKpzs/MSqbtuwY9dWdWhtV9Q50PmO+jOkUKIWIX4G85dwt7lxdHLXsiaEKPdKmC8F41w=="],
|
||||||
|
|
||||||
"@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="],
|
"@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="],
|
||||||
|
|
||||||
"@tanstack/router-core": ["@tanstack/router-core@1.171.8", "", { "dependencies": { "@tanstack/history": "1.162.0", "cookie-es": "^3.0.0", "seroval": "^1.5.4", "seroval-plugins": "^1.5.4" } }, "sha512-PbrTBbofFcacrH3RLgHYILRqTFnAGq+gXrXoA/vo7qUSkJpSO4GWfLtrtCahD4VayzRm19IPwcjPPLEugag6pw=="],
|
"@tanstack/router-core": ["@tanstack/router-core@1.171.6", "", { "dependencies": { "@tanstack/history": "1.162.0", "cookie-es": "^3.0.0", "seroval": "^1.5.4", "seroval-plugins": "^1.5.4" } }, "sha512-Ol6DQ+j6rf/rPVELIzo8LHwOQV2KL+zry3b+39kL/GKrt7YId52WJRAFMzuseY4XceSW+PU7sG/Cc1QkwJr0hg=="],
|
||||||
|
|
||||||
"@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.168.0", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16" }, "peerDependencies": { "@tanstack/router-core": "^1.170.0", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-wQoQhlBK7nlZgqzaqdYXKWNTpdHdsaREdaPhFZVH0/Ador+F+eM3/NF2i3f2LPeS0GgKraZUQXe1Q/1+KHyEYg=="],
|
"@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.168.0", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16" }, "peerDependencies": { "@tanstack/router-core": "^1.170.0", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-wQoQhlBK7nlZgqzaqdYXKWNTpdHdsaREdaPhFZVH0/Ador+F+eM3/NF2i3f2LPeS0GgKraZUQXe1Q/1+KHyEYg=="],
|
||||||
|
|
||||||
"@tanstack/router-generator": ["@tanstack/router-generator@1.167.12", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.8", "@tanstack/router-utils": "1.162.1", "@tanstack/virtual-file-routes": "1.162.0", "jiti": "^2.7.0", "magic-string": "^0.30.21", "prettier": "^3.5.0", "zod": "^4.4.3" } }, "sha512-FGr7nn6VhjL53TUCTyDgApSkAYRxhId+v0HVQdSu0ADkNuHY+sUnYEMqiF6aN82jYWuXzrSL1xazg6/rfEP82g=="],
|
"@tanstack/router-generator": ["@tanstack/router-generator@1.167.10", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.6", "@tanstack/router-utils": "1.162.1", "@tanstack/virtual-file-routes": "1.162.0", "jiti": "^2.7.0", "magic-string": "^0.30.21", "prettier": "^3.5.0", "zod": "^4.4.3" } }, "sha512-CjbjWRSo6djLU/C7ncb9IbKUcf4IwpdqhLGngkwKkXaVFXGxEAafA/uhvOCv/UEUVR7NI3tJqqQmxYXGcJPbjw=="],
|
||||||
|
|
||||||
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.168.13", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.8", "@tanstack/router-generator": "1.167.12", "@tanstack/router-utils": "1.162.1", "@tanstack/virtual-file-routes": "1.162.0", "chokidar": "^5.0.0", "unplugin": "^3.0.0", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2 || ^2.0.0", "@tanstack/react-router": "^1.170.10", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-LnepwDai+TaC4K3aZeXrrKpnGoP8xGGilVGFfa5flGgC3+jCSBysb8SktidRE8eF2/iOzCQC0LIGirtMyZepSA=="],
|
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.168.11", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.6", "@tanstack/router-generator": "1.167.10", "@tanstack/router-utils": "1.162.1", "@tanstack/virtual-file-routes": "1.162.0", "chokidar": "^5.0.0", "unplugin": "^3.0.0", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2 || ^2.0.0", "@tanstack/react-router": "^1.170.8", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-b2eom/8xCWL/OiWxKub8kYsr8p+kvmB/eXwYGqCWG8vilcJo+eQCSyp54nKt0AZ5k/ET1+eINc+4mwL3bVeAgg=="],
|
||||||
|
|
||||||
"@tanstack/router-ssr-query-core": ["@tanstack/router-ssr-query-core@1.169.1", "", { "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/router-core": ">=1.127.0" } }, "sha512-rngux8s/3mPQzcjLYDLkNU31coYVyCgrVTfpdwqUdY5jIEHqGTXrO73DTkPR1PppwYUeVhmNCgl8TctRcnupjg=="],
|
"@tanstack/router-ssr-query-core": ["@tanstack/router-ssr-query-core@1.169.0", "", { "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/router-core": ">=1.127.0" } }, "sha512-zueXiVsF1BbVc8iaalHILRGURDCVlTTOVdUy/36VVeKVKr778vqJzyus+erEoVu5x4vl4DBGGM8RHqCaus1TQQ=="],
|
||||||
|
|
||||||
"@tanstack/router-utils": ["@tanstack/router-utils@1.162.1", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-62layyTGmclHDQS/eidwKRfN1hhCKwViG7iEBcVmL0MXgcAB3OOucWCEcDDGd9Cu11H6b4QQ5oOo47MWIqwz0A=="],
|
"@tanstack/router-utils": ["@tanstack/router-utils@1.162.1", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-62layyTGmclHDQS/eidwKRfN1hhCKwViG7iEBcVmL0MXgcAB3OOucWCEcDDGd9Cu11H6b4QQ5oOo47MWIqwz0A=="],
|
||||||
|
|
||||||
"@tanstack/start-client-core": ["@tanstack/start-client-core@1.170.6", "", { "dependencies": { "@tanstack/router-core": "1.171.8", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-storage-context": "1.167.10", "seroval": "^1.5.4" } }, "sha512-Zh4JY3bWiM8K807CnyRE/+53YiUx/R6nRhQr1BSxaXM3iFh9/FxoN4peK+yhkG7Hq/O2AGBWHt9yscxrsFD5+g=="],
|
"@tanstack/start-client-core": ["@tanstack/start-client-core@1.170.4", "", { "dependencies": { "@tanstack/router-core": "1.171.6", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-storage-context": "1.167.8", "seroval": "^1.5.4" } }, "sha512-j/Deupf0zR7P5QObN38xTHufCRZkWTb6a/7aauu8eBmzOzDVggvuEdYHRZWiwJ9HRKbR2/SIJASVKeTtj1OcWw=="],
|
||||||
|
|
||||||
"@tanstack/start-fn-stubs": ["@tanstack/start-fn-stubs@1.162.0", "", {}, "sha512-QWfUZ3Yo923tdQn38LyKMU8rcTw69zc+T4dAvgTWV4O56SqFRsGfS0lSWIMhJRwXIx/bvdi7nTUBDdZtTHtpTQ=="],
|
"@tanstack/start-fn-stubs": ["@tanstack/start-fn-stubs@1.162.0", "", {}, "sha512-QWfUZ3Yo923tdQn38LyKMU8rcTw69zc+T4dAvgTWV4O56SqFRsGfS0lSWIMhJRwXIx/bvdi7nTUBDdZtTHtpTQ=="],
|
||||||
|
|
||||||
"@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.171.10", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@rolldown/pluginutils": "1.0.1", "@tanstack/router-core": "1.171.8", "@tanstack/router-generator": "1.167.12", "@tanstack/router-plugin": "1.168.13", "@tanstack/router-utils": "1.162.1", "@tanstack/start-client-core": "1.170.6", "@tanstack/start-server-core": "1.169.8", "exsolve": "^1.0.7", "lightningcss": "^1.32.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "seroval": "^1.5.4", "source-map": "^0.7.6", "srvx": "^0.11.9", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "vite"] }, "sha512-HYYcgwjnuGIMn7wgASM6AryKktyQ06FwziFDP3d93APFmC8dLPadSzvwV/AUWvPQYCQq2Dp9SAX3byXEMvjO7g=="],
|
"@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.171.6", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@rolldown/pluginutils": "1.0.1", "@tanstack/router-core": "1.171.6", "@tanstack/router-generator": "1.167.10", "@tanstack/router-plugin": "1.168.11", "@tanstack/router-utils": "1.162.1", "@tanstack/start-client-core": "1.170.4", "@tanstack/start-server-core": "1.169.4", "exsolve": "^1.0.7", "lightningcss": "^1.32.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "seroval": "^1.5.4", "source-map": "^0.7.6", "srvx": "^0.11.9", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "vite"] }, "sha512-e0AUN+omib0qLgs0r3zoKRSeHEkwL8qs8skvbl8zgDQXw9zF73K7ZXE7QarSzbqfLAiehVqlv0iPETp8ogUftQ=="],
|
||||||
|
|
||||||
"@tanstack/start-server-core": ["@tanstack/start-server-core@1.169.8", "", { "dependencies": { "@tanstack/history": "1.162.0", "@tanstack/router-core": "1.171.8", "@tanstack/start-client-core": "1.170.6", "@tanstack/start-storage-context": "1.167.10", "fetchdts": "^0.1.6", "h3-v2": "npm:h3@2.0.1-rc.20", "seroval": "^1.5.4" } }, "sha512-yVhdg9QLNUrXdXDn5kN76u0YFKraR7bgb6ZFqHCbX63sTPabD4Z1fBr68PnzqKWB2gXfJmP9JN1puvcdChKeYA=="],
|
"@tanstack/start-server-core": ["@tanstack/start-server-core@1.169.4", "", { "dependencies": { "@tanstack/history": "1.162.0", "@tanstack/router-core": "1.171.6", "@tanstack/start-client-core": "1.170.4", "@tanstack/start-storage-context": "1.167.8", "fetchdts": "^0.1.6", "h3-v2": "npm:h3@2.0.1-rc.20", "seroval": "^1.5.4" } }, "sha512-iM3HamWRQPROuAb+22frV/+GkqG2a3rL0X14N+Y0Dt5OajrIumPuprOn9ldUXsbdg89RTBf1KoJNDPeYGOqH4g=="],
|
||||||
|
|
||||||
"@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.167.10", "", { "dependencies": { "@tanstack/router-core": "1.171.8" } }, "sha512-geCsFpgCt+S2gQjzXILdPZ9obIxtzuN8C0Esc1fcyWZhwYyqo4C8G2o/dIck8xGixCMSvOsxL5NkCXDdOm2KOQ=="],
|
"@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.167.8", "", { "dependencies": { "@tanstack/router-core": "1.171.6" } }, "sha512-y9T+bIIp1ihLAXyS2+r+UovSupfu4KydSXpnoeRsw/14/E0huJsX7xB/n6XXOdmDYAaJ2WGOrG9wYjzeIDuBAw=="],
|
||||||
|
|
||||||
"@tanstack/store": ["@tanstack/store@0.11.0", "", {}, "sha512-WlzzCt3xi0G6pCAJu1U+2jiECwabETDpQDi3hfkFZvJii9AuZqEKbOiVarX1/bWhTNjU486yQtJCCasi/0q+Cw=="],
|
"@tanstack/store": ["@tanstack/store@0.11.0", "", {}, "sha512-WlzzCt3xi0G6pCAJu1U+2jiECwabETDpQDi3hfkFZvJii9AuZqEKbOiVarX1/bWhTNjU486yQtJCCasi/0q+Cw=="],
|
||||||
|
|
||||||
|
|
@ -1170,25 +1169,25 @@
|
||||||
|
|
||||||
"@vitest/utils": ["@vitest/utils@4.1.7", "", { "dependencies": { "@vitest/pretty-format": "4.1.7", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw=="],
|
"@vitest/utils": ["@vitest/utils@4.1.7", "", { "dependencies": { "@vitest/pretty-format": "4.1.7", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw=="],
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-core": ["@voidzero-dev/vite-plus-core@0.1.23", "", { "dependencies": { "@oxc-project/runtime": "=0.133.0", "@oxc-project/types": "=0.133.0", "lightningcss": "^1.30.2", "postcss": "^8.5.6" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@tsdown/css": "0.22.0", "@tsdown/exe": "0.22.0", "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "publint": "^0.3.8", "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", "typescript": "^5.0.0 || ^6.0.0", "unplugin-unused": "^0.5.0", "unrun": "*", "yaml": "^2.4.2" }, "optionalPeers": ["@arethetypeswrong/core", "@tsdown/css", "@tsdown/exe", "@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "publint", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "typescript", "unplugin-unused", "unrun", "yaml"] }, "sha512-Twi+95cq1pObzkNR4u6lP7z4gPhtS0/vxeBAdbTvAeA12qlyyFED7mQZnAgaVIN3k1C1ve0997F3/ncUBAwQ8w=="],
|
"@voidzero-dev/vite-plus-core": ["@voidzero-dev/vite-plus-core@0.1.22", "", { "dependencies": { "@oxc-project/runtime": "=0.129.0", "@oxc-project/types": "=0.129.0", "lightningcss": "^1.30.2", "postcss": "^8.5.6" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@tsdown/css": "0.22.0", "@tsdown/exe": "0.22.0", "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "publint": "^0.3.8", "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", "typescript": "^5.0.0 || ^6.0.0", "unplugin-unused": "^0.5.0", "unrun": "*", "yaml": "^2.4.2" }, "optionalPeers": ["@arethetypeswrong/core", "@tsdown/css", "@tsdown/exe", "@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "publint", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "typescript", "unplugin-unused", "unrun", "yaml"] }, "sha512-OC7tChagbJCoY7YKzD5MuyxJO1km5IF42B3ltZoQ9Twc8UuPrMuWZrVoP984tJKYd/gFJuQFM/lrbNtBm9kyDg=="],
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-darwin-arm64": ["@voidzero-dev/vite-plus-darwin-arm64@0.1.23", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5tRAYzVHE6X+QK7MVE2URyubL5d9+wbXhepImVEwkdOhGThxsTiCchxzJrjeqVSFf0GT8eFC3URBuKTzWBD9NA=="],
|
"@voidzero-dev/vite-plus-darwin-arm64": ["@voidzero-dev/vite-plus-darwin-arm64@0.1.22", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+6sRVGCAQSpO96WC0EZtSLJ01VzNiZL/eQUQ8NLVl1oH+0+KgHF2UXyqUXGCGf/JCu34egEwBEjDU3WUwN2mxg=="],
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-darwin-x64": ["@voidzero-dev/vite-plus-darwin-x64@0.1.23", "", { "os": "darwin", "cpu": "x64" }, "sha512-05qpV7lqe9iiyCIWKhdlwEiMG4NYPt/9FZa+fBvXo0YOV4JTC1yTUMWr1X4edzYJg7Y1Jdx06u3Tu2AJvGKwSw=="],
|
"@voidzero-dev/vite-plus-darwin-x64": ["@voidzero-dev/vite-plus-darwin-x64@0.1.22", "", { "os": "darwin", "cpu": "x64" }, "sha512-rqsCW/Brt2froW7VhLE+gVHKtGniyLdHlfcmTLfuM5vnd5skdQlymibRw/lviJU+mSl0x8pGcXZbvA4TLHbCoA=="],
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-linux-arm64-gnu": ["@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.23", "", { "os": "linux", "cpu": "arm64" }, "sha512-qb5gUpBJovwfkDjHZKHy8LTh+69kQIKlIXVYLe8wVAw6+cEI4WNWPf6YHJdnPIKtOYsGNZ5vllFHbtT3i6uOfA=="],
|
"@voidzero-dev/vite-plus-linux-arm64-gnu": ["@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.22", "", { "os": "linux", "cpu": "arm64" }, "sha512-OL/WT6pvJpFS1L+hWe8g2LCEHCJfEBSgxV0vbSoQDdfTuilUJaVK8rljVWgtIVjUQSjIx8jKfPsl/I8iEBh0GQ=="],
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-linux-arm64-musl": ["@voidzero-dev/vite-plus-linux-arm64-musl@0.1.23", "", { "os": "linux", "cpu": "arm64" }, "sha512-mvB3XYltfUn55WdgRYZHDY+bgeOwrwsMhdftXDC8T7tIUglIxcuo3+xMOZOrbFFjJXNPelUjvU1R9ItAR8rY1w=="],
|
"@voidzero-dev/vite-plus-linux-arm64-musl": ["@voidzero-dev/vite-plus-linux-arm64-musl@0.1.22", "", { "os": "linux", "cpu": "arm64" }, "sha512-SdZLL2aXm9XlbNfygsIifxhTjnRa2gI5oXNCh9QmLmXN36yhXt816I5Tl5IQ5DJwxhnG1+5Uqp2D6tHaT5g6nQ=="],
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-linux-x64-gnu": ["@voidzero-dev/vite-plus-linux-x64-gnu@0.1.23", "", { "os": "linux", "cpu": "x64" }, "sha512-Hf7phM8L2wUdLloQHbylRzmJEv9PIUCC5Yvdo1UycWyiEl99CUgAlrSmZyfkKxPh+MSFaa+xNwfXdf9zwXM8mg=="],
|
"@voidzero-dev/vite-plus-linux-x64-gnu": ["@voidzero-dev/vite-plus-linux-x64-gnu@0.1.22", "", { "os": "linux", "cpu": "x64" }, "sha512-Qn6WPTn61A47ZBCPm+v8kCrMgXlI5p10pllKYkce2PFeCotN6v1bqu2GBZIkLVSi534ywGqdkiG1kaesM9e1vw=="],
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-linux-x64-musl": ["@voidzero-dev/vite-plus-linux-x64-musl@0.1.23", "", { "os": "linux", "cpu": "x64" }, "sha512-YJ+OT7tddkUshfojk1jIyJcQHA3h5cMKSpCVVJcJwTCZNb9z9zRcjZCxGbwwTOqrV4PzP8TVy50aEEl0jP1lTg=="],
|
"@voidzero-dev/vite-plus-linux-x64-musl": ["@voidzero-dev/vite-plus-linux-x64-musl@0.1.22", "", { "os": "linux", "cpu": "x64" }, "sha512-DsVE09IgvYBR2PY2Bohd08tScYDa8K8KJkIGc8Y6uRXR14NEldoufmWJdCmEsGLA8puRv5HV3ZheWFFjmw5Liw=="],
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-test": ["@voidzero-dev/vite-plus-test@0.1.23", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@voidzero-dev/vite-plus-core": "0.1.23", "es-module-lexer": "^1.7.0", "obug": "^2.1.1", "pixelmatch": "^7.1.0", "pngjs": "^7.0.0", "sirv": "^3.0.2", "std-env": "^4.0.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "ws": "^8.18.3" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/coverage-istanbul": "4.1.7", "@vitest/coverage-v8": "4.1.7", "@vitest/ui": "4.1.7", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"] }, "sha512-50NmnIMHsES5f+4iScEwqAR6LlsE1oP7n1HBxaYVX839tjMWCYHRUiBlBZFU+OoWwuFNq0I1ap0j0vamvJsYGg=="],
|
"@voidzero-dev/vite-plus-test": ["@voidzero-dev/vite-plus-test@0.1.22", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@voidzero-dev/vite-plus-core": "0.1.22", "es-module-lexer": "^1.7.0", "obug": "^2.1.1", "pixelmatch": "^7.1.0", "pngjs": "^7.0.0", "sirv": "^3.0.2", "std-env": "^4.0.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "ws": "^8.18.3" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/coverage-istanbul": "4.1.6", "@vitest/coverage-v8": "4.1.6", "@vitest/ui": "4.1.6", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"] }, "sha512-6VKDNXH+ygDyTXpBYn+g+2a9j3zuAZRlP2ZSx0RcjPMdGUMpX6Mox4CmdK8SkZUvi+f6a1siX50ZnCOcQoTgmQ=="],
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-win32-arm64-msvc": ["@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.23", "", { "os": "win32", "cpu": "arm64" }, "sha512-/y2Yz5/kbDv4VlgGHiCXNTZ2ZeIPjSo5iogytb3kX2skxaQi7JqAwxjYjfjSD6q6e6lFIyxNEX81BSbcbIFVXw=="],
|
"@voidzero-dev/vite-plus-win32-arm64-msvc": ["@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.22", "", { "os": "win32", "cpu": "arm64" }, "sha512-/1JDhTu7SAIjpqJVAN3D3zmN/O8cCRwdn63Qs0ep5GzNYh3+TtVyw3xdUY7YHeyuSypgKlqnr6IPeMlNijOG/Q=="],
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-win32-x64-msvc": ["@voidzero-dev/vite-plus-win32-x64-msvc@0.1.23", "", { "os": "win32", "cpu": "x64" }, "sha512-YoHCatW5TBhQvIJU0ewA4TVGvznTfhLgjqHY8Pn3HVFNWhNO9DCOsP4ihpvKzYFS7F9dx0xmmAYXOH/suArDFQ=="],
|
"@voidzero-dev/vite-plus-win32-x64-msvc": ["@voidzero-dev/vite-plus-win32-x64-msvc@0.1.22", "", { "os": "win32", "cpu": "x64" }, "sha512-GITqtIWeTaWZ7mo1799sIB6XhhSAL1TmuJvrtBz8e3SAUpjDsIYACDYumUDhowPdIHRO3rasyJg9jJZA82BjKQ=="],
|
||||||
|
|
||||||
"@xmldom/is-dom-node": ["@xmldom/is-dom-node@1.0.1", "", {}, "sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q=="],
|
"@xmldom/is-dom-node": ["@xmldom/is-dom-node@1.0.1", "", {}, "sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q=="],
|
||||||
|
|
||||||
|
|
@ -1246,7 +1245,7 @@
|
||||||
|
|
||||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.30", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-xjOFN16Ha1+Rz4nFYKqHU/LSB+gx/Vi3yQLX7r7sAW+Wa+8hhF2h4pvqTrTMc8+WcDBEunnUurr46Jvv0jk3Vg=="],
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.30", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-xjOFN16Ha1+Rz4nFYKqHU/LSB+gx/Vi3yQLX7r7sAW+Wa+8hhF2h4pvqTrTMc8+WcDBEunnUurr46Jvv0jk3Vg=="],
|
||||||
|
|
||||||
"better-auth": ["better-auth@1.6.12", "", { "dependencies": { "@better-auth/core": "1.6.12", "@better-auth/drizzle-adapter": "1.6.12", "@better-auth/kysely-adapter": "1.6.12", "@better-auth/memory-adapter": "1.6.12", "@better-auth/mongo-adapter": "1.6.12", "@better-auth/prisma-adapter": "1.6.12", "@better-auth/telemetry": "1.6.12", "@better-auth/utils": "0.4.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.5", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-vJG8hB+zcayZEJgcWGTzP2XODZuf/WKViOtam+uhhQ9879yc7fDWAV9O4jSs+R28noSXIAaB3zhIMN3DaDO3cA=="],
|
"better-auth": ["better-auth@1.6.11", "", { "dependencies": { "@better-auth/core": "1.6.11", "@better-auth/drizzle-adapter": "1.6.11", "@better-auth/kysely-adapter": "1.6.11", "@better-auth/memory-adapter": "1.6.11", "@better-auth/mongo-adapter": "1.6.11", "@better-auth/prisma-adapter": "1.6.11", "@better-auth/telemetry": "1.6.11", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.5", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-Wwt6+q07dwIhsp6XiM7L1qSXVUWBEtNl+eZvwM778CguFqDZFBN9Pt6LtFaHl55t8Z+Zc//5kxcbgDY8/79vFQ=="],
|
||||||
|
|
||||||
"better-call": ["better-call@1.3.5", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA=="],
|
"better-call": ["better-call@1.3.5", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA=="],
|
||||||
|
|
||||||
|
|
@ -1322,7 +1321,7 @@
|
||||||
|
|
||||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||||
|
|
||||||
"commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="],
|
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
||||||
|
|
||||||
"compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="],
|
"compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="],
|
||||||
|
|
||||||
|
|
@ -1330,7 +1329,7 @@
|
||||||
|
|
||||||
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||||
|
|
||||||
"content-disposition": ["content-disposition@2.0.1", "", {}, "sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog=="],
|
"content-disposition": ["content-disposition@2.0.0", "", {}, "sha512-qqGFOrKmFP1lTfG24opOJFcTMza1BqyTSUKVbMGUP5uRsBH+C00Q1loOk+JSFshyRE0ji4HtCJeNN2WHWd6PGw=="],
|
||||||
|
|
||||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||||
|
|
||||||
|
|
@ -1384,7 +1383,7 @@
|
||||||
|
|
||||||
"data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="],
|
"data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="],
|
||||||
|
|
||||||
"date-fns": ["date-fns@4.4.0", "", {}, "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w=="],
|
"date-fns": ["date-fns@4.3.0", "", {}, "sha512-OYcL+3N/jyWbYdFGqoMAhytDgxP9pbYPUUiRCOgn4Fewaadk9l/Wam4Avciiyp2BgkpfQyBV9B+ehnVJych+eQ=="],
|
||||||
|
|
||||||
"dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="],
|
"dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="],
|
||||||
|
|
||||||
|
|
@ -1436,7 +1435,7 @@
|
||||||
|
|
||||||
"dotenv-expand": ["dotenv-expand@12.0.3", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA=="],
|
"dotenv-expand": ["dotenv-expand@12.0.3", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA=="],
|
||||||
|
|
||||||
"drizzle-kit": ["drizzle-kit@1.0.0-rc.4-ca0f029", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-hM1K72ptIPOpJDScHo7cAF+K8A8Tr3q2yHvBJvOZWvLn2lN+G4RDUvG3rT3ohlSE2O3BN/s7g3e3JiQLStt8iQ=="],
|
"drizzle-kit": ["drizzle-kit@1.0.0-rc.4-5d5b77c", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-cDQOQXacxeaQl0Ib18EfPwVMyfvvybuOBj/f/WclmuGZtxHOTOMTpJ/3qEpoXe8+lCDRagCpS3w1wYQyUmT9wg=="],
|
||||||
|
|
||||||
"drizzle-orm": ["drizzle-orm@1.0.0-rc.4-5d5b77c", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql-pg": ">=4.0.0-beta.58 || >=4.0.0", "@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", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "effect": ">=4.0.0-beta.58 || >=4.0.0", "expo-sqlite": ">=14.0.0", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/mssql", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "effect", "expo-sqlite", "mssql", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-Oq9W4B11PracWY9cuCLTFT+JA5kXqR6rBmWcM8oh0xwLgCviVl9wh6ZuWX7Zpv1Jir/8W+M37syvjSYtj1f5iA=="],
|
"drizzle-orm": ["drizzle-orm@1.0.0-rc.4-5d5b77c", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql-pg": ">=4.0.0-beta.58 || >=4.0.0", "@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", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "effect": ">=4.0.0-beta.58 || >=4.0.0", "expo-sqlite": ">=14.0.0", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/mssql", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "effect", "expo-sqlite", "mssql", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-Oq9W4B11PracWY9cuCLTFT+JA5kXqR6rBmWcM8oh0xwLgCviVl9wh6ZuWX7Zpv1Jir/8W+M37syvjSYtj1f5iA=="],
|
||||||
|
|
||||||
|
|
@ -1562,7 +1561,7 @@
|
||||||
|
|
||||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||||
|
|
||||||
"framer-motion": ["framer-motion@12.40.0", "", { "dependencies": { "motion-dom": "^12.40.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg=="],
|
"framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="],
|
||||||
|
|
||||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||||
|
|
||||||
|
|
@ -1570,11 +1569,11 @@
|
||||||
|
|
||||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
"fumadocs-core": ["fumadocs-core@16.9.3", "", { "dependencies": { "@orama/orama": "^3.1.18", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "js-yaml": "^4.1.1", "mdast-util-mdx": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", "remark": "^15.0.1", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^4.1.0", "tinyglobby": "^0.2.16", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@mdx-js/mdx": "*", "@mixedbread/sdk": "0.x.x", "@orama/core": "1.x.x", "@oramacloud/client": "2.x.x", "@tanstack/react-router": "1.x.x", "@types/estree-jsx": "*", "@types/hast": "*", "@types/mdast": "*", "@types/react": "*", "algoliasearch": "5.x.x", "flexsearch": "*", "lucide-react": "*", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router": "7.x.x", "waku": "*", "zod": "4.x.x" }, "optionalPeers": ["@mdx-js/mdx", "@mixedbread/sdk", "@orama/core", "@oramacloud/client", "@tanstack/react-router", "@types/estree-jsx", "@types/hast", "@types/mdast", "@types/react", "algoliasearch", "flexsearch", "lucide-react", "next", "react", "react-dom", "react-router", "waku", "zod"] }, "sha512-8RVzKnzBJR5o+tJCccY28ntekfMQYBoYiz7alnYb/d9YJc+XpnsINzTl63lQ1eBMZ9gdhm2MqRtgUjh/8rUrbw=="],
|
"fumadocs-core": ["fumadocs-core@16.9.1", "", { "dependencies": { "@orama/orama": "^3.1.18", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "js-yaml": "^4.1.1", "mdast-util-mdx": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", "remark": "^15.0.1", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^4.0.2", "tinyglobby": "^0.2.16", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@mdx-js/mdx": "*", "@mixedbread/sdk": "0.x.x", "@orama/core": "1.x.x", "@oramacloud/client": "2.x.x", "@tanstack/react-router": "1.x.x", "@types/estree-jsx": "*", "@types/hast": "*", "@types/mdast": "*", "@types/react": "*", "algoliasearch": "5.x.x", "flexsearch": "*", "lucide-react": "*", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router": "7.x.x", "waku": "*", "zod": "4.x.x" }, "optionalPeers": ["@mdx-js/mdx", "@mixedbread/sdk", "@orama/core", "@oramacloud/client", "@tanstack/react-router", "@types/estree-jsx", "@types/hast", "@types/mdast", "@types/react", "algoliasearch", "flexsearch", "lucide-react", "next", "react", "react-dom", "react-router", "waku", "zod"] }, "sha512-8VW4aD1iG5SrCChRq84QpNyjKY69i5WfUFFgtx/LSUuX1RewWb0qps7DhKLSjOWraemhZatzsZ7iceivJmYRTg=="],
|
||||||
|
|
||||||
"fumadocs-mdx": ["fumadocs-mdx@15.0.10", "", { "dependencies": { "@mdx-js/mdx": "^3.1.1", "@standard-schema/spec": "^1.1.0", "chokidar": "^5.0.0", "esbuild": "^0.28.0", "estree-util-value-to-estree": "^3.5.0", "js-yaml": "^4.1.1", "mdast-util-mdx": "^3.0.0", "picocolors": "^1.1.1", "picomatch": "^4.0.4", "tinyexec": "^1.2.2", "tinyglobby": "^0.2.16", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3", "zod": "^4.4.3" }, "peerDependencies": { "@types/mdast": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "^16.7.0", "mdast-util-directive": "*", "next": "^15.3.0 || ^16.0.0", "react": "^19.2.0", "rolldown": "*", "vite": "7.x.x || 8.x.x" }, "optionalPeers": ["@types/mdast", "@types/mdx", "@types/react", "mdast-util-directive", "next", "react", "rolldown", "vite"], "bin": { "fumadocs-mdx": "./bin.js" } }, "sha512-kH3S7ESS9yXTAaCkA8dDugsCK/MbnpgyZ5qBEL7cWoavV0O/T4+4YTYFkvNknz7cw+T/r+OG0p2BvlVhkk4fww=="],
|
"fumadocs-mdx": ["fumadocs-mdx@14.3.2", "", { "dependencies": { "@mdx-js/mdx": "^3.1.1", "@standard-schema/spec": "^1.1.0", "chokidar": "^5.0.0", "esbuild": "^0.28.0", "estree-util-value-to-estree": "^3.5.0", "js-yaml": "^4.1.1", "mdast-util-mdx": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", "picocolors": "^1.1.1", "picomatch": "^4.0.4", "tinyexec": "^1.1.1", "tinyglobby": "^0.2.16", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3", "zod": "^4.3.6" }, "peerDependencies": { "@types/mdast": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "^15.0.0 || ^16.0.0", "mdast-util-directive": "*", "next": "^15.3.0 || ^16.0.0", "react": "^19.2.0", "vite": "6.x.x || 7.x.x || 8.x.x" }, "optionalPeers": ["@types/mdast", "@types/mdx", "@types/react", "mdast-util-directive", "next", "react", "vite"], "bin": { "fumadocs-mdx": "dist/bin.js" } }, "sha512-73SoZkbUuqnD91G/0zBcaQdM1TMnYw5JJzKgkGvQTiZbtLQFuWTt8/uRqnzFMuNIUu/WY9Lo9d1iZ8G+jOVieA=="],
|
||||||
|
|
||||||
"fumadocs-ui": ["fumadocs-ui@16.9.3", "", { "dependencies": { "@fumadocs/tailwind": "0.0.5", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-direction": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-presence": "^1.1.5", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", "class-variance-authority": "^0.7.1", "lucide-react": "^1.17.0", "motion": "^12.40.0", "next-themes": "^0.4.6", "react-remove-scroll": "^2.7.2", "rehype-raw": "^7.0.0", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^4.1.0", "tailwind-merge": "^3.6.0", "unist-util-visit": "^5.1.0" }, "peerDependencies": { "@takumi-rs/image-response": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "16.9.3", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0" }, "optionalPeers": ["@takumi-rs/image-response", "@types/mdx", "@types/react", "next"] }, "sha512-eoVKj1H+ATut0su+WIoPWBLRqzPMGD0hekIBr4GopWvUg1lS997HL4kP+Leyf+3CYlZtFgyXb6ylbvRLFtEj6Q=="],
|
"fumadocs-ui": ["fumadocs-ui@16.9.1", "", { "dependencies": { "@fumadocs/tailwind": "0.0.5", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-direction": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-presence": "^1.1.5", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", "class-variance-authority": "^0.7.1", "lucide-react": "^1.16.0", "motion": "^12.38.0", "next-themes": "^0.4.6", "react-remove-scroll": "^2.7.2", "rehype-raw": "^7.0.0", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^4.0.2", "tailwind-merge": "^3.6.0", "unist-util-visit": "^5.1.0" }, "peerDependencies": { "@takumi-rs/image-response": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "16.9.1", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0" }, "optionalPeers": ["@takumi-rs/image-response", "@types/mdx", "@types/react", "next"] }, "sha512-2A8wO/RuoV0eOgabKMlcjBEPQKynbE4wkxoTfAj/E6bYCid/zY3DG+hhSI1Ssen5ws/73mFPacIEBsWhdAAsnw=="],
|
||||||
|
|
||||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
|
|
@ -1684,7 +1683,7 @@
|
||||||
|
|
||||||
"ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="],
|
"ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="],
|
||||||
|
|
||||||
"ipaddr.js": ["ipaddr.js@2.4.0", "", {}, "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ=="],
|
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||||
|
|
||||||
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
|
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
|
||||||
|
|
||||||
|
|
@ -1800,7 +1799,7 @@
|
||||||
|
|
||||||
"lru-cache": ["lru-cache@11.3.6", "", {}, "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A=="],
|
"lru-cache": ["lru-cache@11.3.6", "", {}, "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A=="],
|
||||||
|
|
||||||
"lucide-react": ["lucide-react@1.17.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w=="],
|
"lucide-react": ["lucide-react@1.16.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ=="],
|
||||||
|
|
||||||
"luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="],
|
"luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="],
|
||||||
|
|
||||||
|
|
@ -1942,11 +1941,11 @@
|
||||||
|
|
||||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||||
|
|
||||||
"motion": ["motion@12.40.0", "", { "dependencies": { "framer-motion": "^12.40.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-yjrHUrBFW6kQvjJwRsoiPSAhC5tRwRqNGJWmiJ4CrGnbKp0V88AdzkhBmDoqIsIPfarOe0Uddd37Xq43/gIocA=="],
|
"motion": ["motion@12.38.0", "", { "dependencies": { "framer-motion": "^12.38.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w=="],
|
||||||
|
|
||||||
"motion-dom": ["motion-dom@12.40.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg=="],
|
"motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="],
|
||||||
|
|
||||||
"motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
|
"motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="],
|
||||||
|
|
||||||
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||||
|
|
||||||
|
|
@ -2016,9 +2015,9 @@
|
||||||
|
|
||||||
"oxfmt": ["oxfmt@0.52.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.52.0", "@oxfmt/binding-android-arm64": "0.52.0", "@oxfmt/binding-darwin-arm64": "0.52.0", "@oxfmt/binding-darwin-x64": "0.52.0", "@oxfmt/binding-freebsd-x64": "0.52.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.52.0", "@oxfmt/binding-linux-arm-musleabihf": "0.52.0", "@oxfmt/binding-linux-arm64-gnu": "0.52.0", "@oxfmt/binding-linux-arm64-musl": "0.52.0", "@oxfmt/binding-linux-ppc64-gnu": "0.52.0", "@oxfmt/binding-linux-riscv64-gnu": "0.52.0", "@oxfmt/binding-linux-riscv64-musl": "0.52.0", "@oxfmt/binding-linux-s390x-gnu": "0.52.0", "@oxfmt/binding-linux-x64-gnu": "0.52.0", "@oxfmt/binding-linux-x64-musl": "0.52.0", "@oxfmt/binding-openharmony-arm64": "0.52.0", "@oxfmt/binding-win32-arm64-msvc": "0.52.0", "@oxfmt/binding-win32-ia32-msvc": "0.52.0", "@oxfmt/binding-win32-x64-msvc": "0.52.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-nJlYM35F64zTDMecCNhoHNkf+D/eHv7xcjj9XDSj+bFAVtN93m7v8DQMdHd6nDG6Akf/kEYYHmDUBs2Dz27Sug=="],
|
"oxfmt": ["oxfmt@0.52.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.52.0", "@oxfmt/binding-android-arm64": "0.52.0", "@oxfmt/binding-darwin-arm64": "0.52.0", "@oxfmt/binding-darwin-x64": "0.52.0", "@oxfmt/binding-freebsd-x64": "0.52.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.52.0", "@oxfmt/binding-linux-arm-musleabihf": "0.52.0", "@oxfmt/binding-linux-arm64-gnu": "0.52.0", "@oxfmt/binding-linux-arm64-musl": "0.52.0", "@oxfmt/binding-linux-ppc64-gnu": "0.52.0", "@oxfmt/binding-linux-riscv64-gnu": "0.52.0", "@oxfmt/binding-linux-riscv64-musl": "0.52.0", "@oxfmt/binding-linux-s390x-gnu": "0.52.0", "@oxfmt/binding-linux-x64-gnu": "0.52.0", "@oxfmt/binding-linux-x64-musl": "0.52.0", "@oxfmt/binding-openharmony-arm64": "0.52.0", "@oxfmt/binding-win32-arm64-msvc": "0.52.0", "@oxfmt/binding-win32-ia32-msvc": "0.52.0", "@oxfmt/binding-win32-x64-msvc": "0.52.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-nJlYM35F64zTDMecCNhoHNkf+D/eHv7xcjj9XDSj+bFAVtN93m7v8DQMdHd6nDG6Akf/kEYYHmDUBs2Dz27Sug=="],
|
||||||
|
|
||||||
"oxlint": ["oxlint@1.67.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.67.0", "@oxlint/binding-android-arm64": "1.67.0", "@oxlint/binding-darwin-arm64": "1.67.0", "@oxlint/binding-darwin-x64": "1.67.0", "@oxlint/binding-freebsd-x64": "1.67.0", "@oxlint/binding-linux-arm-gnueabihf": "1.67.0", "@oxlint/binding-linux-arm-musleabihf": "1.67.0", "@oxlint/binding-linux-arm64-gnu": "1.67.0", "@oxlint/binding-linux-arm64-musl": "1.67.0", "@oxlint/binding-linux-ppc64-gnu": "1.67.0", "@oxlint/binding-linux-riscv64-gnu": "1.67.0", "@oxlint/binding-linux-riscv64-musl": "1.67.0", "@oxlint/binding-linux-s390x-gnu": "1.67.0", "@oxlint/binding-linux-x64-gnu": "1.67.0", "@oxlint/binding-linux-x64-musl": "1.67.0", "@oxlint/binding-openharmony-arm64": "1.67.0", "@oxlint/binding-win32-arm64-msvc": "1.67.0", "@oxlint/binding-win32-ia32-msvc": "1.67.0", "@oxlint/binding-win32-x64-msvc": "1.67.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-blwwaHPdoH8piQ5/z0KHeoHFR7FZgl12WluKJfu4qFLPkZl6mK04PkLE45Fw1NxfBRSlh40Gu7MkxHUw++ociQ=="],
|
"oxlint": ["oxlint@1.63.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.63.0", "@oxlint/binding-android-arm64": "1.63.0", "@oxlint/binding-darwin-arm64": "1.63.0", "@oxlint/binding-darwin-x64": "1.63.0", "@oxlint/binding-freebsd-x64": "1.63.0", "@oxlint/binding-linux-arm-gnueabihf": "1.63.0", "@oxlint/binding-linux-arm-musleabihf": "1.63.0", "@oxlint/binding-linux-arm64-gnu": "1.63.0", "@oxlint/binding-linux-arm64-musl": "1.63.0", "@oxlint/binding-linux-ppc64-gnu": "1.63.0", "@oxlint/binding-linux-riscv64-gnu": "1.63.0", "@oxlint/binding-linux-riscv64-musl": "1.63.0", "@oxlint/binding-linux-s390x-gnu": "1.63.0", "@oxlint/binding-linux-x64-gnu": "1.63.0", "@oxlint/binding-linux-x64-musl": "1.63.0", "@oxlint/binding-openharmony-arm64": "1.63.0", "@oxlint/binding-win32-arm64-msvc": "1.63.0", "@oxlint/binding-win32-ia32-msvc": "1.63.0", "@oxlint/binding-win32-x64-msvc": "1.63.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-9TGXetdjgIHOJ9OiReomP7nnrMkV9HxC1xM2ramJSLQpzxjsAJtQwa4wqkJN2f/uCrqZuJseFuSlWDdvcruveg=="],
|
||||||
|
|
||||||
"oxlint-tsgolint": ["oxlint-tsgolint@0.23.0", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.23.0", "@oxlint-tsgolint/darwin-x64": "0.23.0", "@oxlint-tsgolint/linux-arm64": "0.23.0", "@oxlint-tsgolint/linux-x64": "0.23.0", "@oxlint-tsgolint/win32-arm64": "0.23.0", "@oxlint-tsgolint/win32-x64": "0.23.0" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-3mBv3CoPbh8dFbzfDGIWa2ytZjn2v+3EX4aKRXjIhsoGFzG8GCjfRirz3rwZf1wYbZzsNLTSgpw8VjQuWdp/jA=="],
|
"oxlint-tsgolint": ["oxlint-tsgolint@0.22.1", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.22.1", "@oxlint-tsgolint/darwin-x64": "0.22.1", "@oxlint-tsgolint/linux-arm64": "0.22.1", "@oxlint-tsgolint/linux-x64": "0.22.1", "@oxlint-tsgolint/win32-arm64": "0.22.1", "@oxlint-tsgolint/win32-x64": "0.22.1" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-YUSGSLUnoolsu8gxISEDio3q1rtsCozwfOzASUn3DT2mR2EeQ93uEEnen7s+6LpF+lyTQFln1pQfqwBh/fsVEg=="],
|
||||||
|
|
||||||
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||||
|
|
||||||
|
|
@ -2108,7 +2107,7 @@
|
||||||
|
|
||||||
"react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
|
"react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
|
||||||
|
|
||||||
"react-hook-form": ["react-hook-form@7.77.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-Sslh9YDYc0GDlWT/lxasnIduNo4v3yyvqRGvmGKUre5AFjDs/HV9/OafHGD8d+sB2yoL4UIL9L8X9i0WlZZebg=="],
|
"react-hook-form": ["react-hook-form@7.76.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-rYM7tPiWlu3nZchkR/ex7piyzui2vFPyaLnXnI/RnblB/L4qfMmyses8llJVtF1NpE9WBBsJlGtcSZzPCXW1qQ=="],
|
||||||
|
|
||||||
"react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
"react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
||||||
|
|
||||||
|
|
@ -2222,7 +2221,7 @@
|
||||||
|
|
||||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||||
|
|
||||||
"shadcn": ["shadcn@4.8.3", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-cnP485rIqtDb8waOp+IKUIfifVf64/PCd5VX/nnLeIP+qZ3yS4r6FOQtOdQkoQEvomQUsJS2OHr5CTzKWw0wKQ=="],
|
"shadcn": ["shadcn@4.8.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-LAm3I/1FdoU/zu5GVG8Hbna4X9zlzEG5TeeCPXqsopkjvGk8QUF9OFhqeRN8oM6Oh/ynUI/yQHZxQAO3Ymcqsg=="],
|
||||||
|
|
||||||
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
||||||
|
|
||||||
|
|
@ -2232,7 +2231,7 @@
|
||||||
|
|
||||||
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
|
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
|
||||||
|
|
||||||
"shiki": ["shiki@4.1.0", "", { "dependencies": { "@shikijs/core": "4.1.0", "@shikijs/engine-javascript": "4.1.0", "@shikijs/engine-oniguruma": "4.1.0", "@shikijs/langs": "4.1.0", "@shikijs/themes": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q=="],
|
"shiki": ["shiki@4.0.2", "", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/engine-javascript": "4.0.2", "@shikijs/engine-oniguruma": "4.0.2", "@shikijs/langs": "4.0.2", "@shikijs/themes": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ=="],
|
||||||
|
|
||||||
"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": ["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=="],
|
||||||
|
|
||||||
|
|
@ -2414,7 +2413,7 @@
|
||||||
|
|
||||||
"vite-plugin-killer-instincts": ["vite-plugin-killer-instincts@1.1.0", "", { "peerDependencies": { "vite": ">=5.0.0" } }, "sha512-QUVaWT6ktzZtcnS/O4Ov/LiwzqxF4EwOQdoTgBR48hF3hDB+6YcfqfVVna5Oe96xF7u1/NDYKGecmZdxio2GSA=="],
|
"vite-plugin-killer-instincts": ["vite-plugin-killer-instincts@1.1.0", "", { "peerDependencies": { "vite": ">=5.0.0" } }, "sha512-QUVaWT6ktzZtcnS/O4Ov/LiwzqxF4EwOQdoTgBR48hF3hDB+6YcfqfVVna5Oe96xF7u1/NDYKGecmZdxio2GSA=="],
|
||||||
|
|
||||||
"vite-plus": ["vite-plus@0.1.23", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@oxlint/plugins": "=1.61.0", "@voidzero-dev/vite-plus-core": "0.1.23", "@voidzero-dev/vite-plus-test": "0.1.23", "oxfmt": "=0.52.0", "oxlint": "=1.67.0", "oxlint-tsgolint": "=0.23.0" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "0.1.23", "@voidzero-dev/vite-plus-darwin-x64": "0.1.23", "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.1.23", "@voidzero-dev/vite-plus-linux-arm64-musl": "0.1.23", "@voidzero-dev/vite-plus-linux-x64-gnu": "0.1.23", "@voidzero-dev/vite-plus-linux-x64-musl": "0.1.23", "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.1.23", "@voidzero-dev/vite-plus-win32-x64-msvc": "0.1.23" }, "bin": { "vp": "bin/vp", "oxfmt": "bin/oxfmt", "oxlint": "bin/oxlint" } }, "sha512-4VCb0L6uaN3dTvBD6u4Wk0MqhXIKvi2InyCRw+RkOQ0NEt7bgcF4xD9aJRakH0r0EW9MQKLUGjIUH25dtGjncA=="],
|
"vite-plus": ["vite-plus@0.1.22", "", { "dependencies": { "@oxc-project/types": "=0.129.0", "@oxlint/plugins": "=1.61.0", "@voidzero-dev/vite-plus-core": "0.1.22", "@voidzero-dev/vite-plus-test": "0.1.22", "oxfmt": "=0.48.0", "oxlint": "=1.63.0", "oxlint-tsgolint": "=0.22.1" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "0.1.22", "@voidzero-dev/vite-plus-darwin-x64": "0.1.22", "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.1.22", "@voidzero-dev/vite-plus-linux-arm64-musl": "0.1.22", "@voidzero-dev/vite-plus-linux-x64-gnu": "0.1.22", "@voidzero-dev/vite-plus-linux-x64-musl": "0.1.22", "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.1.22", "@voidzero-dev/vite-plus-win32-x64-msvc": "0.1.22" }, "bin": { "vp": "bin/vp", "oxfmt": "bin/oxfmt", "oxlint": "bin/oxlint" } }, "sha512-fCCmEKjI+Hv74PdL/MKcrBkdYPHFNcqD5568KxwN0sa4SGxtcbs55i/577LxKs0w5zIjuLRZZ0zQPu9MO+9itg=="],
|
||||||
|
|
||||||
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
|
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
|
||||||
|
|
||||||
|
|
@ -2510,51 +2509,49 @@
|
||||||
|
|
||||||
"@ecies/ciphers/@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="],
|
"@ecies/ciphers/@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="],
|
||||||
|
|
||||||
"@hey-api/openapi-ts/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
|
||||||
|
|
||||||
"@hey-api/shared/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
"@hey-api/shared/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||||
|
|
||||||
"@inquirer/checkbox/@inquirer/core": ["@inquirer/core@11.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA=="],
|
"@inquirer/checkbox/@inquirer/core": ["@inquirer/core@11.2.0", "", { "dependencies": { "@inquirer/ansi": "^2.0.6", "@inquirer/figures": "^2.0.6", "@inquirer/type": "^4.0.6", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^4.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-joR1YS2sI0us+9d0I8ViqFbrRLONO8CFTuyvBX4ZVBSch+VsZiugUABdrhBXXJR1VyEzvpz5SQCix3keETQ58g=="],
|
||||||
|
|
||||||
"@inquirer/checkbox/@inquirer/type": ["@inquirer/type@4.0.7", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g=="],
|
"@inquirer/checkbox/@inquirer/type": ["@inquirer/type@4.0.6", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-J+9tdxOskuYuGjsvGaq00AamhDgjR7anhEW2dP4QdQpFCMPngCeC/bCYWQ5NsMWZRdsy53is7kAHb/+7cwDk2g=="],
|
||||||
|
|
||||||
"@inquirer/core/@inquirer/ansi": ["@inquirer/ansi@2.0.5", "", {}, "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw=="],
|
"@inquirer/core/@inquirer/ansi": ["@inquirer/ansi@2.0.5", "", {}, "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw=="],
|
||||||
|
|
||||||
"@inquirer/core/@inquirer/figures": ["@inquirer/figures@2.0.5", "", {}, "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ=="],
|
"@inquirer/core/@inquirer/figures": ["@inquirer/figures@2.0.5", "", {}, "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ=="],
|
||||||
|
|
||||||
"@inquirer/editor/@inquirer/core": ["@inquirer/core@11.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA=="],
|
"@inquirer/editor/@inquirer/core": ["@inquirer/core@11.2.0", "", { "dependencies": { "@inquirer/ansi": "^2.0.6", "@inquirer/figures": "^2.0.6", "@inquirer/type": "^4.0.6", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^4.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-joR1YS2sI0us+9d0I8ViqFbrRLONO8CFTuyvBX4ZVBSch+VsZiugUABdrhBXXJR1VyEzvpz5SQCix3keETQ58g=="],
|
||||||
|
|
||||||
"@inquirer/editor/@inquirer/type": ["@inquirer/type@4.0.7", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g=="],
|
"@inquirer/editor/@inquirer/type": ["@inquirer/type@4.0.6", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-J+9tdxOskuYuGjsvGaq00AamhDgjR7anhEW2dP4QdQpFCMPngCeC/bCYWQ5NsMWZRdsy53is7kAHb/+7cwDk2g=="],
|
||||||
|
|
||||||
"@inquirer/expand/@inquirer/core": ["@inquirer/core@11.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA=="],
|
"@inquirer/expand/@inquirer/core": ["@inquirer/core@11.2.0", "", { "dependencies": { "@inquirer/ansi": "^2.0.6", "@inquirer/figures": "^2.0.6", "@inquirer/type": "^4.0.6", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^4.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-joR1YS2sI0us+9d0I8ViqFbrRLONO8CFTuyvBX4ZVBSch+VsZiugUABdrhBXXJR1VyEzvpz5SQCix3keETQ58g=="],
|
||||||
|
|
||||||
"@inquirer/expand/@inquirer/type": ["@inquirer/type@4.0.7", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g=="],
|
"@inquirer/expand/@inquirer/type": ["@inquirer/type@4.0.6", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-J+9tdxOskuYuGjsvGaq00AamhDgjR7anhEW2dP4QdQpFCMPngCeC/bCYWQ5NsMWZRdsy53is7kAHb/+7cwDk2g=="],
|
||||||
|
|
||||||
"@inquirer/input/@inquirer/core": ["@inquirer/core@11.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA=="],
|
"@inquirer/input/@inquirer/core": ["@inquirer/core@11.2.0", "", { "dependencies": { "@inquirer/ansi": "^2.0.6", "@inquirer/figures": "^2.0.6", "@inquirer/type": "^4.0.6", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^4.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-joR1YS2sI0us+9d0I8ViqFbrRLONO8CFTuyvBX4ZVBSch+VsZiugUABdrhBXXJR1VyEzvpz5SQCix3keETQ58g=="],
|
||||||
|
|
||||||
"@inquirer/input/@inquirer/type": ["@inquirer/type@4.0.7", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g=="],
|
"@inquirer/input/@inquirer/type": ["@inquirer/type@4.0.6", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-J+9tdxOskuYuGjsvGaq00AamhDgjR7anhEW2dP4QdQpFCMPngCeC/bCYWQ5NsMWZRdsy53is7kAHb/+7cwDk2g=="],
|
||||||
|
|
||||||
"@inquirer/number/@inquirer/core": ["@inquirer/core@11.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA=="],
|
"@inquirer/number/@inquirer/core": ["@inquirer/core@11.2.0", "", { "dependencies": { "@inquirer/ansi": "^2.0.6", "@inquirer/figures": "^2.0.6", "@inquirer/type": "^4.0.6", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^4.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-joR1YS2sI0us+9d0I8ViqFbrRLONO8CFTuyvBX4ZVBSch+VsZiugUABdrhBXXJR1VyEzvpz5SQCix3keETQ58g=="],
|
||||||
|
|
||||||
"@inquirer/number/@inquirer/type": ["@inquirer/type@4.0.7", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g=="],
|
"@inquirer/number/@inquirer/type": ["@inquirer/type@4.0.6", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-J+9tdxOskuYuGjsvGaq00AamhDgjR7anhEW2dP4QdQpFCMPngCeC/bCYWQ5NsMWZRdsy53is7kAHb/+7cwDk2g=="],
|
||||||
|
|
||||||
"@inquirer/password/@inquirer/core": ["@inquirer/core@11.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA=="],
|
"@inquirer/password/@inquirer/core": ["@inquirer/core@11.2.0", "", { "dependencies": { "@inquirer/ansi": "^2.0.6", "@inquirer/figures": "^2.0.6", "@inquirer/type": "^4.0.6", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^4.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-joR1YS2sI0us+9d0I8ViqFbrRLONO8CFTuyvBX4ZVBSch+VsZiugUABdrhBXXJR1VyEzvpz5SQCix3keETQ58g=="],
|
||||||
|
|
||||||
"@inquirer/password/@inquirer/type": ["@inquirer/type@4.0.7", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g=="],
|
"@inquirer/password/@inquirer/type": ["@inquirer/type@4.0.6", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-J+9tdxOskuYuGjsvGaq00AamhDgjR7anhEW2dP4QdQpFCMPngCeC/bCYWQ5NsMWZRdsy53is7kAHb/+7cwDk2g=="],
|
||||||
|
|
||||||
"@inquirer/prompts/@inquirer/confirm": ["@inquirer/confirm@6.1.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ=="],
|
"@inquirer/prompts/@inquirer/confirm": ["@inquirer/confirm@6.1.0", "", { "dependencies": { "@inquirer/core": "^11.2.0", "@inquirer/type": "^4.0.6" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-USpeB76eqK7yGricDlGAupxWlp4a59qpeZOoNWaxO/nJln7agpJveyNkQ1d5u8YXG6TOqxZtQpKPORQQDrdVsA=="],
|
||||||
|
|
||||||
"@inquirer/rawlist/@inquirer/core": ["@inquirer/core@11.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA=="],
|
"@inquirer/rawlist/@inquirer/core": ["@inquirer/core@11.2.0", "", { "dependencies": { "@inquirer/ansi": "^2.0.6", "@inquirer/figures": "^2.0.6", "@inquirer/type": "^4.0.6", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^4.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-joR1YS2sI0us+9d0I8ViqFbrRLONO8CFTuyvBX4ZVBSch+VsZiugUABdrhBXXJR1VyEzvpz5SQCix3keETQ58g=="],
|
||||||
|
|
||||||
"@inquirer/rawlist/@inquirer/type": ["@inquirer/type@4.0.7", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g=="],
|
"@inquirer/rawlist/@inquirer/type": ["@inquirer/type@4.0.6", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-J+9tdxOskuYuGjsvGaq00AamhDgjR7anhEW2dP4QdQpFCMPngCeC/bCYWQ5NsMWZRdsy53is7kAHb/+7cwDk2g=="],
|
||||||
|
|
||||||
"@inquirer/search/@inquirer/core": ["@inquirer/core@11.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA=="],
|
"@inquirer/search/@inquirer/core": ["@inquirer/core@11.2.0", "", { "dependencies": { "@inquirer/ansi": "^2.0.6", "@inquirer/figures": "^2.0.6", "@inquirer/type": "^4.0.6", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^4.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-joR1YS2sI0us+9d0I8ViqFbrRLONO8CFTuyvBX4ZVBSch+VsZiugUABdrhBXXJR1VyEzvpz5SQCix3keETQ58g=="],
|
||||||
|
|
||||||
"@inquirer/search/@inquirer/type": ["@inquirer/type@4.0.7", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g=="],
|
"@inquirer/search/@inquirer/type": ["@inquirer/type@4.0.6", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-J+9tdxOskuYuGjsvGaq00AamhDgjR7anhEW2dP4QdQpFCMPngCeC/bCYWQ5NsMWZRdsy53is7kAHb/+7cwDk2g=="],
|
||||||
|
|
||||||
"@inquirer/select/@inquirer/core": ["@inquirer/core@11.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA=="],
|
"@inquirer/select/@inquirer/core": ["@inquirer/core@11.2.0", "", { "dependencies": { "@inquirer/ansi": "^2.0.6", "@inquirer/figures": "^2.0.6", "@inquirer/type": "^4.0.6", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^4.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-joR1YS2sI0us+9d0I8ViqFbrRLONO8CFTuyvBX4ZVBSch+VsZiugUABdrhBXXJR1VyEzvpz5SQCix3keETQ58g=="],
|
||||||
|
|
||||||
"@inquirer/select/@inquirer/type": ["@inquirer/type@4.0.7", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g=="],
|
"@inquirer/select/@inquirer/type": ["@inquirer/type@4.0.6", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-J+9tdxOskuYuGjsvGaq00AamhDgjR7anhEW2dP4QdQpFCMPngCeC/bCYWQ5NsMWZRdsy53is7kAHb/+7cwDk2g=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk/hono": ["hono@4.12.19", "", {}, "sha512-xa3eYXYXx68XTT4hZ7dRzsXBhaq85ToSrlUJNoR0gwz/1Ap/CNwX47wfvV7pc/xWhjKVVkLT7zBJy8chhNguqQ=="],
|
"@modelcontextprotocol/sdk/hono": ["hono@4.12.19", "", {}, "sha512-xa3eYXYXx68XTT4hZ7dRzsXBhaq85ToSrlUJNoR0gwz/1Ap/CNwX47wfvV7pc/xWhjKVVkLT7zBJy8chhNguqQ=="],
|
||||||
|
|
||||||
|
|
@ -2610,9 +2607,9 @@
|
||||||
|
|
||||||
"@types/ws/@types/node": ["@types/node@25.8.0", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ=="],
|
"@types/ws/@types/node": ["@types/node@25.8.0", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ=="],
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-test/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
|
"@voidzero-dev/vite-plus-core/postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
|
||||||
|
|
||||||
"better-call/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
|
"@voidzero-dev/vite-plus-test/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
|
||||||
|
|
||||||
"better-call/rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
|
"better-call/rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
|
||||||
|
|
||||||
|
|
@ -2634,8 +2631,6 @@
|
||||||
|
|
||||||
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||||
|
|
||||||
"fumadocs-mdx/tinyexec": ["tinyexec@1.2.3", "", {}, "sha512-g62dB+w1/OEFnPvmX0yd/HnetYITOL+1nJW7kitOycOeAvmbWC/nu0fwmmQ/kupNojqExzyC/T++pST/jRJ2mQ=="],
|
|
||||||
|
|
||||||
"happy-dom/@types/node": ["@types/node@25.8.0", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ=="],
|
"happy-dom/@types/node": ["@types/node@25.8.0", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ=="],
|
||||||
|
|
||||||
"hast-util-raw/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
"hast-util-raw/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||||
|
|
@ -2666,8 +2661,6 @@
|
||||||
|
|
||||||
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||||
|
|
||||||
"proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
|
||||||
|
|
||||||
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
"recharts/es-toolkit": ["es-toolkit@1.46.1", "", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="],
|
"recharts/es-toolkit": ["es-toolkit@1.46.1", "", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="],
|
||||||
|
|
@ -2678,8 +2671,6 @@
|
||||||
|
|
||||||
"router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
|
"router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
|
||||||
|
|
||||||
"shadcn/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
|
||||||
|
|
||||||
"shadcn/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="],
|
"shadcn/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="],
|
||||||
|
|
||||||
"shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
@ -2700,6 +2691,8 @@
|
||||||
|
|
||||||
"vite/rolldown": ["rolldown@1.0.2", "", { "dependencies": { "@oxc-project/types": "=0.132.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.2", "@rolldown/binding-darwin-arm64": "1.0.2", "@rolldown/binding-darwin-x64": "1.0.2", "@rolldown/binding-freebsd-x64": "1.0.2", "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", "@rolldown/binding-linux-arm64-gnu": "1.0.2", "@rolldown/binding-linux-arm64-musl": "1.0.2", "@rolldown/binding-linux-ppc64-gnu": "1.0.2", "@rolldown/binding-linux-s390x-gnu": "1.0.2", "@rolldown/binding-linux-x64-gnu": "1.0.2", "@rolldown/binding-linux-x64-musl": "1.0.2", "@rolldown/binding-openharmony-arm64": "1.0.2", "@rolldown/binding-wasm32-wasi": "1.0.2", "@rolldown/binding-win32-arm64-msvc": "1.0.2", "@rolldown/binding-win32-x64-msvc": "1.0.2" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g=="],
|
"vite/rolldown": ["rolldown@1.0.2", "", { "dependencies": { "@oxc-project/types": "=0.132.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.2", "@rolldown/binding-darwin-arm64": "1.0.2", "@rolldown/binding-darwin-x64": "1.0.2", "@rolldown/binding-freebsd-x64": "1.0.2", "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", "@rolldown/binding-linux-arm64-gnu": "1.0.2", "@rolldown/binding-linux-arm64-musl": "1.0.2", "@rolldown/binding-linux-ppc64-gnu": "1.0.2", "@rolldown/binding-linux-s390x-gnu": "1.0.2", "@rolldown/binding-linux-x64-gnu": "1.0.2", "@rolldown/binding-linux-x64-musl": "1.0.2", "@rolldown/binding-openharmony-arm64": "1.0.2", "@rolldown/binding-wasm32-wasi": "1.0.2", "@rolldown/binding-win32-arm64-msvc": "1.0.2", "@rolldown/binding-win32-x64-msvc": "1.0.2" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g=="],
|
||||||
|
|
||||||
|
"vite-plus/oxfmt": ["oxfmt@0.48.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.48.0", "@oxfmt/binding-android-arm64": "0.48.0", "@oxfmt/binding-darwin-arm64": "0.48.0", "@oxfmt/binding-darwin-x64": "0.48.0", "@oxfmt/binding-freebsd-x64": "0.48.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.48.0", "@oxfmt/binding-linux-arm-musleabihf": "0.48.0", "@oxfmt/binding-linux-arm64-gnu": "0.48.0", "@oxfmt/binding-linux-arm64-musl": "0.48.0", "@oxfmt/binding-linux-ppc64-gnu": "0.48.0", "@oxfmt/binding-linux-riscv64-gnu": "0.48.0", "@oxfmt/binding-linux-riscv64-musl": "0.48.0", "@oxfmt/binding-linux-s390x-gnu": "0.48.0", "@oxfmt/binding-linux-x64-gnu": "0.48.0", "@oxfmt/binding-linux-x64-musl": "0.48.0", "@oxfmt/binding-openharmony-arm64": "0.48.0", "@oxfmt/binding-win32-arm64-msvc": "0.48.0", "@oxfmt/binding-win32-ia32-msvc": "0.48.0", "@oxfmt/binding-win32-x64-msvc": "0.48.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-AVaLh+7XeGx+R1zfFV+f6VV61nT2MWVJXVUDhbTm5LBWGyNt64xAyh3NYYyjeY2WykNt9AvqSQLPHcbWquYF9g=="],
|
||||||
|
|
||||||
"wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
@ -2720,9 +2713,27 @@
|
||||||
|
|
||||||
"@dotenvx/dotenvx/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
|
"@dotenvx/dotenvx/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
|
||||||
|
|
||||||
"@inquirer/prompts/@inquirer/confirm/@inquirer/core": ["@inquirer/core@11.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA=="],
|
"@inquirer/checkbox/@inquirer/core/mute-stream": ["mute-stream@4.0.0", "", {}, "sha512-gSrprq0fJ3EiOErzjdIZrjysVVmJ4uu1QWfCDss5LypA5OXvrMje5Ym5z6V6RLyJ2eF87lasX7t6a0AnFvZblg=="],
|
||||||
|
|
||||||
"@inquirer/prompts/@inquirer/confirm/@inquirer/type": ["@inquirer/type@4.0.7", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g=="],
|
"@inquirer/editor/@inquirer/core/mute-stream": ["mute-stream@4.0.0", "", {}, "sha512-gSrprq0fJ3EiOErzjdIZrjysVVmJ4uu1QWfCDss5LypA5OXvrMje5Ym5z6V6RLyJ2eF87lasX7t6a0AnFvZblg=="],
|
||||||
|
|
||||||
|
"@inquirer/expand/@inquirer/core/mute-stream": ["mute-stream@4.0.0", "", {}, "sha512-gSrprq0fJ3EiOErzjdIZrjysVVmJ4uu1QWfCDss5LypA5OXvrMje5Ym5z6V6RLyJ2eF87lasX7t6a0AnFvZblg=="],
|
||||||
|
|
||||||
|
"@inquirer/input/@inquirer/core/mute-stream": ["mute-stream@4.0.0", "", {}, "sha512-gSrprq0fJ3EiOErzjdIZrjysVVmJ4uu1QWfCDss5LypA5OXvrMje5Ym5z6V6RLyJ2eF87lasX7t6a0AnFvZblg=="],
|
||||||
|
|
||||||
|
"@inquirer/number/@inquirer/core/mute-stream": ["mute-stream@4.0.0", "", {}, "sha512-gSrprq0fJ3EiOErzjdIZrjysVVmJ4uu1QWfCDss5LypA5OXvrMje5Ym5z6V6RLyJ2eF87lasX7t6a0AnFvZblg=="],
|
||||||
|
|
||||||
|
"@inquirer/password/@inquirer/core/mute-stream": ["mute-stream@4.0.0", "", {}, "sha512-gSrprq0fJ3EiOErzjdIZrjysVVmJ4uu1QWfCDss5LypA5OXvrMje5Ym5z6V6RLyJ2eF87lasX7t6a0AnFvZblg=="],
|
||||||
|
|
||||||
|
"@inquirer/prompts/@inquirer/confirm/@inquirer/core": ["@inquirer/core@11.2.0", "", { "dependencies": { "@inquirer/ansi": "^2.0.6", "@inquirer/figures": "^2.0.6", "@inquirer/type": "^4.0.6", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^4.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-joR1YS2sI0us+9d0I8ViqFbrRLONO8CFTuyvBX4ZVBSch+VsZiugUABdrhBXXJR1VyEzvpz5SQCix3keETQ58g=="],
|
||||||
|
|
||||||
|
"@inquirer/prompts/@inquirer/confirm/@inquirer/type": ["@inquirer/type@4.0.6", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-J+9tdxOskuYuGjsvGaq00AamhDgjR7anhEW2dP4QdQpFCMPngCeC/bCYWQ5NsMWZRdsy53is7kAHb/+7cwDk2g=="],
|
||||||
|
|
||||||
|
"@inquirer/rawlist/@inquirer/core/mute-stream": ["mute-stream@4.0.0", "", {}, "sha512-gSrprq0fJ3EiOErzjdIZrjysVVmJ4uu1QWfCDss5LypA5OXvrMje5Ym5z6V6RLyJ2eF87lasX7t6a0AnFvZblg=="],
|
||||||
|
|
||||||
|
"@inquirer/search/@inquirer/core/mute-stream": ["mute-stream@4.0.0", "", {}, "sha512-gSrprq0fJ3EiOErzjdIZrjysVVmJ4uu1QWfCDss5LypA5OXvrMje5Ym5z6V6RLyJ2eF87lasX7t6a0AnFvZblg=="],
|
||||||
|
|
||||||
|
"@inquirer/select/@inquirer/core/mute-stream": ["mute-stream@4.0.0", "", {}, "sha512-gSrprq0fJ3EiOErzjdIZrjysVVmJ4uu1QWfCDss5LypA5OXvrMje5Ym5z6V6RLyJ2eF87lasX7t6a0AnFvZblg=="],
|
||||||
|
|
||||||
"@tanstack/react-router/@tanstack/react-store/@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="],
|
"@tanstack/react-router/@tanstack/react-store/@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="],
|
||||||
|
|
||||||
|
|
@ -2766,6 +2777,44 @@
|
||||||
|
|
||||||
"tough-cookie/tldts/tldts-core": ["tldts-core@7.0.30", "", {}, "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q=="],
|
"tough-cookie/tldts/tldts-core": ["tldts-core@7.0.30", "", {}, "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q=="],
|
||||||
|
|
||||||
|
"vite-plus/oxfmt/@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.48.0", "", { "os": "android", "cpu": "arm" }, "sha512-uwqk+/KhQvBIpULD8SMM/zAafMRC/+DV/xsEQjkkIsJ/kLmEI/2bxonVowcYTiXqqZ/a0FEW8DPkZY3VvwELDA=="],
|
||||||
|
|
||||||
|
"vite-plus/oxfmt/@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.48.0", "", { "os": "android", "cpu": "arm64" }, "sha512-VUCiKuXK5+McVssgHEJdrcGK7hRJzrRb36zm9/jwzMholyYt4BgXhw5Nm1V1DX6Ce717Zi/1jk432b/tgmQgtQ=="],
|
||||||
|
|
||||||
|
"vite-plus/oxfmt/@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.48.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IkKp8rnIyQLW6Jt+6jragCbUVYSayk55lapiprLjIVvt4NczLyO/nwX2GgefLQ5iaBdfS8UEAFgCs/pLO6Cl0w=="],
|
||||||
|
|
||||||
|
"vite-plus/oxfmt/@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.48.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-+aFuhsGIuvnoOjXyKVHMhPKJZR1kQkAl8QyrKoMlA7yJsSTC3N0Asl53La8TChSHhW8epToQ/Q0nvLmEmfNmLg=="],
|
||||||
|
|
||||||
|
"vite-plus/oxfmt/@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.48.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fbqzQL8FjI9gGnktI7RIo0dksDziTAYBy7xlI7jU7eID5fxLF/25fS4Xj6GydD8Y5oWHL83U4NK160QaOAxtyg=="],
|
||||||
|
|
||||||
|
"vite-plus/oxfmt/@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.48.0", "", { "os": "linux", "cpu": "arm" }, "sha512-hn4i0zhAyTiB3ZHjQfYUZkDvrbVkohw1S7pySWxWUoZ87HnkDoTFThj7QTxk40hNPOTUP0vHbPRNamFIv1HBJQ=="],
|
||||||
|
|
||||||
|
"vite-plus/oxfmt/@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.48.0", "", { "os": "linux", "cpu": "arm" }, "sha512-R4WBD9qF3QM9hqgdAa+fBGXmquTvDUujrPQ36t2Sjk8RPOSKGHDeN7l/khr10hqbQaOq9KCgPHG9ubNET/X/RQ=="],
|
||||||
|
|
||||||
|
"vite-plus/oxfmt/@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.48.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-5bVdwSwlm1M8wbYCorLOxWxUBw/8tBvHYyQNIfwWVPwOJaj5vg1APSGJQVpwJfV5VNE9PSrR91UKEpoNwHhqUA=="],
|
||||||
|
|
||||||
|
"vite-plus/oxfmt/@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.48.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-vCS3Fk7gFslTqE1lUE2IlroyVV7u/9SmMA/uBqDoshuck2psGWcjW0ePyPZI3rM3+qtf2pDaMVIKMHozraifuw=="],
|
||||||
|
|
||||||
|
"vite-plus/oxfmt/@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.48.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gKtfFfueUClXDumyoHUbymqRf7prHejOOyzJK0eIJn93GF9JBdFHdo60TM1ZBHxkEwZvjuOgHmKtneKbEOc/Eg=="],
|
||||||
|
|
||||||
|
"vite-plus/oxfmt/@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.48.0", "", { "os": "linux", "cpu": "none" }, "sha512-SYt0UhOvZD/UwZz9sXq6J2uAw8o24f5VZpLB2DH01f6MevshmlgakQlZe2lwek2sZJkd07eLu7mZa0g7yeiw7Q=="],
|
||||||
|
|
||||||
|
"vite-plus/oxfmt/@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.48.0", "", { "os": "linux", "cpu": "none" }, "sha512-JLbrwck2AopG4ud/XklZO5N+qxGC7cS7ROvXZVNfx0MCLDDL2kGOLvzuWORkVjnjAM0CMAfIMU2zNBtQbM+4dw=="],
|
||||||
|
|
||||||
|
"vite-plus/oxfmt/@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.48.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-mdxt5L8OQLxkQH+JVpdC/lknZNe0lX4hlO3d8+xvw2wToo+iDrid9tiGOd5bmHfUVd5wVhrUry0qlu5vq66NkQ=="],
|
||||||
|
|
||||||
|
"vite-plus/oxfmt/@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.48.0", "", { "os": "linux", "cpu": "x64" }, "sha512-oEz1BQwMrV7OMEFx/3VPDU3n9TM0AnxpktDYXjEg5i6nTX87wo18wSfBvkl4tzAICdKtoAQAdBIl7Y7hsPlx5w=="],
|
||||||
|
|
||||||
|
"vite-plus/oxfmt/@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.48.0", "", { "os": "linux", "cpu": "x64" }, "sha512-g2SKTTurP5mWjd8Ecait0erYqmltL4IqW1EwttM25BxM6NiTt4ubobJYMR1uox1V2QgG4UfHH10CGRvWlUixjw=="],
|
||||||
|
|
||||||
|
"vite-plus/oxfmt/@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.48.0", "", { "os": "none", "cpu": "arm64" }, "sha512-CIg24VgheEpvolHL2gQuax5qcQ602bRMHrJ9g8XsQr3iVj9aSPgopigBKuMqrXsupwkrU+RQCn5cG8PgFntR6w=="],
|
||||||
|
|
||||||
|
"vite-plus/oxfmt/@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.48.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-zeaWkcxcEULwkGF3I/HgEvcDPN8buYDrxibBUa/IFh5Vmwyge+KpLO+hEwSovW349H0O/C0Z2kaFmEzEDm00/Q=="],
|
||||||
|
|
||||||
|
"vite-plus/oxfmt/@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.48.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-yiEKnIAGvx5CyZQOlMaNlZkAbwT7/Quk0j3WLt+PR5hK+qYjPTRRJYDfD77wCBPLvEYAG41v4KG3iL0H+uxoxg=="],
|
||||||
|
|
||||||
|
"vite-plus/oxfmt/@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.48.0", "", { "os": "win32", "cpu": "x64" }, "sha512-GSD2+7t2UoVMV2NgxXypa4bKewflPMAjYnF0Xw9/ht82ZfafAHhb8STwrEd7wlH2PFogt5zw3WVCxYJaHUdbeQ=="],
|
||||||
|
|
||||||
"vite/rolldown/@oxc-project/types": ["@oxc-project/types@0.132.0", "", {}, "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ=="],
|
"vite/rolldown/@oxc-project/types": ["@oxc-project/types@0.132.0", "", {}, "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ=="],
|
||||||
|
|
||||||
"vite/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.2", "", { "os": "android", "cpu": "arm64" }, "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ=="],
|
"vite/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.2", "", { "os": "android", "cpu": "arm64" }, "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ=="],
|
||||||
|
|
@ -2797,5 +2846,7 @@
|
||||||
"vite/rolldown/@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A=="],
|
"vite/rolldown/@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A=="],
|
||||||
|
|
||||||
"vite/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ=="],
|
"vite/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ=="],
|
||||||
|
|
||||||
|
"@inquirer/prompts/@inquirer/confirm/@inquirer/core/mute-stream": ["mute-stream@4.0.0", "", {}, "sha512-gSrprq0fJ3EiOErzjdIZrjysVVmJ4uu1QWfCDss5LypA5OXvrMje5Ym5z6V6RLyJ2eF87lasX7t6a0AnFvZblg=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import { execFileSync } from "node:child_process";
|
|
||||||
import { defaultPlugins, defineConfig } from "@hey-api/openapi-ts";
|
import { defaultPlugins, defineConfig } from "@hey-api/openapi-ts";
|
||||||
|
import { config } from "./app/server/core/config.js";
|
||||||
const openApiInput = `${execFileSync("portless", ["get", "zerobyte"], { encoding: "utf8" }).trim().replace(/\/$/, "")}/api/v1/openapi.json`;
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
input: openApiInput,
|
input: `http://${config.serverIp}:3000/api/v1/openapi.json`,
|
||||||
output: {
|
output: {
|
||||||
path: "./app/client/api-client",
|
path: "./app/client/api-client",
|
||||||
header: ["// @ts-nocheck", "// This file is auto-generated by @hey-api/openapi-ts"],
|
header: ["// @ts-nocheck", "// This file is auto-generated by @hey-api/openapi-ts"],
|
||||||
|
|
|
||||||
34
package.json
34
package.json
|
|
@ -15,7 +15,6 @@
|
||||||
"build": "bunx --bun vite build",
|
"build": "bunx --bun vite build",
|
||||||
"start": "bun run .output/server/index.mjs",
|
"start": "bun run .output/server/index.mjs",
|
||||||
"build:backend-integration": "bun build app/test/backend-integration/index.ts --outdir .output/backend-integration --target bun",
|
"build:backend-integration": "bun build app/test/backend-integration/index.ts --outdir .output/backend-integration --target bun",
|
||||||
"test:integration": "bash app/test/integration/run.sh",
|
|
||||||
"test:integration:backends": "bash app/test/backend-integration/run.sh",
|
"test:integration:backends": "bash app/test/backend-integration/run.sh",
|
||||||
"preview": "bunx --bun vite preview",
|
"preview": "bunx --bun vite preview",
|
||||||
"cli:dev": "bun run app/server/cli/main.ts",
|
"cli:dev": "bun run app/server/cli/main.ts",
|
||||||
|
|
@ -25,7 +24,7 @@
|
||||||
"start:prod": "docker compose down && docker compose up --build zerobyte-prod",
|
"start:prod": "docker compose down && docker compose up --build zerobyte-prod",
|
||||||
"start:e2e": "docker compose down && mkdir -p playwright/data playwright/temp playwright/tinyauth/app-data playwright/tinyauth/caddy-data && rm -rf playwright/data/* playwright/.auth/user.json playwright/restic.pass playwright/temp/* playwright/tinyauth/app-data/* && docker compose up --build zerobyte-e2e",
|
"start:e2e": "docker compose down && mkdir -p playwright/data playwright/temp playwright/tinyauth/app-data playwright/tinyauth/caddy-data && rm -rf playwright/data/* playwright/.auth/user.json playwright/restic.pass playwright/temp/* playwright/tinyauth/app-data/* && docker compose up --build zerobyte-e2e",
|
||||||
"start:distroless": "docker compose down && docker compose up --build zerobyte-distroless",
|
"start:distroless": "docker compose down && docker compose up --build zerobyte-distroless",
|
||||||
"gen:api-client": "NODE_TLS_REJECT_UNAUTHORIZED=0 dotenv -e .env.local -- openapi-ts",
|
"gen:api-client": "dotenv -e .env.local -- openapi-ts",
|
||||||
"gen:migrations": "dotenv -e .env.local -- drizzle-kit generate",
|
"gen:migrations": "dotenv -e .env.local -- drizzle-kit generate",
|
||||||
"staged": "vp staged",
|
"staged": "vp staged",
|
||||||
"studio": "drizzle-kit studio",
|
"studio": "drizzle-kit studio",
|
||||||
|
|
@ -37,14 +36,14 @@
|
||||||
"test:codegen": "playwright codegen localhost:4096"
|
"test:codegen": "playwright codegen localhost:4096"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@better-auth/passkey": "^1.6.12",
|
"@better-auth/passkey": "^1.6.11",
|
||||||
"@better-auth/sso": "^1.6.12",
|
"@better-auth/sso": "^1.6.11",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@hono/standard-validator": "^0.2.2",
|
"@hono/standard-validator": "^0.2.2",
|
||||||
"@hookform/resolvers": "^5.4.0",
|
"@hookform/resolvers": "^5.4.0",
|
||||||
"@inquirer/prompts": "^8.5.1",
|
"@inquirer/prompts": "^8.5.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
|
@ -63,20 +62,20 @@
|
||||||
"@tanstack/react-hotkeys": "^0.10.0",
|
"@tanstack/react-hotkeys": "^0.10.0",
|
||||||
"@tanstack/react-query": "^5.100.14",
|
"@tanstack/react-query": "^5.100.14",
|
||||||
"@tanstack/react-query-devtools": "^5.100.14",
|
"@tanstack/react-query-devtools": "^5.100.14",
|
||||||
"@tanstack/react-router": "^1.170.10",
|
"@tanstack/react-router": "^1.170.8",
|
||||||
"@tanstack/react-router-ssr-query": "^1.167.1",
|
"@tanstack/react-router-ssr-query": "^1.167.0",
|
||||||
"@tanstack/react-start": "^1.168.18",
|
"@tanstack/react-start": "^1.168.13",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@zerobyte/contracts": "workspace:*",
|
"@zerobyte/contracts": "workspace:*",
|
||||||
"@zerobyte/core": "workspace:*",
|
"@zerobyte/core": "workspace:*",
|
||||||
"better-auth": "^1.6.12",
|
"better-auth": "^1.6.11",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"commander": "^15.0.0",
|
"commander": "^14.0.3",
|
||||||
"consola": "^3.4.2",
|
"consola": "^3.4.2",
|
||||||
"content-disposition": "^2.0.1",
|
"content-disposition": "^2.0.0",
|
||||||
"cron-parser": "^5.5.0",
|
"cron-parser": "^5.5.0",
|
||||||
"date-fns": "^4.4.0",
|
"date-fns": "^4.3.0",
|
||||||
"dither-plugin": "^1.1.1",
|
"dither-plugin": "^1.1.1",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"drizzle-orm": "^1.0.0-rc.4-5d5b77c",
|
"drizzle-orm": "^1.0.0-rc.4-5d5b77c",
|
||||||
|
|
@ -87,12 +86,11 @@
|
||||||
"hono-rate-limiter": "^0.5.3",
|
"hono-rate-limiter": "^0.5.3",
|
||||||
"http-errors-enhanced": "^4.0.2",
|
"http-errors-enhanced": "^4.0.2",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"ipaddr.js": "^2.4.0",
|
"lucide-react": "^1.16.0",
|
||||||
"lucide-react": "^1.17.0",
|
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
"react-hook-form": "^7.77.0",
|
"react-hook-form": "^7.76.1",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"recharts": "3.8.1",
|
"recharts": "3.8.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
|
|
@ -104,7 +102,7 @@
|
||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@better-auth/utils": "0.4.1",
|
"@better-auth/utils": "0.4.0",
|
||||||
"@effect/language-service": "^0.86.2",
|
"@effect/language-service": "^0.86.2",
|
||||||
"@faker-js/faker": "^10.4.0",
|
"@faker-js/faker": "^10.4.0",
|
||||||
"@hey-api/openapi-ts": "^0.97.3",
|
"@hey-api/openapi-ts": "^0.97.3",
|
||||||
|
|
@ -127,7 +125,7 @@
|
||||||
"@vitejs/plugin-react": "^6.0.2",
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
"babel-plugin-react-compiler": "^1.0.0",
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"drizzle-kit": "^1.0.0-rc.4-ca0f029",
|
"drizzle-kit": "^1.0.0-rc.4-5d5b77c",
|
||||||
"fast-check": "^4.8.0",
|
"fast-check": "^4.8.0",
|
||||||
"happy-dom": "^20.9.0",
|
"happy-dom": "^20.9.0",
|
||||||
"msw": "^2.14.6",
|
"msw": "^2.14.6",
|
||||||
|
|
@ -137,7 +135,7 @@
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"vite": "^8.0.14",
|
"vite": "^8.0.14",
|
||||||
"vite-plus": "^0.1.23",
|
"vite-plus": "^0.1.22",
|
||||||
"vitest": "^4.1.7",
|
"vitest": "^4.1.7",
|
||||||
"wait-for-expect": "^4.0.0"
|
"wait-for-expect": "^4.0.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -21,27 +21,6 @@ import {
|
||||||
|
|
||||||
const compressionModeSchema = z.enum(["off", "auto", "max"]) satisfies z.ZodType<CompressionMode>;
|
const compressionModeSchema = z.enum(["off", "auto", "max"]) satisfies z.ZodType<CompressionMode>;
|
||||||
|
|
||||||
export const AGENT_PROTOCOL_VERSION = 1;
|
|
||||||
export const SUPPORTED_AGENT_PROTOCOL_MIN_VERSION = 1;
|
|
||||||
export const SUPPORTED_AGENT_PROTOCOL_MAX_VERSION = 1;
|
|
||||||
|
|
||||||
export type AgentProtocolRejectionReason =
|
|
||||||
| "agent_too_old"
|
|
||||||
| "agent_too_new"
|
|
||||||
| "invalid_agent_ready"
|
|
||||||
| "unexpected_startup_message"
|
|
||||||
| "invalid_startup_json";
|
|
||||||
|
|
||||||
export type AgentProtocolRejection = {
|
|
||||||
reason: AgentProtocolRejectionReason;
|
|
||||||
protocolVersion?: number;
|
|
||||||
supportedProtocolMinVersion: number;
|
|
||||||
supportedProtocolMaxVersion: number;
|
|
||||||
hostname?: string;
|
|
||||||
platform?: string;
|
|
||||||
messageType?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const backupExecutionOptionsSchema = z.object({
|
const backupExecutionOptionsSchema = z.object({
|
||||||
oneFileSystem: z.boolean(),
|
oneFileSystem: z.boolean(),
|
||||||
excludePatterns: z.array(z.string()).nullable(),
|
excludePatterns: z.array(z.string()).nullable(),
|
||||||
|
|
@ -200,21 +179,6 @@ const agentReadySchema = z.object({
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const agentStartupMessageSchema = z.object({
|
|
||||||
type: z.string(),
|
|
||||||
payload: z.unknown().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const stableAgentReadySchema = z.object({
|
|
||||||
type: z.literal("agent.ready"),
|
|
||||||
payload: z.object({
|
|
||||||
protocolVersion: z.number(),
|
|
||||||
hostname: z.string().optional(),
|
|
||||||
platform: z.string().optional(),
|
|
||||||
capabilities: z.record(z.string(), z.unknown()).optional(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const backupStartedSchema = z.object({
|
const backupStartedSchema = z.object({
|
||||||
type: z.literal("backup.started"),
|
type: z.literal("backup.started"),
|
||||||
payload: z.object({ jobId: z.string(), scheduleId: z.string() }),
|
payload: z.object({ jobId: z.string(), scheduleId: z.string() }),
|
||||||
|
|
@ -339,69 +303,6 @@ export const parseAgentMessage = (data: string) => {
|
||||||
return agentMessageSchema.safeParse(parsed);
|
return agentMessageSchema.safeParse(parsed);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseAgentStartupMessage = (data: string): AgentProtocolRejection | { success: true; data: unknown } => {
|
|
||||||
const parsed = safeJsonParse(data);
|
|
||||||
if (parsed === null) {
|
|
||||||
return {
|
|
||||||
reason: "invalid_startup_json",
|
|
||||||
supportedProtocolMinVersion: SUPPORTED_AGENT_PROTOCOL_MIN_VERSION,
|
|
||||||
supportedProtocolMaxVersion: SUPPORTED_AGENT_PROTOCOL_MAX_VERSION,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const startupMessage = agentStartupMessageSchema.safeParse(parsed);
|
|
||||||
if (!startupMessage.success) {
|
|
||||||
return {
|
|
||||||
reason: "unexpected_startup_message",
|
|
||||||
supportedProtocolMinVersion: SUPPORTED_AGENT_PROTOCOL_MIN_VERSION,
|
|
||||||
supportedProtocolMaxVersion: SUPPORTED_AGENT_PROTOCOL_MAX_VERSION,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startupMessage.data.type !== "agent.ready") {
|
|
||||||
return {
|
|
||||||
reason: "unexpected_startup_message",
|
|
||||||
messageType: startupMessage.data.type,
|
|
||||||
supportedProtocolMinVersion: SUPPORTED_AGENT_PROTOCOL_MIN_VERSION,
|
|
||||||
supportedProtocolMaxVersion: SUPPORTED_AGENT_PROTOCOL_MAX_VERSION,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const readyMessage = stableAgentReadySchema.safeParse(parsed);
|
|
||||||
if (!readyMessage.success) {
|
|
||||||
return {
|
|
||||||
reason: "invalid_agent_ready",
|
|
||||||
supportedProtocolMinVersion: SUPPORTED_AGENT_PROTOCOL_MIN_VERSION,
|
|
||||||
supportedProtocolMaxVersion: SUPPORTED_AGENT_PROTOCOL_MAX_VERSION,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const { protocolVersion, hostname, platform } = readyMessage.data.payload;
|
|
||||||
if (protocolVersion < SUPPORTED_AGENT_PROTOCOL_MIN_VERSION) {
|
|
||||||
return {
|
|
||||||
reason: "agent_too_old",
|
|
||||||
protocolVersion,
|
|
||||||
hostname,
|
|
||||||
platform,
|
|
||||||
supportedProtocolMinVersion: SUPPORTED_AGENT_PROTOCOL_MIN_VERSION,
|
|
||||||
supportedProtocolMaxVersion: SUPPORTED_AGENT_PROTOCOL_MAX_VERSION,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (protocolVersion > SUPPORTED_AGENT_PROTOCOL_MAX_VERSION) {
|
|
||||||
return {
|
|
||||||
reason: "agent_too_new",
|
|
||||||
protocolVersion,
|
|
||||||
hostname,
|
|
||||||
platform,
|
|
||||||
supportedProtocolMinVersion: SUPPORTED_AGENT_PROTOCOL_MIN_VERSION,
|
|
||||||
supportedProtocolMaxVersion: SUPPORTED_AGENT_PROTOCOL_MAX_VERSION,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, data: parsed };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createControllerMessage = <TType extends ControllerMessage["type"]>(
|
export const createControllerMessage = <TType extends ControllerMessage["type"]>(
|
||||||
type: TType,
|
type: TType,
|
||||||
payload: PayloadForMessage<ControllerMessage, TType>,
|
payload: PayloadForMessage<ControllerMessage, TType>,
|
||||||
|
|
|
||||||
169
packages/core/src/restic/lock-diagnostics.ts
Normal file
169
packages/core/src/restic/lock-diagnostics.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
import { logger, safeExec } from "../node";
|
||||||
|
import { toMessage } from "../utils";
|
||||||
|
import { ResticLockError } from "./error";
|
||||||
|
import { addCommonArgs } from "./helpers/add-common-args";
|
||||||
|
import { buildEnv } from "./helpers/build-env";
|
||||||
|
import { buildRepoUrl } from "./helpers/build-repo-url";
|
||||||
|
import { cleanupTemporaryKeys } from "./helpers/cleanup-temporary-keys";
|
||||||
|
import type { RepositoryConfig } from "./schemas";
|
||||||
|
import type { ResticDeps } from "./types";
|
||||||
|
|
||||||
|
type ResticLockDiagnosticContext = {
|
||||||
|
error: unknown;
|
||||||
|
operation: string;
|
||||||
|
repositoryConfig: RepositoryConfig;
|
||||||
|
organizationId: string;
|
||||||
|
resticDeps: ResticDeps;
|
||||||
|
relatedRepositoryConfigs?: RepositoryConfig[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const LOCK_ERROR_PATTERNS = [
|
||||||
|
/unable to create lock in backend/i,
|
||||||
|
/repository is already locked/i,
|
||||||
|
/failed to lock repository/i,
|
||||||
|
/"code"\s*:\s*11/i,
|
||||||
|
/\bexit_error\b.*\b11\b/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const isResticLockFailure = (error: unknown) => {
|
||||||
|
if (error instanceof ResticLockError) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = toMessage(error);
|
||||||
|
return LOCK_ERROR_PATTERNS.some((pattern) => pattern.test(message));
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseLockIds = (stdout: string) => {
|
||||||
|
const ids = new Set<string>();
|
||||||
|
|
||||||
|
for (const line of stdout.split("\n")) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
|
||||||
|
const jsonId = trimmed.match(/"id"\s*:\s*"([^"]+)"/)?.[1];
|
||||||
|
if (jsonId) {
|
||||||
|
ids.add(jsonId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hexId = trimmed.match(/[a-f0-9]{64}/i)?.[0];
|
||||||
|
if (hexId) {
|
||||||
|
ids.add(hexId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...ids].slice(0, 20);
|
||||||
|
};
|
||||||
|
|
||||||
|
const inspectResticLocks = async (config: RepositoryConfig, organizationId: string, deps: ResticDeps) => {
|
||||||
|
const repoUrl = buildRepoUrl(config);
|
||||||
|
const env = await buildEnv(config, organizationId, deps);
|
||||||
|
const baseArgs = ["--repo", repoUrl];
|
||||||
|
addCommonArgs(baseArgs, env, config);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const listResult = await safeExec({
|
||||||
|
command: "restic",
|
||||||
|
args: [...baseArgs, "list", "locks"],
|
||||||
|
env,
|
||||||
|
timeout: 15_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const lockIds = listResult.exitCode === 0 ? parseLockIds(listResult.stdout) : [];
|
||||||
|
const lockDetails = [];
|
||||||
|
|
||||||
|
for (const lockId of lockIds) {
|
||||||
|
const catResult = await safeExec({
|
||||||
|
command: "restic",
|
||||||
|
args: [...baseArgs, "cat", "lock", lockId],
|
||||||
|
env,
|
||||||
|
timeout: 15_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
lockDetails.push({
|
||||||
|
lockId,
|
||||||
|
exitCode: catResult.exitCode,
|
||||||
|
stdout: catResult.stdout,
|
||||||
|
stderr: catResult.stderr,
|
||||||
|
timedOut: catResult.timedOut,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
repoUrl,
|
||||||
|
list: {
|
||||||
|
exitCode: listResult.exitCode,
|
||||||
|
stdout: listResult.stdout,
|
||||||
|
stderr: listResult.stderr,
|
||||||
|
timedOut: listResult.timedOut,
|
||||||
|
},
|
||||||
|
lockIds,
|
||||||
|
lockDetails,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
await cleanupTemporaryKeys(env, deps);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logResticLockFailureDiagnostics = async ({
|
||||||
|
error,
|
||||||
|
operation,
|
||||||
|
repositoryConfig,
|
||||||
|
organizationId,
|
||||||
|
resticDeps,
|
||||||
|
relatedRepositoryConfigs = [],
|
||||||
|
}: ResticLockDiagnosticContext) => {
|
||||||
|
if (!isResticLockFailure(error)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const configsByRepoUrl = new Map(
|
||||||
|
[repositoryConfig, ...relatedRepositoryConfigs].map((config) => [buildRepoUrl(config), config]),
|
||||||
|
);
|
||||||
|
const configsToInspect = [...configsByRepoUrl.entries()];
|
||||||
|
|
||||||
|
logger.error("[ResticLockFailure] Restic repository lock failure detected", {
|
||||||
|
operation,
|
||||||
|
error: toMessage(error),
|
||||||
|
process: {
|
||||||
|
pid: process.pid,
|
||||||
|
hostname: process.env.HOSTNAME,
|
||||||
|
nodeEnv: process.env.NODE_ENV,
|
||||||
|
},
|
||||||
|
repository: {
|
||||||
|
repoUrl: buildRepoUrl(repositoryConfig),
|
||||||
|
},
|
||||||
|
relatedRepositories: relatedRepositoryConfigs.map((config) => ({
|
||||||
|
repoUrl: buildRepoUrl(config),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [repoUrl, config] of configsToInspect) {
|
||||||
|
try {
|
||||||
|
const resticLocks = await inspectResticLocks(config, organizationId, resticDeps);
|
||||||
|
logger.error("[ResticLockFailure] Restic backend lock dump", {
|
||||||
|
operation,
|
||||||
|
repoUrl,
|
||||||
|
resticLocks,
|
||||||
|
});
|
||||||
|
} catch (diagnosticError) {
|
||||||
|
logger.error("[ResticLockFailure] Failed to inspect restic backend locks", {
|
||||||
|
operation,
|
||||||
|
repoUrl,
|
||||||
|
error: toMessage(diagnosticError),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (diagnosticError) {
|
||||||
|
logger.error("[ResticLockFailure] Failed to collect lock diagnostics", {
|
||||||
|
operation,
|
||||||
|
repoUrl: buildRepoUrl(repositoryConfig),
|
||||||
|
error: toMessage(diagnosticError),
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -16,6 +16,7 @@ import { stats } from "./commands/stats";
|
||||||
import { tagSnapshots } from "./commands/tag-snapshots";
|
import { tagSnapshots } from "./commands/tag-snapshots";
|
||||||
import { unlock } from "./commands/unlock";
|
import { unlock } from "./commands/unlock";
|
||||||
import { ResticLockError } from "./error";
|
import { ResticLockError } from "./error";
|
||||||
|
import { logResticLockFailureDiagnostics } from "./lock-diagnostics";
|
||||||
import type { RepositoryConfig } from "./schemas";
|
import type { RepositoryConfig } from "./schemas";
|
||||||
import type { ResticDeps } from "./types";
|
import type { ResticDeps } from "./types";
|
||||||
|
|
||||||
|
|
@ -24,14 +25,17 @@ export { buildEnv } from "./helpers/build-env";
|
||||||
export { buildRepoUrl } from "./helpers/build-repo-url";
|
export { buildRepoUrl } from "./helpers/build-repo-url";
|
||||||
export { cleanupTemporaryKeys } from "./helpers/cleanup-temporary-keys";
|
export { cleanupTemporaryKeys } from "./helpers/cleanup-temporary-keys";
|
||||||
export { validateCustomResticParams } from "./helpers/validate-custom-params";
|
export { validateCustomResticParams } from "./helpers/validate-custom-params";
|
||||||
|
export { isResticLockFailure, logResticLockFailureDiagnostics } from "./lock-diagnostics";
|
||||||
export { isResticError, ResticError, ResticLockError } from "./error";
|
export { isResticError, ResticError, ResticLockError } from "./error";
|
||||||
|
|
||||||
type LockRecoveryContext = {
|
type LockDiagnosticCommandContext = {
|
||||||
repositoryConfigs: RepositoryConfig[];
|
repositoryConfig: RepositoryConfig;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
|
relatedRepositoryConfigs?: RepositoryConfig[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type ResticCommandOptions = { organizationId: string };
|
type ResticCommandOptions = { organizationId: string };
|
||||||
|
type ResticCommandResult = { error?: unknown };
|
||||||
type ResticCommandFailure<Failure> = Failure | ResticLockError;
|
type ResticCommandFailure<Failure> = Failure | ResticLockError;
|
||||||
type RunResticCommand<Success, Failure, Requirements> = () => Effect.Effect<
|
type RunResticCommand<Success, Failure, Requirements> = () => Effect.Effect<
|
||||||
Success,
|
Success,
|
||||||
|
|
@ -39,37 +43,56 @@ type RunResticCommand<Success, Failure, Requirements> = () => Effect.Effect<
|
||||||
Requirements
|
Requirements
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const getLockRecoveryContext = (operation: string, args: unknown[]): LockRecoveryContext => {
|
const getCommandContext = (operation: string, args: unknown[]): LockDiagnosticCommandContext => {
|
||||||
const firstRepositoryConfig = args[0] as RepositoryConfig;
|
const firstRepositoryConfig = args[0] as RepositoryConfig;
|
||||||
const options = args.at(-1) as ResticCommandOptions;
|
const options = args.at(-1) as ResticCommandOptions;
|
||||||
|
|
||||||
if (operation === "restic.copy") {
|
if (operation === "restic.copy") {
|
||||||
return {
|
return {
|
||||||
repositoryConfigs: [args[1] as RepositoryConfig, firstRepositoryConfig],
|
repositoryConfig: args[1] as RepositoryConfig,
|
||||||
organizationId: options.organizationId,
|
organizationId: options.organizationId,
|
||||||
|
relatedRepositoryConfigs: [firstRepositoryConfig],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
repositoryConfigs: [firstRepositoryConfig],
|
repositoryConfig: firstRepositoryConfig,
|
||||||
organizationId: options.organizationId,
|
organizationId: options.organizationId,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const unlockStaleLocks = (context: LockRecoveryContext, deps: ResticDeps) =>
|
const logLockFailure = async (
|
||||||
|
error: unknown,
|
||||||
|
operation: string,
|
||||||
|
context: LockDiagnosticCommandContext,
|
||||||
|
deps: ResticDeps,
|
||||||
|
) =>
|
||||||
|
logResticLockFailureDiagnostics({
|
||||||
|
error,
|
||||||
|
operation,
|
||||||
|
repositoryConfig: context.repositoryConfig,
|
||||||
|
organizationId: context.organizationId,
|
||||||
|
resticDeps: deps,
|
||||||
|
relatedRepositoryConfigs: context.relatedRepositoryConfigs,
|
||||||
|
});
|
||||||
|
|
||||||
|
const unlockStaleLocks = (context: LockDiagnosticCommandContext, deps: ResticDeps): Effect.Effect<void, Error> =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
for (const repositoryConfig of context.repositoryConfigs) {
|
for (const repositoryConfig of [context.repositoryConfig, ...(context.relatedRepositoryConfigs ?? [])]) {
|
||||||
yield* unlock(repositoryConfig, { organizationId: context.organizationId }, deps);
|
yield* unlock(repositoryConfig, { organizationId: context.organizationId }, deps);
|
||||||
}
|
}
|
||||||
}).pipe(Effect.catchAll(() => Effect.void));
|
});
|
||||||
|
|
||||||
const recoverFromLockFailure = <Success, Failure, Requirements>(
|
const recoverFromLockFailure = <Success, Failure, Requirements>(
|
||||||
context: LockRecoveryContext,
|
error: ResticLockError,
|
||||||
runCommand: RunResticCommand<Success, Failure, Requirements>,
|
operation: string,
|
||||||
|
context: LockDiagnosticCommandContext,
|
||||||
deps: ResticDeps,
|
deps: ResticDeps,
|
||||||
|
runCommand: RunResticCommand<Success, Failure, Requirements>,
|
||||||
): Effect.Effect<Success, ResticCommandFailure<Failure> | Error, Requirements> =>
|
): Effect.Effect<Success, ResticCommandFailure<Failure> | Error, Requirements> =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* logger.effect.warn("Restic lock failure detected. Removing stale locks and retrying once.");
|
yield* logger.effect.warn("Restic lock failure detected. Removing stale locks and retrying once.");
|
||||||
|
yield* Effect.promise(() => logLockFailure(error, operation, context, deps));
|
||||||
yield* unlockStaleLocks(context, deps);
|
yield* unlockStaleLocks(context, deps);
|
||||||
|
|
||||||
const retryResult = yield* runCommand();
|
const retryResult = yield* runCommand();
|
||||||
|
|
@ -84,11 +107,20 @@ function withDeps<Args extends unknown[], Success, Failure, Requirements>(
|
||||||
deps: ResticDeps,
|
deps: ResticDeps,
|
||||||
): (...args: Args) => Effect.Effect<Success, ResticCommandFailure<Failure> | Error, Requirements> {
|
): (...args: Args) => Effect.Effect<Success, ResticCommandFailure<Failure> | Error, Requirements> {
|
||||||
return (...args: Args) => {
|
return (...args: Args) => {
|
||||||
const context = getLockRecoveryContext(operation, args);
|
const context = getCommandContext(operation, args);
|
||||||
const runCommand = () => command(...args, deps);
|
const runCommand = () => command(...args, deps);
|
||||||
|
|
||||||
return runCommand().pipe(
|
const commandEffect = runCommand().pipe(
|
||||||
Effect.catchTag("ResticLockError", () => recoverFromLockFailure(context, runCommand, deps)),
|
Effect.catchTag("ResticLockError", (error) =>
|
||||||
|
recoverFromLockFailure(error as ResticLockError, operation, context, deps, runCommand),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return commandEffect.pipe(
|
||||||
|
Effect.tap((result) => {
|
||||||
|
const { error } = result as ResticCommandResult;
|
||||||
|
return error ? Effect.promise(() => logLockFailure(error, operation, context, deps)) : Effect.void;
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import viteReact, { reactCompilerPreset } from "@vitejs/plugin-react";
|
||||||
import babel from "@rolldown/plugin-babel";
|
import babel from "@rolldown/plugin-babel";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
clearScreen: false,
|
|
||||||
plugins: [
|
plugins: [
|
||||||
tanstackStart({
|
tanstackStart({
|
||||||
srcDirectory: "app",
|
srcDirectory: "app",
|
||||||
|
|
@ -31,7 +30,7 @@ export default defineConfig({
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
port: 3000,
|
port: 3000,
|
||||||
allowedHosts: [".ts.net"],
|
allowedHosts: [".ts.net"]
|
||||||
},
|
},
|
||||||
fmt: {
|
fmt: {
|
||||||
printWidth: 120,
|
printWidth: 120,
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,6 @@ export default defineConfig({
|
||||||
"app/client/**/*.test.tsx",
|
"app/client/**/*.test.tsx",
|
||||||
"app/client/**/*.spec.ts",
|
"app/client/**/*.spec.ts",
|
||||||
"app/client/**/*.spec.tsx",
|
"app/client/**/*.spec.tsx",
|
||||||
"app/test/integration/**/*.test.ts",
|
|
||||||
"app/test/integration/**/*.spec.ts",
|
|
||||||
],
|
],
|
||||||
setupFiles: ["./app/test/setup.ts"],
|
setupFiles: ["./app/test/setup.ts"],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue