diff --git a/CLAUDE.md b/CLAUDE.md index 39161e39..ab91695c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,16 +7,15 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co VaultLink is a self-hosted Obsidian file-sync system. Two halves of one repo: - `sync-server/` — Rust (axum + sqlx/SQLite). Source of truth for vault state, broadcasts changes via WebSocket. -- `frontend/` — npm workspaces. The sync engine (`sync-client`) is consumed by an Obsidian plugin, a standalone CLI, a fuzz E2E harness, a scripted determinism harness, and a history UI. +- `frontend/` — npm workspaces. The sync engine (`sync-client`) is consumed by an Obsidian plugin, a standalone CLI, a fuzz E2E harness, and a scripted determinism harness. -The HTTP/WS API types are generated from Rust (`ts-rs`) and mirrored into the TS workspaces. **Never hand-edit files in `frontend/sync-client/src/services/types/` or `frontend/history-ui/src/lib/types/`** — run `scripts/update-api-types.sh` after changing anything Serde-derived in the server. +The HTTP/WS API types are generated from Rust (`ts-rs`) and mirrored into the TS workspaces. **Never hand-edit files in `frontend/sync-client/src/services/types/`** — run `scripts/update-api-types.sh` after changing anything Serde-derived in the server. ### Frontend workspaces - `sync-client` — the sync engine; published to consumers via `dist/`. All other TS workspaces depend on it via `file:../sync-client`. - `obsidian-plugin` — Obsidian plugin built from `sync-client`. - `local-client-cli` — same engine wrapped as a standalone CLI. -- `history-ui` — vault-history web UI. - `test-client` — fuzz E2E harness (random ops across N processes). - `deterministic-tests` — scripted multi-client tests with an in-memory FS, run against a real server. @@ -67,7 +66,7 @@ Frontend dev (sync-client + obsidian-plugin watch in parallel): cd frontend && npm install && npm run dev ``` -Regenerate TS bindings from Rust types (touches `frontend/{sync-client,history-ui}/src/.../types/`): +Regenerate TS bindings from Rust types (touches `frontend/sync-client/src/services/types/`): ```sh scripts/update-api-types.sh diff --git a/frontend/deterministic-tests/README.md b/frontend/deterministic-tests/README.md index 487c7e1c..a420c1c0 100644 --- a/frontend/deterministic-tests/README.md +++ b/frontend/deterministic-tests/README.md @@ -89,18 +89,19 @@ export const myScenarioTest: TestDefinition = { The `verify` callback receives an `AssertableState` object with chainable assertion methods: ```typescript -s.assertFileCount(n); // exact file count -s.assertFileExists("path"); // file must exist -s.assertFileNotExists("path"); // file must not exist -s.assertContent("path", "expected"); // exact content match -s.assertContains("path", "a", "b"); // all substrings present in file -s.assertContainsAny("path", "a", "b"); // at least one substring present -s.assertAnyFileContains("text"); // substring present in some file -s.assertNoFileContains("text"); // substring absent from every file -s.assertSubstringCount("path", "x", 3); // substring appears exactly N times -s.assertContentInAtMostOneFile("text"); // no duplicate content -s.ifFileExists("path", (s) => { /* … */ }); // conditional block -s.getContent("path"); // raw content (or "" if missing) +s.assertFileCount(n); // exact file count +s.assertFileExists("path"); // file must exist +s.assertFileNotExists("path"); // file must not exist +s.assertContent("path", "expected"); // exact content match +s.assertContains("path", "a", "b"); // all substrings present in file +s.assertContainsAny("path", "a", "b"); // at least one substring present +s.assertAnyFileContains("text"); // substring present in some file +s.assertNoFileContains("text"); // substring absent from every file +s.assertContentInAtMostOneFile("text"); // no duplicate content +s.ifFileExists("path", (s) => { + /* … */ +}); // conditional block +s.getContent("path"); // raw content (or "" if missing) ``` 2. Register it in `src/test-registry.ts`: diff --git a/frontend/deterministic-tests/src/cli.ts b/frontend/deterministic-tests/src/cli.ts index 6e15cac0..0beaca03 100644 --- a/frontend/deterministic-tests/src/cli.ts +++ b/frontend/deterministic-tests/src/cli.ts @@ -42,7 +42,7 @@ function testUsesPauseServer(test: TestDefinition): boolean { */ function findProjectRoot(): string { let dir = path.dirname(__filename); - const root = path.parse(dir).root; + const { root } = path.parse(dir); while (dir !== root) { if ( fs.existsSync(path.join(dir, "sync-server")) && diff --git a/frontend/deterministic-tests/src/deterministic-agent.ts b/frontend/deterministic-tests/src/deterministic-agent.ts index 9fb1eaa5..08baef59 100644 --- a/frontend/deterministic-tests/src/deterministic-agent.ts +++ b/frontend/deterministic-tests/src/deterministic-agent.ts @@ -37,15 +37,15 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { private readonly wsFactory = new ManagedWebSocketFactory(); private nextWriteRename: | { - oldPath: RelativePath; - newPath: RelativePath; - } + oldPath: RelativePath; + newPath: RelativePath; + } | undefined; private nextCreateResponseDrop: | { - dropped: Promise; - resolveDropped: () => void; - } + dropped: Promise; + resolveDropped: () => void; + } | undefined; public constructor( @@ -138,13 +138,12 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { this.nextCreateResponseDrop === undefined, `Client ${this.clientId} already has a create response drop armed` ); - let resolveDropped: () => void = () => {}; - const dropped = new Promise((resolve) => { - resolveDropped = resolve; - }); + const resolvers = Promise.withResolvers(); this.nextCreateResponseDrop = { - dropped, - resolveDropped + dropped: resolvers.promise as Promise, + resolveDropped: (): void => { + resolvers.resolve(undefined); + } }; this.log("Armed next create response drop"); } @@ -175,9 +174,7 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { await withTimeout( new Promise((resolve) => { const unsubscribe = this.client.onSyncHistoryUpdated.add(() => { - const entry = this.client - .getHistoryEntries() - .find(matches); + const entry = this.client.getHistoryEntries().find(matches); if (entry === undefined) { return; } @@ -324,11 +321,8 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { }); } - const nextWriteRename = this.nextWriteRename; - if ( - nextWriteRename !== undefined && - nextWriteRename.oldPath === path - ) { + const { nextWriteRename } = this; + if (nextWriteRename?.oldPath === path) { this.nextWriteRename = undefined; await super.rename( nextWriteRename.oldPath, @@ -480,5 +474,4 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { return response; }; } - } diff --git a/frontend/deterministic-tests/src/server-control.ts b/frontend/deterministic-tests/src/server-control.ts index 9cb4cde0..62475779 100644 --- a/frontend/deterministic-tests/src/server-control.ts +++ b/frontend/deterministic-tests/src/server-control.ts @@ -46,7 +46,7 @@ export class ServerControl { // Retry on bind failure: findFreePort closes its probe before we // spawn, so under heavy parallelism another process can grab the // same port. Each attempt picks a fresh port. - let lastError: unknown; + let lastError: unknown = undefined; for (let attempt = 1; attempt <= SERVER_START_MAX_ATTEMPTS; attempt++) { try { await this.startOnce(); @@ -65,69 +65,6 @@ export class ServerControl { ); } - private async startOnce(): Promise { - const reservation = await findFreePort(); - this._port = reservation.port; - const tmpBase = os.tmpdir(); - this.tempDir = fs.mkdtempSync(path.join(tmpBase, "vault-link-test-")); - const tempConfigPath = path.join(this.tempDir, "config.yml"); - const dbDir = path.join(this.tempDir, "databases"); - - this.writeConfigFile(tempConfigPath, dbDir); - - this.logger.info( - `Starting server: ${this.serverPath} (port ${this._port})` - ); - - // Release the port reservation right before spawning to minimize - // the TOCTOU window between port discovery and server binding. - reservation.release(); - - this.process = spawn(this.serverPath, [tempConfigPath], { - stdio: ["ignore", "pipe", "pipe"], - detached: false - }); - - this.process.stdout?.on("data", (data: Buffer) => { - this.logger.info(`[SERVER] ${data.toString().trim()}`); - }); - - this.process.stderr?.on("data", (data: Buffer) => { - this.logger.info(`[SERVER] ${data.toString().trim()}`); - }); - - this.process.on("error", (err) => { - this.logger.error(`[SERVER] Process error: ${err.message}`); - }); - - const currentProcess = this.process; - currentProcess.on("exit", (code, signal) => { - this.logger.info( - `Server exited with code ${code}, signal ${signal}` - ); - // Only clear state if this handler is for the current process. - // A fast stop→start cycle could create a new process before this - // handler fires — clearing state here would corrupt the new one. - if (this.process === currentProcess) { - this.process = null; - this._isPaused = false; - } - }); - - try { - await this.waitForReady(); - } catch (error) { - // Kill the spawned process if it failed to become ready, - // preventing a zombie process from lingering. - try { - await this.stop(); - } catch { - // Best-effort cleanup - } - throw error; - } - } - public async waitForReady( maxAttempts: number = SERVER_READY_MAX_ATTEMPTS ): Promise { @@ -239,8 +176,7 @@ export class ServerControl { public isRunning(): boolean { const proc = this.process; return ( - proc !== null && - proc.pid !== undefined && + proc?.pid !== undefined && proc.exitCode === null && proc.signalCode === null ); @@ -269,6 +205,69 @@ export class ServerControl { } } + private async startOnce(): Promise { + const reservation = await findFreePort(); + this._port = reservation.port; + const tmpBase = os.tmpdir(); + this.tempDir = fs.mkdtempSync(path.join(tmpBase, "vault-link-test-")); + const tempConfigPath = path.join(this.tempDir, "config.yml"); + const dbDir = path.join(this.tempDir, "databases"); + + this.writeConfigFile(tempConfigPath, dbDir); + + this.logger.info( + `Starting server: ${this.serverPath} (port ${this._port})` + ); + + // Release the port reservation right before spawning to minimize + // the TOCTOU window between port discovery and server binding. + reservation.release(); + + this.process = spawn(this.serverPath, [tempConfigPath], { + stdio: ["ignore", "pipe", "pipe"], + detached: false + }); + + this.process.stdout?.on("data", (data: Buffer) => { + this.logger.info(`[SERVER] ${data.toString().trim()}`); + }); + + this.process.stderr?.on("data", (data: Buffer) => { + this.logger.info(`[SERVER] ${data.toString().trim()}`); + }); + + this.process.on("error", (err) => { + this.logger.error(`[SERVER] Process error: ${err.message}`); + }); + + const currentProcess = this.process; + currentProcess.on("exit", (code, signal) => { + this.logger.info( + `Server exited with code ${code}, signal ${signal}` + ); + // Only clear state if this handler is for the current process. + // A fast stop→start cycle could create a new process before this + // handler fires — clearing state here would corrupt the new one. + if (this.process === currentProcess) { + this.process = null; + this._isPaused = false; + } + }); + + try { + await this.waitForReady(); + } catch (error) { + // Kill the spawned process if it failed to become ready, + // preventing a zombie process from lingering. + try { + await this.stop(); + } catch { + // Best-effort cleanup + } + throw error; + } + } + private writeConfigFile(destPath: string, dbDir: string): void { // Assumes config-e2e.yml has exactly one 2-space-indented `port:` and // one `databases_directory_path:` (under `server:` and `database:` diff --git a/frontend/deterministic-tests/src/test-runner.ts b/frontend/deterministic-tests/src/test-runner.ts index 411e9b08..2bb29704 100644 --- a/frontend/deterministic-tests/src/test-runner.ts +++ b/frontend/deterministic-tests/src/test-runner.ts @@ -1,7 +1,7 @@ import type { TestDefinition, TestResult, TestStep } from "./test-definition"; import { DeterministicAgent } from "./deterministic-agent"; import type { ServerControl } from "./server-control"; -import type { SyncSettings, Logger } from "sync-client"; +import { SyncType, type SyncSettings, type Logger } from "sync-client"; import { assert } from "./utils/assert"; import { AssertableState } from "./utils/assertable-state"; import { sleep } from "./utils/sleep"; @@ -188,9 +188,11 @@ export class TestRunner { const agent = this.getAgent(step.client); const historySeen = agent.waitForHistoryEntry( (entry) => - entry.details.type === step.syncType && + entry.details.type === SyncType[step.syncType] && entry.details.relativePath === step.path, - () => this.serverControl.pause() + () => { + this.serverControl.pause(); + } ); this.serverControl.resume(); await historySeen; diff --git a/frontend/deterministic-tests/src/tests/concurrent-rename-and-create-at-target-create-first.test.ts b/frontend/deterministic-tests/src/tests/concurrent-rename-and-create-at-target-create-first.test.ts index cd8046ce..719cde4d 100644 --- a/frontend/deterministic-tests/src/tests/concurrent-rename-and-create-at-target-create-first.test.ts +++ b/frontend/deterministic-tests/src/tests/concurrent-rename-and-create-at-target-create-first.test.ts @@ -1,49 +1,50 @@ import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; -export const concurrentRenameAndCreateAtTargetCreateFirstTest: TestDefinition = { - description: - "One client renames X to Y while another creates a new file at Y, " + - "both offline. After syncing, Y should contain merged content from " + - "both the renamed file and the newly created file.", - clients: 2, - steps: [ - { - type: "create", - client: 0, - path: "X.md", - content: "original file X" - }, - { type: "enable-sync", client: 0 }, - { type: "enable-sync", client: 1 }, - { type: "barrier" }, +export const concurrentRenameAndCreateAtTargetCreateFirstTest: TestDefinition = + { + description: + "One client renames X to Y while another creates a new file at Y, " + + "both offline. After syncing, Y should contain merged content from " + + "both the renamed file and the newly created file.", + clients: 2, + steps: [ + { + type: "create", + client: 0, + path: "X.md", + content: "original file X" + }, + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, - { type: "disable-sync", client: 0 }, - { type: "disable-sync", client: 1 }, + { type: "disable-sync", client: 0 }, + { type: "disable-sync", client: 1 }, - { type: "rename", client: 0, oldPath: "X.md", newPath: "Y.md" }, + { type: "rename", client: 0, oldPath: "X.md", newPath: "Y.md" }, - { - type: "create", - client: 1, - path: "Y.md", - content: "brand new Y content" - }, + { + type: "create", + client: 1, + path: "Y.md", + content: "brand new Y content" + }, - { type: "enable-sync", client: 1 }, - { type: "sync", client: 1 }, + { type: "enable-sync", client: 1 }, + { type: "sync", client: 1 }, - { type: "enable-sync", client: 0 }, - { type: "barrier" }, + { type: "enable-sync", client: 0 }, + { type: "barrier" }, - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state - .assertFileCount(2) - .assertContains("Y (1).md", "original file X") - .assertContains("Y.md", "brand new Y content"); + { + type: "assert-consistent", + verify: (state: AssertableState): void => { + state + .assertFileCount(2) + .assertContains("Y (1).md", "original file X") + .assertContains("Y.md", "brand new Y content"); + } } - } - ] -}; + ] + }; diff --git a/frontend/deterministic-tests/src/tests/concurrent-rename-and-create-at-target-rename-first.test.ts b/frontend/deterministic-tests/src/tests/concurrent-rename-and-create-at-target-rename-first.test.ts index 0ac0b721..a1ba9c2c 100644 --- a/frontend/deterministic-tests/src/tests/concurrent-rename-and-create-at-target-rename-first.test.ts +++ b/frontend/deterministic-tests/src/tests/concurrent-rename-and-create-at-target-rename-first.test.ts @@ -1,52 +1,53 @@ import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; -export const concurrentRenameAndCreateAtTargetRenameFirstTest: TestDefinition = { - description: - "One client renames X to Y while another creates a new file at Y, " + - "both offline. We can't merge the create because it would result in a cycle", - clients: 2, - steps: [ - { - type: "create", - client: 0, - path: "X.md", - content: "original file X" - }, - { type: "enable-sync", client: 0 }, - { type: "enable-sync", client: 1 }, - { type: "barrier" }, +export const concurrentRenameAndCreateAtTargetRenameFirstTest: TestDefinition = + { + description: + "One client renames X to Y while another creates a new file at Y, " + + "both offline. We can't merge the create because it would result in a cycle", + clients: 2, + steps: [ + { + type: "create", + client: 0, + path: "X.md", + content: "original file X" + }, + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, - { type: "disable-sync", client: 0 }, - { type: "disable-sync", client: 1 }, + { type: "disable-sync", client: 0 }, + { type: "disable-sync", client: 1 }, - { type: "rename", client: 0, oldPath: "X.md", newPath: "Y.md" }, + { type: "rename", client: 0, oldPath: "X.md", newPath: "Y.md" }, - { - type: "create", - client: 1, - path: "Y.md", - content: "brand new Y content" - }, + { + type: "create", + client: 1, + path: "Y.md", + content: "brand new Y content" + }, - { type: "enable-sync", client: 0 }, - { type: "sync", client: 0 }, + { type: "enable-sync", client: 0 }, + { type: "sync", client: 0 }, - { type: "enable-sync", client: 1 }, - { type: "barrier" }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, - { - type: "assert-consistent", - verify: (state: AssertableState): void => { - state - .assertFileNotExists("X.md") - .assertFileExists("Y.md") - .assertFileExists("Y (1).md") - .assertAnyFileContains( - "original file X", - "brand new Y content" - ); + { + type: "assert-consistent", + verify: (state: AssertableState): void => { + state + .assertFileNotExists("X.md") + .assertFileExists("Y.md") + .assertFileExists("Y (1).md") + .assertAnyFileContains( + "original file X", + "brand new Y content" + ); + } } - } - ] -}; + ] + }; diff --git a/frontend/deterministic-tests/src/utils/assertable-state.ts b/frontend/deterministic-tests/src/utils/assertable-state.ts index 7c6f192c..67a300af 100644 --- a/frontend/deterministic-tests/src/utils/assertable-state.ts +++ b/frontend/deterministic-tests/src/utils/assertable-state.ts @@ -106,22 +106,6 @@ export class AssertableState { return this; } - public assertSubstringCount( - path: string, - substring: string, - expected: number - ): this { - this.assertFileExists(path); - const content = this.files.get(path) ?? ""; - const actual = content.split(substring).length - 1; - if (actual !== expected) { - throw new Error( - `Expected "${substring}" to appear ${expected} time(s) in "${path}", found ${actual}. Content: "${content}"` - ); - } - return this; - } - public assertContentInAtMostOneFile(substring: string): this { const matches = Array.from(this.files.entries()).filter(([, content]) => content.includes(substring) @@ -143,8 +127,4 @@ export class AssertableState { } return this; } - - public getContent(path: string): string { - return this.files.get(path) ?? ""; - } } diff --git a/frontend/local-client-cli/src/args.test.ts b/frontend/local-client-cli/src/args.test.ts index fdf0b6c8..075f9446 100644 --- a/frontend/local-client-cli/src/args.test.ts +++ b/frontend/local-client-cli/src/args.test.ts @@ -169,7 +169,6 @@ test("parseArgs - parse ERROR log level", () => { assert.equal(args.logLevel, LogLevel.ERROR); }); - test("parseArgs - reads required options from environment variables", () => { process.env.VAULTLINK_LOCAL_PATH = "/env/path"; process.env.VAULTLINK_REMOTE_URI = "https://env.example.com"; diff --git a/frontend/local-client-cli/src/cli.ts b/frontend/local-client-cli/src/cli.ts index 31c81d5c..39c3eb38 100644 --- a/frontend/local-client-cli/src/cli.ts +++ b/frontend/local-client-cli/src/cli.ts @@ -1,11 +1,10 @@ import * as path from "path"; import * as fs from "fs/promises"; import * as fsSync from "fs"; -import type { NetworkConnectionStatus } from "sync-client"; +import type { NetworkConnectionStatus, Logger } from "sync-client"; import { SyncClient, DEFAULT_SETTINGS, - Logger, LogLevel, LogLine, type SyncSettings, diff --git a/frontend/local-client-cli/src/node-filesystem.ts b/frontend/local-client-cli/src/node-filesystem.ts index ba95ab6a..794072bd 100644 --- a/frontend/local-client-cli/src/node-filesystem.ts +++ b/frontend/local-client-cli/src/node-filesystem.ts @@ -15,7 +15,7 @@ import { toUnixPath } from "./path-utils"; export const VAULTLINK_DIR = ".vaultlink"; export class NodeFileSystemOperations implements FileSystemOperations { - public constructor(private readonly basePath: string) { } + public constructor(private readonly basePath: string) {} public async listFilesRecursively( directory: RelativePath | undefined diff --git a/frontend/obsidian-plugin/src/obsidian-file-system.ts b/frontend/obsidian-plugin/src/obsidian-file-system.ts index ceb8bc2a..f1a43518 100644 --- a/frontend/obsidian-plugin/src/obsidian-file-system.ts +++ b/frontend/obsidian-plugin/src/obsidian-file-system.ts @@ -139,10 +139,6 @@ export class ObsidianFileSystemOperations implements FileSystemOperations { return (await this.statFile(path)).size; } - public async getModificationTime(path: RelativePath): Promise { - return new Date((await this.statFile(path)).mtime); - } - public async exists(path: RelativePath): Promise { return this.vault.adapter.exists(normalizePath(path)); } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d1057be3..b0c7d1e0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,8 +10,7 @@ "obsidian-plugin", "test-client", "deterministic-tests", - "local-client-cli", - "history-ui" + "local-client-cli" ], "devDependencies": { "concurrently": "^9.2.1", @@ -40,6 +39,7 @@ }, "history-ui": { "version": "0.14.0", + "extraneous": true, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.0", "svelte": "^5.0.0", @@ -83,278 +83,6 @@ "node": ">=14.17.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/linux-x64": { "version": "0.27.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", @@ -371,159 +99,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -746,17 +321,6 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "dev": true, @@ -873,395 +437,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@sentry-internal/browser-utils": { "version": "10.30.0", "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.30.0.tgz", @@ -1337,56 +512,6 @@ "node": ">=18" } }, - "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", - "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^8.9.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz", - "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", - "debug": "^4.4.1", - "deepmerge": "^4.3.1", - "kleur": "^4.1.5", - "magic-string": "^0.30.17", - "vitefu": "^1.0.6" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" - }, - "peerDependencies": { - "svelte": "^5.0.0", - "vite": "^6.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", - "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.7" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" - }, - "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^5.0.0", - "svelte": "^5.0.0", - "vite": "^6.0.0" - } - }, "node_modules/@types/codemirror": { "version": "5.60.8", "dev": true, @@ -1441,13 +566,6 @@ "@types/estree": "*" } }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true, - "license": "MIT" - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.49.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz", @@ -2005,26 +1123,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/aria-query": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", - "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "dev": true, @@ -2242,16 +1340,6 @@ "node": ">=6" } }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/color-convert": { "version": "2.0.1", "dev": true, @@ -2413,16 +1501,6 @@ "dev": true, "license": "MIT" }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/detect-libc": { "version": "1.0.3", "dev": true, @@ -2446,13 +1524,6 @@ "dev": true, "license": "MIT" }, - "node_modules/devalue": { - "version": "5.6.4", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", - "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", - "dev": true, - "license": "MIT" - }, "node_modules/dunder-proto": { "version": "1.0.1", "dev": true, @@ -3130,13 +2201,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/esm-env": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", - "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "dev": true, - "license": "MIT" - }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -3166,17 +2230,6 @@ "node": ">=0.10" } }, - "node_modules/esrap": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz", - "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15", - "@typescript-eslint/types": "^8.2.0" - } - }, "node_modules/esrecurse": { "version": "4.3.0", "dev": true, @@ -3503,10 +2556,6 @@ "node": ">= 0.4" } }, - "node_modules/history-ui": { - "resolved": "history-ui", - "link": true - }, "node_modules/icss-utils": { "version": "5.1.0", "dev": true, @@ -3640,16 +2689,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-reference": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", - "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.6" - } - }, "node_modules/isexe": { "version": "2.0.0", "dev": true, @@ -3734,16 +2773,6 @@ "node": ">=0.10.0" } }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/levn": { "version": "0.4.1", "dev": true, @@ -3786,13 +2815,6 @@ "resolved": "local-client-cli", "link": true }, - "node_modules/locate-character": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true, - "license": "MIT" - }, "node_modules/locate-path": { "version": "6.0.0", "dev": true, @@ -3812,16 +2834,6 @@ "dev": true, "license": "MIT" }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "dev": true, @@ -4503,51 +3515,6 @@ "node": ">=12" } }, - "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" - } - }, "node_modules/rxjs": { "version": "7.8.2", "dev": true, @@ -4874,34 +3841,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/svelte": { - "version": "5.53.12", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.12.tgz", - "integrity": "sha512-4x/uk4rQe/d7RhfvS8wemTfNjQ0bJbKvamIzRBfTe2eHHjzBZ7PZicUQrC2ryj83xxEacfA1zHKd1ephD1tAxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "@jridgewell/sourcemap-codec": "^1.5.0", - "@sveltejs/acorn-typescript": "^1.0.5", - "@types/estree": "^1.0.5", - "@types/trusted-types": "^2.0.7", - "acorn": "^8.12.1", - "aria-query": "5.3.1", - "axobject-query": "^4.1.0", - "clsx": "^2.1.1", - "devalue": "^5.6.4", - "esm-env": "^1.2.1", - "esrap": "^2.2.2", - "is-reference": "^3.0.3", - "locate-character": "^3.0.0", - "magic-string": "^0.30.11", - "zimmerframe": "^1.1.2" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/sync-client": { "resolved": "sync-client", "link": true @@ -5330,191 +4269,6 @@ "resolved": "obsidian-plugin", "link": true }, - "node_modules/vite": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vitefu": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz", - "integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==", - "dev": true, - "license": "MIT", - "workspaces": [ - "tests/deps/*", - "tests/projects/*", - "tests/projects/workspace/packages/*" - ], - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", @@ -5833,13 +4587,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zimmerframe": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", - "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", - "dev": true, - "license": "MIT" - }, "obsidian-plugin": { "name": "vault-link-obsidian-plugin", "version": "0.14.0", diff --git a/frontend/sync-client/src/services/sync-service.ts b/frontend/sync-client/src/services/sync-service.ts index 0a99fe84..56d28d3d 100644 --- a/frontend/sync-client/src/services/sync-service.ts +++ b/frontend/sync-client/src/services/sync-service.ts @@ -13,8 +13,6 @@ import { HttpClientError } from "../errors/http-client-error"; import type { SerializedError } from "./types/SerializedError"; import type { DocumentVersionWithoutContent } from "./types/DocumentVersionWithoutContent"; import type { DocumentUpdateResponse } from "./types/DocumentUpdateResponse"; -import type { DocumentVersion } from "./types/DocumentVersion"; -import type { FetchLatestDocumentsResponse } from "./types/FetchLatestDocumentsResponse"; import type { PingResponse } from "./types/PingResponse"; import type { UpdateTextDocumentVersion } from "./types/UpdateTextDocumentVersion"; import { buildVaultUrl } from "./build-vault-url"; @@ -272,32 +270,6 @@ export class SyncService { }); } - public async get({ - documentId - }: { - documentId: DocumentId; - }): Promise { - return this.retryForever(async () => { - this.logger.debug(`Getting document with id ${documentId}`); - - const response = await this.client( - this.getUrl(`/documents/${documentId}`), - { - headers: this.getDefaultHeaders() - } - ); - - await SyncService.throwIfNotOk(response, "get document"); - - const result: DocumentVersion = - (await response.json()) as DocumentVersion; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion - - this.logger.debug(`Got document ${JSON.stringify(result)}`); - - return result; - }); - } - public async getDocumentVersionContent({ documentId, vaultUpdateId @@ -332,36 +304,6 @@ export class SyncService { }); } - public async getAll( - since?: VaultUpdateId - ): Promise { - return this.retryForever(async () => { - this.logger.debug( - "Getting all documents" + - (since != null ? ` since ${since}` : "") - ); - - const url = new URL(this.getUrl("/documents")); - if (since !== undefined) { - url.searchParams.append("since_update_id", since.toString()); - } - const response = await this.client(url.toString(), { - headers: this.getDefaultHeaders() - }); - - await SyncService.throwIfNotOk(response, "get documents"); - - const result: FetchLatestDocumentsResponse = - (await response.json()) as FetchLatestDocumentsResponse; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion - - this.logger.debug( - `Got ${result.latestDocuments.length} document metadata` - ); - - return result; - }); - } - public async ping(): Promise { this.logger.debug("Pinging server"); const response = await this.pingClient(this.getUrl("/ping"), { diff --git a/frontend/sync-client/src/services/types/FetchLatestDocumentsResponse.ts b/frontend/sync-client/src/services/types/FetchLatestDocumentsResponse.ts deleted file mode 100644 index 315d701a..00000000 --- a/frontend/sync-client/src/services/types/FetchLatestDocumentsResponse.ts +++ /dev/null @@ -1,13 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutContent"; - -/** - * Response to a fetch latest documents request. - */ -export interface FetchLatestDocumentsResponse { - latestDocuments: DocumentVersionWithoutContent[]; - /** - * The update ID of the latest document in the response. - */ - lastUpdateId: bigint; -} diff --git a/frontend/sync-client/src/sync-client.ts b/frontend/sync-client/src/sync-client.ts index dd537296..3a47152e 100644 --- a/frontend/sync-client/src/sync-client.ts +++ b/frontend/sync-client/src/sync-client.ts @@ -56,13 +56,7 @@ export class SyncClient { private readonly contentCache: FixedSizeDocumentCache, private readonly serverConfig: ServerConfig, private readonly syncService: SyncService, - private readonly expectedFsEvents: ExpectedFsEvents, - private readonly persistence: PersistenceProvider< - Partial<{ - settings: Partial; - database: Partial; - }> - > + private readonly expectedFsEvents: ExpectedFsEvents ) {} public get syncedDocumentCount(): number { @@ -172,7 +166,7 @@ export class SyncClient { // new deviceId, the server-side query would miss, and the // pending-but-lost create would deconflict instead of // binding to the doc its content was already absorbed into. - let deviceId = state.deviceId; + let { deviceId } = state; if (deviceId === undefined) { deviceId = createClientId(); state = { ...state, deviceId }; @@ -269,8 +263,7 @@ export class SyncClient { contentCache, serverConfig, syncService, - expectedFsEvents, - persistence + expectedFsEvents ); logger.info("SyncClient created successfully"); @@ -322,26 +315,6 @@ export class SyncClient { } } - /** - * Reload settings from disk overriding current in-memory settings. - * Missing values will be filled in from DEFAULT_SETTINGS rather than - * retaining current in-memory settings. - */ - public async reloadSettings(): Promise { - this.checkIfDestroyed("reloadSettings"); - - const state = (await this.persistence.load()) ?? { - settings: undefined - }; - - const settings = { - ...DEFAULT_SETTINGS, - ...(state.settings ?? {}) - }; - - await this.setSettings(settings); - } - public async checkConnection(): Promise { this.checkIfDestroyed("checkConnection"); diff --git a/frontend/sync-client/src/sync-operations/offline-change-detector.test.ts b/frontend/sync-client/src/sync-operations/offline-change-detector.test.ts index cc710e6a..3a7007ef 100644 --- a/frontend/sync-client/src/sync-operations/offline-change-detector.test.ts +++ b/frontend/sync-client/src/sync-operations/offline-change-detector.test.ts @@ -2,7 +2,10 @@ import { describe, it } from "node:test"; import assert from "node:assert"; import { Logger } from "../tracing/logger"; import { Settings } from "../persistence/settings"; -import { STORED_STATE_SCHEMA_VERSION, SyncEventQueue } from "./sync-event-queue"; +import { + STORED_STATE_SCHEMA_VERSION, + SyncEventQueue +} from "./sync-event-queue"; import { scheduleOfflineChanges } from "./offline-change-detector"; import type { FileOperations } from "../file-operations/file-operations"; import type { RelativePath } from "./types"; @@ -22,19 +25,20 @@ const makeQueue = async (): Promise => { ); }; -const makeOperations = ( - files: Record -): FileOperations => { - return { - listFilesRecursively: async () => Object.keys(files), +const makeOperations = (files: Record): FileOperations => { + const map = new Map(Object.entries(files)); + const partial: Partial = { + listFilesRecursively: async () => [...map.keys()], read: async (path: RelativePath) => { - const data = files[path]; + const data = map.get(path); if (data === undefined) { throw new Error(`File not found: ${path}`); } return data; } - } as unknown as FileOperations; + }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + return partial as FileOperations; }; describe("scheduleOfflineChanges", () => { @@ -70,7 +74,8 @@ describe("scheduleOfflineChanges", () => { operations, queue, (path) => enqueued.push({ kind: "create", path }), - (args) => enqueued.push({ kind: "update", path: args.relativePath }), + (args) => + enqueued.push({ kind: "update", path: args.relativePath }), (path) => enqueued.push({ kind: "delete", path }) ); @@ -109,13 +114,12 @@ describe("scheduleOfflineChanges", () => { operations, queue, (path) => enqueued.push({ kind: "create", path }), - (args) => enqueued.push({ kind: "update", path: args.relativePath }), + (args) => + enqueued.push({ kind: "update", path: args.relativePath }), (path) => enqueued.push({ kind: "delete", path }) ); - assert.deepStrictEqual(enqueued, [ - { kind: "update", path: "doc.md" } - ]); + assert.deepStrictEqual(enqueued, [{ kind: "update", path: "doc.md" }]); }); it("schedules a delete for a settled record whose local file is missing", async () => { @@ -136,13 +140,12 @@ describe("scheduleOfflineChanges", () => { operations, queue, (path) => enqueued.push({ kind: "create", path }), - (args) => enqueued.push({ kind: "update", path: args.relativePath }), + (args) => + enqueued.push({ kind: "update", path: args.relativePath }), (path) => enqueued.push({ kind: "delete", path }) ); - assert.deepStrictEqual(enqueued, [ - { kind: "delete", path: "gone.md" } - ]); + assert.deepStrictEqual(enqueued, [{ kind: "delete", path: "gone.md" }]); }); it("detects an offline rename when an untracked file matches a deleted record's content hash", async () => { diff --git a/frontend/sync-client/src/sync-operations/offline-change-detector.ts b/frontend/sync-client/src/sync-operations/offline-change-detector.ts index 5b91e782..320ec92a 100644 --- a/frontend/sync-client/src/sync-operations/offline-change-detector.ts +++ b/frontend/sync-client/src/sync-operations/offline-change-detector.ts @@ -7,6 +7,24 @@ import type { SyncEventQueue } from "./sync-event-queue"; import { removeFromArray } from "../utils/remove-from-array"; import { FileNotFoundError } from "../errors/file-not-found-error"; +async function readOrUndefined( + operations: FileOperations, + path: RelativePath, + logger: Logger +): Promise { + try { + return await operations.read(path); + } catch (e) { + if (e instanceof FileNotFoundError) { + logger.debug( + `File ${path} disappeared before offline-scan could read it; skipping` + ); + return undefined; + } + throw e; + } +} + /** * Scans the local filesystem and the document database to determine * which files were created, updated, moved, or deleted while the @@ -85,18 +103,10 @@ export async function scheduleOfflineChanges( // the whole scan; nothing to sync for a file that's already gone. const disappearedPaths = new Set(); for (const path of locallyPossibleCreatedFiles) { - let content: Uint8Array; - try { - content = await operations.read(path); - } catch (e) { - if (e instanceof FileNotFoundError) { - logger.debug( - `File ${path} disappeared before offline-scan could read it; skipping` - ); - disappearedPaths.add(path); - continue; - } - throw e; + const content = await readOrUndefined(operations, path, logger); + if (content === undefined) { + disappearedPaths.add(path); + continue; } const contentHash = await hash(content); @@ -148,8 +158,7 @@ export async function scheduleOfflineChanges( for (const path of syncedLocalFiles) { const record = allDocuments.get(path); if ( - record !== undefined && - record.localPath !== undefined && + record?.localPath !== undefined && record.localPath !== record.remoteRelativePath && !allLocalFiles.has(record.remoteRelativePath) && queue.byLocalPath.get(record.remoteRelativePath) === undefined diff --git a/frontend/sync-client/src/sync-operations/reconciler.test.ts b/frontend/sync-client/src/sync-operations/reconciler.test.ts index 13a08363..9533b8e7 100644 --- a/frontend/sync-client/src/sync-operations/reconciler.test.ts +++ b/frontend/sync-client/src/sync-operations/reconciler.test.ts @@ -2,7 +2,10 @@ import { describe, it } from "node:test"; import assert from "node:assert"; import { Logger, LogLevel } from "../tracing/logger"; import { Settings } from "../persistence/settings"; -import { STORED_STATE_SCHEMA_VERSION, SyncEventQueue } from "./sync-event-queue"; +import { + STORED_STATE_SCHEMA_VERSION, + SyncEventQueue +} from "./sync-event-queue"; import { Reconciler } from "./reconciler"; import { SyncResetError } from "../errors/sync-reset-error"; import type { FileOperations } from "../file-operations/file-operations"; @@ -32,18 +35,22 @@ describe("Reconciler", () => { localPath: undefined }); - const operations = { + const operationsPartial: Partial = { exists: async () => false, create: async () => { assert.fail("reset-interrupted placement should not write"); } - } as unknown as FileOperations; + }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + const operations = operationsPartial as FileOperations; - const syncService = { + const syncServicePartial: Partial = { getDocumentVersionContent: async () => { throw new SyncResetError(); } - } as unknown as SyncService; + }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + const syncService = syncServicePartial as SyncService; const reconciler = new Reconciler( logger, diff --git a/frontend/sync-client/src/sync-operations/sync-event-queue.test.ts b/frontend/sync-client/src/sync-operations/sync-event-queue.test.ts index d2676011..aef7c5f7 100644 --- a/frontend/sync-client/src/sync-operations/sync-event-queue.test.ts +++ b/frontend/sync-client/src/sync-operations/sync-event-queue.test.ts @@ -307,7 +307,10 @@ describe("SyncEventQueue", () => { queue.byLocalPath.get("renamed.md" as RelativePath), undefined ); - assert.strictEqual(queue.getDocumentByDocumentId("A")?.localPath, "a.md"); + assert.strictEqual( + queue.getDocumentByDocumentId("A")?.localPath, + "a.md" + ); // setLocalPath does re-key — it's the explicit path-mutation API. await queue.setLocalPath("A", "later.md" as RelativePath); diff --git a/frontend/sync-client/src/sync-operations/sync-event-queue.ts b/frontend/sync-client/src/sync-operations/sync-event-queue.ts index 75f675d0..9cc986d9 100644 --- a/frontend/sync-client/src/sync-operations/sync-event-queue.ts +++ b/frontend/sync-client/src/sync-operations/sync-event-queue.ts @@ -220,9 +220,7 @@ export class SyncEventQueue { * path) still fires when neither side holds a record for the * collision target. */ - public lastSeenUpdateIdForCreate( - requestPath: RelativePath - ): VaultUpdateId { + public lastSeenUpdateIdForCreate(requestPath: RelativePath): VaultUpdateId { let watermark = this._lastSeenUpdateId.min; for (const record of this.byDocId.values()) { if ( @@ -324,7 +322,7 @@ export class SyncEventQueue { !pendingCreate.isProcessing ) { this.cancelPendingCreate(pendingCreate); - if (recordIsDeleting && record !== undefined) { + if (recordIsDeleting) { // A stale deleting record was still claiming this path. // The not-yet-started create/delete pair collapsed to // nothing, and the disk file is gone, so clear the stale @@ -343,11 +341,11 @@ export class SyncEventQueue { path: lookupPath }); this.notifyPendingUpdateCountChanged(); - if (recordOwnsLookupPath && record !== undefined) { + if (recordOwnsLookupPath) { // The file is gone from disk; clear the doc's localPath so the // Reconciler doesn't try to operate on a vacated slot. await this.setLocalPath(record.documentId, undefined); - } else if (recordIsDeleting && record !== undefined) { + } else if (recordIsDeleting) { // A stale deleting record was still claiming this path while a // newer pending create owned the actual disk file. Drop the // stale claim now that the file is gone. @@ -648,14 +646,6 @@ export class SyncEventQueue { return this.byDocId.get(target); } - public getDocumentByDocumentIdOrFail(target: DocumentId): DocumentRecord { - const result = this.getDocumentByDocumentId(target); - if (!result) { - throw new Error(`No document found with id ${target}`); - } - return result; - } - public getRecordByLocalPath( path: RelativePath ): DocumentRecord | undefined { @@ -814,6 +804,7 @@ export class SyncEventQueue { event.path === path && event.documentId !== promise ) { + // eslint-disable-next-line no-restricted-syntax -- splice-by-index here is a reorder, not an item removal this.events.splice(i, 1); this.events.splice(createIndex, 0, event); createIndex++; @@ -866,6 +857,7 @@ export class SyncEventQueue { typeof event.documentId === "string" && blockingDocIds.has(event.documentId) ) { + // eslint-disable-next-line no-restricted-syntax -- splice-by-index here is a reorder, not an item removal this.events.splice(i, 1); this.events.splice(createIndex, 0, event); createIndex++; @@ -907,8 +899,8 @@ export class SyncEventQueue { this._byLocalPath.delete(previousLocalPath); } record.localPath = newLocalPath; - let displacedRecord: DocumentRecord | undefined; - let displacedOldPath: RelativePath | undefined; + let displacedRecord: DocumentRecord | undefined = undefined; + let displacedOldPath: RelativePath | undefined = undefined; if (newLocalPath !== undefined) { const displaced = this._byLocalPath.get(newLocalPath); if (displaced !== undefined && displaced !== record) { diff --git a/frontend/sync-client/src/tracing/logger.ts b/frontend/sync-client/src/tracing/logger.ts index 6d544fbc..1801a40f 100644 --- a/frontend/sync-client/src/tracing/logger.ts +++ b/frontend/sync-client/src/tracing/logger.ts @@ -54,11 +54,6 @@ export class Logger { ); } - public reset(): void { - this.messages.length = 0; - this.debug("Logger has been reset"); - } - private pushMessage(message: string, level: LogLevel): void { const logLine = new LogLine(level, message); this.messages.push(logLine); diff --git a/frontend/sync-client/src/utils/data-structures/locks.ts b/frontend/sync-client/src/utils/data-structures/locks.ts index 99c33075..452fa874 100644 --- a/frontend/sync-client/src/utils/data-structures/locks.ts +++ b/frontend/sync-client/src/utils/data-structures/locks.ts @@ -92,10 +92,6 @@ export class Locks { this.waiters.clear(); } - public isLocked(key: T): boolean { - return this.locked.has(key); - } - /** * Attempts to acquire a lock immediately without waiting. * Must call `unlock()` if successful. diff --git a/frontend/test-client/src/agent/mock-agent.ts b/frontend/test-client/src/agent/mock-agent.ts index d4fc8c82..53dd59f1 100644 --- a/frontend/test-client/src/agent/mock-agent.ts +++ b/frontend/test-client/src/agent/mock-agent.ts @@ -58,16 +58,18 @@ export class MockAgent extends MockClient { // (e.g. `initial-1.md → initial-1 (2).md` after a same-path // collision) lands at a path the touch-list never knew about, // and an offline rename against that path strands the file. - this.client.onDocumentPathChanged.add((_documentId, oldPath, newPath) => { - if (oldPath !== undefined && newPath !== undefined) { - if (this.doNotTouchWhileOffline.includes(oldPath)) { - this.doNotTouchWhileOffline.push(newPath); - } - if (this.doNotRenameWhileOffline.includes(oldPath)) { - this.doNotRenameWhileOffline.push(newPath); + this.client.onDocumentPathChanged.add( + (_documentId, oldPath, newPath) => { + if (oldPath !== undefined && newPath !== undefined) { + if (this.doNotTouchWhileOffline.includes(oldPath)) { + this.doNotTouchWhileOffline.push(newPath); + } + if (this.doNotRenameWhileOffline.includes(oldPath)) { + this.doNotRenameWhileOffline.push(newPath); + } } } - }); + ); this.client.logger.onLogEmitted.add((logLine: LogLine) => { const state = this.client.getSettings().isSyncEnabled diff --git a/sync-server/clippy.toml b/sync-server/clippy.toml index 2b275dbd..81c6e562 100644 --- a/sync-server/clippy.toml +++ b/sync-server/clippy.toml @@ -1,4 +1,3 @@ disallowed-macros = [ { path = "std::eprintln", reason = "use log::info! or log::warn! instead" }, - { path = "std::println", reason = "use log::info! or log::warn! instead" }, ] diff --git a/sync-server/src/app_state/cursors.rs b/sync-server/src/app_state/cursors.rs index e17fb4f7..b729131f 100644 --- a/sync-server/src/app_state/cursors.rs +++ b/sync-server/src/app_state/cursors.rs @@ -118,7 +118,7 @@ impl Cursors { }; self.broadcasts.send_document_update( - vault_id.clone(), + vault_id, WebSocketServerMessageWithOrigin::new(WebSocketServerMessage::CursorPositions( CursorPositionFromServer { clients: client_cursors, diff --git a/sync-server/src/app_state/database.rs b/sync-server/src/app_state/database.rs index 1fa6d223..e774824b 100644 --- a/sync-server/src/app_state/database.rs +++ b/sync-server/src/app_state/database.rs @@ -34,6 +34,10 @@ use super::websocket::{ use crate::config::database_config::DatabaseConfig; use crate::consts::IDLE_POOL_TIMEOUT; +fn duration_millis_u64(duration: Duration) -> u64 { + u64::try_from(duration.as_millis()).unwrap_or(u64::MAX) +} + /// Holds separate reader and writer pools for a single vault. /// The writer pool has exactly 1 connection so writes never compete /// with reads for pool slots. @@ -182,7 +186,7 @@ fn rollback_before_acquire( impl Database { fn now_ms(&self) -> u64 { - self.epoch.elapsed().as_millis() as u64 + duration_millis_u64(self.epoch.elapsed()) } pub async fn try_new( @@ -817,8 +821,7 @@ impl Database { } else { WebSocketServerMessageWithOrigin::with_origin(version.device_id.clone(), envelope) }; - self.broadcasts - .send_document_update(vault_id.clone(), with_origin); + self.broadcasts.send_document_update(vault_id, with_origin); Ok(()) } @@ -831,7 +834,7 @@ impl Database { let idle_pools: Vec<(VaultId, Arc)> = { let mut pools = self.connection_pools.lock().await; let now_ms = self.now_ms(); - let idle_threshold_ms = IDLE_POOL_TIMEOUT.as_millis() as u64; + let idle_threshold_ms = duration_millis_u64(IDLE_POOL_TIMEOUT); let vaults_to_remove: Vec = pools .iter() diff --git a/sync-server/src/app_state/database/models.rs b/sync-server/src/app_state/database/models.rs index 976cc7e5..cf8f379c 100644 --- a/sync-server/src/app_state/database/models.rs +++ b/sync-server/src/app_state/database/models.rs @@ -83,7 +83,6 @@ pub struct DocumentVersion { pub device_id: DeviceId, } - impl From for DocumentVersion { fn from(value: StoredDocumentVersion) -> Self { Self { diff --git a/sync-server/src/app_state/websocket/utils.rs b/sync-server/src/app_state/websocket/utils.rs index d78360de..a1e824b7 100644 --- a/sync-server/src/app_state/websocket/utils.rs +++ b/sync-server/src/app_state/websocket/utils.rs @@ -10,7 +10,7 @@ use crate::{ }, config::user_config::User, errors::{SyncServerError, client_error, server_error, unauthenticated_error}, - server::auth::auth, + server::auth::authenticate_for_vault, }; pub struct AuthenticatedWebSocketHandshake { @@ -30,7 +30,7 @@ pub fn get_authenticated_handshake( match message { WebSocketClientMessage::Handshake(handshake) => { - let user = auth(state, handshake.token.trim(), vault_id)?; + let user = authenticate_for_vault(state, handshake.token.trim(), vault_id)?; Ok(AuthenticatedWebSocketHandshake { handshake, user }) } WebSocketClientMessage::CursorPositions(_) => Err(unauthenticated_error( diff --git a/sync-server/src/errors.rs b/sync-server/src/errors.rs index 892db36f..ef0d017d 100644 --- a/sync-server/src/errors.rs +++ b/sync-server/src/errors.rs @@ -79,10 +79,7 @@ impl IntoResponse for SyncServerError { Self::InitError(_) | Self::ServerError(_) => { error!("{serialized}"); } - Self::ClientError(_) | Self::NotFound(_) => { - warn!("{serialized}"); - } - Self::TooManyRequests(_) => { + Self::ClientError(_) | Self::NotFound(_) | Self::TooManyRequests(_) => { warn!("{serialized}"); } Self::Unauthenticated(_) | Self::PermissionDeniedError(_) => {} diff --git a/sync-server/src/main.rs b/sync-server/src/main.rs index dc00d4d5..7cf2227c 100644 --- a/sync-server/src/main.rs +++ b/sync-server/src/main.rs @@ -14,7 +14,7 @@ use cli::args::Args; use config::Config; use consts::DEFAULT_CONFIG_PATH; use errors::{SyncServerError, init_error}; -use log::info; +use log::{error, info, warn}; use server::create_server; use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::{EnvFilter, fmt::format, layer::SubscriberExt, util::SubscriberInitExt}; @@ -36,30 +36,63 @@ async fn main() -> ExitCode { .map_err(init_error) { Ok(config) => config, - Err(e) => { - eprintln!("{}", e.serialize()); - return ExitCode::FAILURE; + Err(error) => { + return exit_with_startup_error(&args, &error); } }; - let result = async { - config.validate().map_err(init_error)?; - // Hold the non-blocking writer guards until shutdown so the - // dedicated writer threads stay alive and flush queued log lines. - let _log_guards = set_up_logging(&args, &config.logging)?; - start_server(config).await + if let Err(error) = config.validate().map_err(init_error) { + return exit_with_startup_error(&args, &error); } - .await; - match result { + // Hold the non-blocking writer guards until shutdown so the dedicated + // writer threads stay alive and flush queued log lines. + let _log_guards = match set_up_logging(&args, &config.logging) { + Ok(log_guards) => log_guards, + Err(error) => { + return exit_with_startup_error(&args, &error); + } + }; + + match start_server(config).await { Ok(()) => ExitCode::SUCCESS, - Err(e) => { - eprintln!("{}", e.serialize()); + Err(error) => { + let serialized = error.serialize(); + warn!("{serialized}"); ExitCode::FAILURE } } } +fn exit_with_startup_error(args: &Args, err: &SyncServerError) -> ExitCode { + let _ = set_up_stderr_logging(args); + + let serialized = err.serialize(); + error!("{serialized}"); + + ExitCode::FAILURE +} + +fn set_up_stderr_logging(args: &Args) -> Result<(), SyncServerError> { + let env_filter = EnvFilter::builder() + .with_default_directive(tracing::Level::WARN.into()) + .from_env() + .context("Failed to create logging env filter") + .map_err(init_error)?; + + let stderr_layer = tracing_subscriber::fmt::layer() + .with_ansi(args.color.use_colors()) + .with_writer(std::io::stderr) + .event_format(format().compact()); + + tracing_subscriber::registry() + .with(env_filter) + .with(stderr_layer) + .try_init() + .context("Failed to initialise fallback tracing") + .map_err(init_error) +} + fn set_up_logging( args: &Args, logging_config: &config::logging_config::LoggingConfig, diff --git a/sync-server/src/server.rs b/sync-server/src/server.rs index 8f4f9a7a..35bcd4f6 100644 --- a/sync-server/src/server.rs +++ b/sync-server/src/server.rs @@ -4,8 +4,6 @@ mod delete_document; mod device_id_header; mod fetch_document_version; mod fetch_document_version_content; -mod fetch_latest_document_version; -mod fetch_latest_documents; mod index; mod ping; mod rate_limit; @@ -14,13 +12,14 @@ mod responses; mod update_document; mod websocket; -use anyhow::{Context as _, Result}; +use anyhow::{Context as _, Result, anyhow}; use auth::auth_middleware; use axum::{ Router, extract::{DefaultBodyLimit, Request}, http::{self, HeaderValue, Method}, middleware, + response::IntoResponse, routing::{IntoMakeService, delete, get, post, put}, }; use device_id_header::DEVICE_ID_HEADER_NAME; @@ -42,6 +41,7 @@ use crate::{ app_state::AppState, config::{Config, server_config::ServerConfig}, consts::GRACEFUL_SHUTDOWN_TIMEOUT, + errors::not_found_error, }; pub async fn create_server(config: Config) -> Result<()> { @@ -95,6 +95,7 @@ pub async fn create_server(config: Config) -> Result<()> { .on_failure(DefaultOnFailure::new().level(Level::ERROR)), ) .with_state(app_state.clone()) + .fallback(handle_404) .into_make_service(); start_server(app, &server_config, app_state).await @@ -131,18 +132,10 @@ fn build_cors_layer(server_config: &ServerConfig) -> Result { fn get_authed_routes(app_state: AppState) -> Router { Router::new() - .route( - "/vaults/:vault_id/documents", - get(fetch_latest_documents::fetch_latest_documents), - ) .route( "/vaults/:vault_id/documents", post(create_document::create_document), ) - .route( - "/vaults/:vault_id/documents/:document_id", - get(fetch_latest_document_version::fetch_latest_document_version), - ) .route( "/vaults/:vault_id/documents/:document_id/binary", put(update_document::update_binary), @@ -233,3 +226,7 @@ async fn shutdown_signal() { () = terminate => {}, } } + +async fn handle_404() -> impl IntoResponse { + not_found_error(anyhow!("Endpoint not found")) +} diff --git a/sync-server/src/server/auth.rs b/sync-server/src/server/auth.rs index 7fa45abd..90bdb205 100644 --- a/sync-server/src/server/auth.rs +++ b/sync-server/src/server/auth.rs @@ -34,7 +34,7 @@ pub async fn auth_middleware( .ok_or_else(|| unauthenticated_error(anyhow::anyhow!("Missing vault_id")))?, ); - let user = auth(&state, token, &vault_id)?; + let user = authenticate_for_vault(&state, token, &vault_id)?; req.extensions_mut().insert(user); @@ -50,7 +50,11 @@ pub fn authenticate(state: &AppState, token: &str) -> Result Result { +pub fn authenticate_for_vault( + state: &AppState, + token: &str, + vault_id: &VaultId, +) -> Result { let user = authenticate(state, token)?; if match user.vault_access { diff --git a/sync-server/src/server/create_document.rs b/sync-server/src/server/create_document.rs index d772e16a..afff662d 100644 --- a/sync-server/src/server/create_document.rs +++ b/sync-server/src/server/create_document.rs @@ -136,9 +136,7 @@ pub async fn create_document( { info!( "Lost-create recovery: binding retry at `{sanitized_relative_path}` to existing doc {} (was at `{}`) in vault `{vault_id}` for device `{}`", - lost_create.document_id, - lost_create.relative_path, - device_id.0 + lost_create.document_id, lost_create.relative_path, device_id.0 ); return update_document::update_document( &sanitized_relative_path, diff --git a/sync-server/src/server/websocket.rs b/sync-server/src/server/websocket.rs index 6e1af0ba..2cf91d1d 100644 --- a/sync-server/src/server/websocket.rs +++ b/sync-server/src/server/websocket.rs @@ -136,8 +136,7 @@ async fn websocket( // catch-up and in a contended-then-released broadcast is // delivered exactly once (via the catch-up). let send_guard = state.broadcasts.acquire_send_lock(&vault_id).await; - let mut broadcast_receiver = match state.broadcasts.get_receiver(vault_id.clone(), max_clients) - { + let mut broadcast_receiver = match state.broadcasts.get_receiver(&vault_id, max_clients) { Ok(receiver) => receiver, Err(err) => { drop(send_guard);