diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..46adaad --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[env] +MTP_TYPE_MAPS = { value = "mtp-type-maps/type-maps.yaml", relative = true } diff --git a/.fallowrc.json b/.fallowrc.json index 022e0cc..ba50fb5 100644 --- a/.fallowrc.json +++ b/.fallowrc.json @@ -3,6 +3,7 @@ "entry": [ "src/index.{ts,tsx,js,jsx}", "src/main.{ts,tsx,js,jsx}", + "apps/pwa/src/serviceWorker.ts", "apps/tauri/render-version.ts", "packages/**/*.test.ts" ], diff --git a/.forgejo/workflows/dependency-builds.yml b/.forgejo/workflows/dependency-builds.yml index 709c75b..2ae48fe 100644 --- a/.forgejo/workflows/dependency-builds.yml +++ b/.forgejo/workflows/dependency-builds.yml @@ -4,7 +4,6 @@ on: pull_request: env: - NIX_CONFIG: experimental-features = nix-command flakes FORGEJO_TOKEN: "" GITHUB_TOKEN: "" @@ -14,7 +13,6 @@ jobs: name: Build web runs-on: nixos steps: - - run: nix profile add nixpkgs#nodejs_24 - uses: https://data.forgejo.org/actions/checkout@v4 with: persist-credentials: false @@ -28,7 +26,6 @@ jobs: name: Build desktop runs-on: nixos steps: - - run: nix profile add nixpkgs#nodejs_24 - uses: https://data.forgejo.org/actions/checkout@v4 with: persist-credentials: false @@ -37,12 +34,23 @@ jobs: - run: nix develop .#electron --command pnpm run build:packages - run: nix develop .#electron --command pnpm run build:desktop - mobile: + native-mtp: if: ${{ github.actor == 'rasensprenger' }} - name: Build mobile + name: Test native MTP runs-on: nixos steps: - run: nix profile add nixpkgs#nodejs_24 + - uses: https://data.forgejo.org/actions/checkout@v4 + with: + persist-credentials: false + - run: git submodule update --init --recursive + - run: nix develop .#electron --command bash -lc 'cd apps/tauri/src-tauri && cargo test' + + mobile: + if: ${{ github.actor == 'rasensprenger' }} + name: Build mobile + runs-on: nixos + steps: - uses: https://data.forgejo.org/actions/checkout@v4 with: persist-credentials: false diff --git a/.forgejo/workflows/deploy-dev.yml b/.forgejo/workflows/deploy-dev.yml index 0d5be50..7f59ec1 100644 --- a/.forgejo/workflows/deploy-dev.yml +++ b/.forgejo/workflows/deploy-dev.yml @@ -1,20 +1,15 @@ on: + workflow_dispatch: push: branches: - dev paths-ignore: - flake.nix -env: - NIX_CONFIG: experimental-features = nix-command flakes - jobs: build-web: runs-on: nixos steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 @@ -39,9 +34,6 @@ jobs: build-mobile: runs-on: nixos steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 @@ -62,8 +54,6 @@ jobs: KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }} run: | - nix profile add nixpkgs#gnused - set -euo pipefail if [ -z "$KEYSTORE_BASE64" ]; then @@ -128,9 +118,6 @@ jobs: matrix: target: [linux] steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 @@ -181,9 +168,6 @@ jobs: runs-on: nixos needs: [build-web, build-mobile, build-desktop] steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 with: diff --git a/.forgejo/workflows/deploy-prod.yml b/.forgejo/workflows/deploy-prod.yml index d85ce27..621a0cd 100644 --- a/.forgejo/workflows/deploy-prod.yml +++ b/.forgejo/workflows/deploy-prod.yml @@ -6,16 +6,10 @@ on: paths-ignore: - flake.nix -env: - NIX_CONFIG: experimental-features = nix-command flakes - jobs: build-web: runs-on: nixos steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 @@ -40,9 +34,6 @@ jobs: build-mobile: runs-on: nixos steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 @@ -63,8 +54,6 @@ jobs: KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }} run: | - nix profile add nixpkgs#gnused - set -euo pipefail if [ -z "$KEYSTORE_BASE64" ]; then @@ -129,9 +118,6 @@ jobs: matrix: target: [linux] steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 @@ -180,9 +166,6 @@ jobs: runs-on: nixos needs: [build-web, build-mobile, build-desktop] steps: - - name: Install node - run: nix profile add nixpkgs#nodejs_24 - - name: Check out repo uses: https://data.forgejo.org/actions/checkout@v4 @@ -328,58 +311,3 @@ jobs: "$API/repos/$REPO/releases/$release_id" done < "$DELETE_RELEASES" EOF - - - name: Update root flake release hash - env: - TAG: ${{ steps.version.outputs.tag }} - run: | - nix develop .#electron --command bash <<'EOF' - set -eu - - DEB="$(find releases -maxdepth 1 -type f -name 'Tensamin-*-linux-amd64.deb' -print -quit)" - test -n "$DEB" - - HASH="$(node -e 'const fs = require("fs"); const crypto = require("crypto"); const file = process.argv[1]; console.log("sha256-" + crypto.createHash("sha256").update(fs.readFileSync(file)).digest("base64"));' "$DEB")" - export HASH - - node -e ' - const fs = require("fs"); - const version = process.env.TAG; - const hash = process.env.HASH; - let content = fs.readFileSync("flake.nix", "utf8"); - content = content.replace(/version = "[^"]+";/, `version = "${version}";`); - content = content.replace(/x86_64DebHash = "sha256-[^"]+";/, `x86_64DebHash = "${hash}";`); - fs.writeFileSync("flake.nix", content); - ' - - if git diff --quiet -- flake.nix; then - echo "flake.nix already has the current release hash on main." - else - git add flake.nix - git -c user.name="forgejo-actions" -c user.email="forgejo-actions@localhost" commit -m "(qol): update release flake hash" - git push - fi - - git fetch origin dev - git worktree add ../dev-flake-update origin/dev - cd ../dev-flake-update - - node -e ' - const fs = require("fs"); - const version = process.env.TAG; - const hash = process.env.HASH; - let content = fs.readFileSync("flake.nix", "utf8"); - content = content.replace(/version = "[^"]+";/, `version = "${version}";`); - content = content.replace(/x86_64DebHash = "sha256-[^"]+";/, `x86_64DebHash = "${hash}";`); - fs.writeFileSync("flake.nix", content); - ' - - if git diff --quiet -- flake.nix; then - echo "flake.nix already has the current release hash on dev." - exit 0 - fi - - git add flake.nix - git -c user.name="forgejo-actions" -c user.email="forgejo-actions@localhost" commit -m "(qol): update release flake hash" - git push origin HEAD:dev - EOF diff --git a/apps/electron/build/icons/128x128.png b/apps/electron/build/icons/128x128.png deleted file mode 100644 index 357e1c2..0000000 Binary files a/apps/electron/build/icons/128x128.png and /dev/null differ diff --git a/apps/electron/build/icons/128x128@2x.png b/apps/electron/build/icons/128x128@2x.png deleted file mode 100644 index be55e24..0000000 Binary files a/apps/electron/build/icons/128x128@2x.png and /dev/null differ diff --git a/apps/electron/build/icons/32x32.png b/apps/electron/build/icons/32x32.png deleted file mode 100644 index bc4fba4..0000000 Binary files a/apps/electron/build/icons/32x32.png and /dev/null differ diff --git a/apps/electron/build/icons/64x64.png b/apps/electron/build/icons/64x64.png deleted file mode 100644 index 567f4ad..0000000 Binary files a/apps/electron/build/icons/64x64.png and /dev/null differ diff --git a/apps/electron/build/icons/icon.icns b/apps/electron/build/icons/icon.icns deleted file mode 100644 index a7f0a1e..0000000 Binary files a/apps/electron/build/icons/icon.icns and /dev/null differ diff --git a/apps/electron/build/icons/icon.ico b/apps/electron/build/icons/icon.ico deleted file mode 100644 index c5c68f8..0000000 Binary files a/apps/electron/build/icons/icon.ico and /dev/null differ diff --git a/apps/electron/build/icons/icon.png b/apps/electron/build/icons/icon.png deleted file mode 100644 index dad3edb..0000000 Binary files a/apps/electron/build/icons/icon.png and /dev/null differ diff --git a/apps/pwa/package.json b/apps/pwa/package.json new file mode 100644 index 0000000..39c4ec5 --- /dev/null +++ b/apps/pwa/package.json @@ -0,0 +1,36 @@ +{ + "name": "@tensamin/pwa", + "private": true, + "version": "0.0.0", + "type": "module", + "exports": { + "./vite": "./src/vite.ts", + "./runtime": "./src/runtime.tsx" + }, + "scripts": { + "format": "pnpm exec prettier --write .", + "lint": "eslint src", + "build": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.worker.json --noEmit" + }, + "dependencies": { + "@methanium/ui": "*", + "@tauri-apps/api": "^2.11.1", + "@tensamin/crypto": "workspace:*", + "@tensamin/shared": "workspace:*", + "@tensamin/storage": "workspace:*", + "mtp": "*", + "react": "^19.2.8", + "sonner": "^2.0.7", + "vite-plugin-pwa": "^1.1.0", + "workbox-core": "^7.3.0", + "workbox-precaching": "^7.3.0", + "workbox-routing": "^7.3.0", + "workbox-strategies": "^7.3.0" + }, + "devDependencies": { + "@types/node": "^26.1.2", + "@types/react": "^19.2.18", + "typescript": "~6.0.3", + "vite": "^8.2.1" + } +} diff --git a/apps/pwa/src/runtime.tsx b/apps/pwa/src/runtime.tsx new file mode 100644 index 0000000..a1947eb --- /dev/null +++ b/apps/pwa/src/runtime.tsx @@ -0,0 +1,189 @@ +import { useEffect } from "react"; +import { toast } from "sonner"; + +import { setDatabaseEntry } from "@tensamin/shared/indexedDb"; +import { isTauri } from "@tauri-apps/api/core"; + +import "./style.css"; + +const launchedFiles: File[] = []; +const fileListeners = new Set<(file: File) => void>(); + +function emitLaunchedFile(file: File) { + if (fileListeners.size === 0) launchedFiles.push(file); + else for (const listener of fileListeners) listener(file); +} + +export function subscribeTuFileLaunch(listener: (file: File) => void) { + fileListeners.add(listener); + for (const file of launchedFiles.splice(0)) listener(file); + return () => { + fileListeners.delete(listener); + }; +} + +function isInstalledPwa() { + return ( + window.matchMedia("(display-mode: standalone)").matches || + window.matchMedia("(display-mode: window-controls-overlay)").matches || + (navigator as Navigator & { standalone?: boolean }).standalone === true + ); +} + +function applicationServerKey(value: string) { + const normalized = value.replace(/-/g, "+").replace(/_/g, "/"); + const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="); + return Uint8Array.from(atob(padded), (character) => character.charCodeAt(0)); +} + +async function enablePush() { + if (!("Notification" in window)) + throw new Error("Notifications are not supported by this browser."); + const permission = await Notification.requestPermission(); + if (permission !== "granted") + throw new Error("Notification permission was not granted."); + + const publicKey = import.meta.env.VITE_WEB_PUSH_PUBLIC_KEY; + if ( + !publicKey || + !("serviceWorker" in navigator) || + !("PushManager" in window) + ) { + return; + } + const registration = await navigator.serviceWorker.ready; + const subscription = + (await registration.pushManager.getSubscription()) ?? + (await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: applicationServerKey(publicKey), + })); + await setDatabaseEntry("keys", "push-subscription", subscription.toJSON()); +} + +function InstalledPwaRuntime() { + useEffect(() => { + if ( + !("serviceWorker" in navigator) || + !["http:", "https:"].includes(window.location.protocol) + ) { + return; + } + let reloading = false; + const handleControllerChange = () => { + if (reloading) return; + reloading = true; + // A newly activated service worker must reload the document it controls. + // eslint-disable-next-line tensamin/no-window-location-reload + window.location.reload(); + }; + navigator.serviceWorker.addEventListener( + "controllerchange", + handleControllerChange, + ); + void navigator.serviceWorker + .register( + import.meta.env.DEV ? "/dev-sw.js?dev-sw" : "/serviceWorker.js", + { + type: "module", + }, + ) + .then((registration) => { + const watchWorker = (worker: ServiceWorker) => { + worker.addEventListener("statechange", () => { + if (worker.state !== "installed") return; + if (!navigator.serviceWorker.controller) { + toast.success("Tensamin is ready for offline startup"); + return; + } + toast("A Tensamin update is ready", { + duration: Infinity, + action: { + label: "Update", + onClick: () => worker.postMessage({ type: "SKIP_WAITING" }), + }, + }); + }); + }; + if (registration.installing) watchWorker(registration.installing); + registration.addEventListener("updatefound", () => { + if (registration.installing) watchWorker(registration.installing); + }); + }) + .catch((error: unknown) => { + console.error("Failed to register the Tensamin service worker", error); + }); + return () => { + navigator.serviceWorker.removeEventListener( + "controllerchange", + handleControllerChange, + ); + }; + }, []); + + useEffect(() => { + const launchQueue = ( + window as Window & { + launchQueue?: { + setConsumer: ( + consumer: (params: { + files?: Array<{ getFile: () => Promise }>; + }) => void, + ) => void; + }; + } + ).launchQueue; + launchQueue?.setConsumer((params) => { + for (const handle of params.files ?? []) { + void handle.getFile().then(emitLaunchedFile); + } + }); + }, []); + + useEffect(() => { + if ( + !("Notification" in window) || + Notification.permission !== "default" || + localStorage.getItem("pwa-push-hint") + ) { + return; + } + localStorage.setItem("pwa-push-hint", "shown"); + toast("Enable message notifications", { + duration: Infinity, + action: { + label: "Enable", + onClick: () => { + void enablePush() + .then(() => toast.success("Notifications enabled")) + .catch((error: unknown) => + toast.error( + error instanceof Error + ? error.message + : "Could not enable notifications", + ), + ); + }, + }, + }); + }, []); + + useEffect(() => { + if ( + "Notification" in window && + Notification.permission === "granted" && + import.meta.env.VITE_WEB_PUSH_PUBLIC_KEY + ) { + void enablePush().catch((error: unknown) => { + console.error("Failed to refresh the Web Push subscription", error); + }); + } + }, []); + + return null; +} + +export default function PwaRuntime() { + if (isTauri() || !isInstalledPwa()) return null; + return ; +} diff --git a/apps/pwa/src/serviceWorker.ts b/apps/pwa/src/serviceWorker.ts new file mode 100644 index 0000000..554ceeb --- /dev/null +++ b/apps/pwa/src/serviceWorker.ts @@ -0,0 +1,161 @@ +/// + +import { base64ToBytes } from "mtp"; +import { clientsClaim } from "workbox-core"; +import { cleanupOutdatedCaches, precacheAndRoute } from "workbox-precaching"; +import { NavigationRoute, registerRoute } from "workbox-routing"; +import { createHandlerBoundToURL } from "workbox-precaching"; +import { CacheFirst } from "workbox-strategies"; + +import { decryptChatText, unwrapChatSecret } from "@tensamin/crypto/chatSecret"; +import { loadSecureBrowserValue } from "@tensamin/storage/browserSecure"; + +declare let self: ServiceWorkerGlobalScope; + +type PushPayload = { + version: 1; + senderId: number; + sender: string; + avatar?: string; + message: { content: string }; + secret: { + chatId: string; + secretId: string; + version: number; + encryptedSecret: string; + kemCiphertext: string; + wrappingScheme: string; + }; +}; + +function isPushPayload(value: unknown): value is PushPayload { + if (!value || typeof value !== "object") return false; + const payload = value as Partial; + const message = payload.message as + Partial | undefined; + const secret = payload.secret as Partial | undefined; + const stringValues = [ + payload.sender, + message?.content, + secret?.chatId, + secret?.secretId, + secret?.encryptedSecret, + secret?.kemCiphertext, + secret?.wrappingScheme, + ]; + return ( + payload.version === 1 && + typeof payload.senderId === "number" && + Number.isSafeInteger(payload.senderId) && + payload.senderId > 0 && + typeof secret?.version === "number" && + stringValues.every((item) => typeof item === "string") + ); +} + +async function decryptPush(payload: PushPayload) { + const keyring = await loadSecureBrowserValue("mtp_keyring"); + if (!keyring) throw new Error("MTP credentials are unavailable."); + const chatSecret = await unwrapChatSecret({ + encryptedSecret: base64ToBytes(payload.secret.encryptedSecret), + kemCiphertext: base64ToBytes(payload.secret.kemCiphertext), + keyring, + chatId: payload.secret.chatId, + secretId: payload.secret.secretId, + version: payload.secret.version, + wrappingScheme: payload.secret.wrappingScheme, + }); + try { + return await decryptChatText(chatSecret, payload.message.content); + } finally { + chatSecret.fill(0); + } +} + +clientsClaim(); +cleanupOutdatedCaches(); +const precacheManifest = self.__WB_MANIFEST; +precacheAndRoute(precacheManifest); + +if ( + precacheManifest.some((entry) => + (typeof entry === "string" ? entry : entry.url).endsWith("index.html"), + ) +) { + registerRoute( + new NavigationRoute(createHandlerBoundToURL("index.html"), { + denylist: [/^\/api\//], + }), + ); +} +registerRoute( + ({ request, url }) => + url.origin === self.location.origin && + ["font", "image", "style"].includes(request.destination), + new CacheFirst({ cacheName: "tensamin-static-v1" }), +); + +self.addEventListener("push", (event) => { + event.waitUntil( + (async () => { + let payload: PushPayload | undefined; + try { + const value = event.data?.json() as unknown; + if (isPushPayload(value)) payload = value; + } catch { + // The generic notification below is safe for malformed payloads. + } + + let body = "Open Tensamin to view the encrypted message."; + if (payload) { + try { + body = await decryptPush(payload); + } catch { + // Do not leak credential or decryption failures in the notification. + } + } + + await self.registration.showNotification(payload?.sender ?? "Tensamin", { + body, + icon: payload?.avatar || "./icons/icon-192.png", + badge: "./icons/notification-badge.png", + tag: payload ? `message-${payload.senderId}` : "message", + data: { url: payload ? `/chat?id=${payload.senderId}` : "/" }, + }); + + const navigatorWithBadge = self.navigator as WorkerNavigator & { + setAppBadge?: (contents?: number) => Promise; + }; + await navigatorWithBadge.setAppBadge?.().catch(() => undefined); + })(), + ); +}); + +self.addEventListener("notificationclick", (event) => { + event.notification.close(); + event.waitUntil( + (async () => { + const target = new URL( + String( + (event.notification.data as { url?: string } | undefined)?.url ?? "/", + ), + self.location.origin, + ); + const windows = await self.clients.matchAll({ + type: "window", + includeUncontrolled: true, + }); + for (const client of windows) { + if ("navigate" in client) await client.navigate(target.href); + return client.focus(); + } + return self.clients.openWindow(target.href); + })(), + ); +}); + +self.addEventListener("message", (event) => { + if ((event.data as { type?: string } | undefined)?.type === "SKIP_WAITING") { + void self.skipWaiting(); + } +}); diff --git a/apps/pwa/src/style.css b/apps/pwa/src/style.css new file mode 100644 index 0000000..213d773 --- /dev/null +++ b/apps/pwa/src/style.css @@ -0,0 +1,18 @@ +@media (display-mode: standalone), (display-mode: fullscreen) { + [data-pwa-root] { + padding-top: env(safe-area-inset-top, 0px); + padding-right: env(safe-area-inset-right, 0px); + padding-left: env(safe-area-inset-left, 0px); + } +} + +@media (display-mode: window-controls-overlay) and (min-width: 768px) { + [data-pwa-navbar] { + min-height: env(titlebar-area-height, 3.375rem); + padding-left: max(1px, env(titlebar-area-x, 0px)); + padding-right: max( + 0px, + calc(100vw - env(titlebar-area-x, 0px) - env(titlebar-area-width, 100vw)) + ); + } +} diff --git a/apps/pwa/src/vite.ts b/apps/pwa/src/vite.ts new file mode 100644 index 0000000..962dce1 --- /dev/null +++ b/apps/pwa/src/vite.ts @@ -0,0 +1,218 @@ +import { readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import type { Plugin } from "vite"; +import { VitePWA } from "vite-plugin-pwa"; + +const pwaDirectory = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const tauriIcons = resolve(pwaDirectory, "../tauri/src-tauri/icons"); +const androidResources = resolve( + pwaDirectory, + "../tauri/src-tauri/gen/android/app/src/main/res", +); + +function emitIcons(): Plugin { + const icons = [ + { + fileName: "icons/icon-180.png", + source: resolve(tauriIcons, "ios/AppIcon-60x60@3x.png"), + }, + { + fileName: "icons/icon-192.png", + source: resolve(androidResources, "mipmap-xxxhdpi/ic_launcher.png"), + }, + { + fileName: "icons/icon-96.png", + source: resolve(androidResources, "mipmap-xhdpi/ic_launcher.png"), + }, + { + fileName: "icons/icon-512.png", + source: resolve(tauriIcons, "icon.png"), + }, + { + fileName: "icons/icon-maskable-512.png", + source: resolve(tauriIcons, "icon.png"), + }, + { + fileName: "icons/icon-monochrome-432.png", + source: resolve( + androidResources, + "mipmap-xxxhdpi/ic_launcher_monochrome.png", + ), + }, + { + fileName: "icons/notification-badge.png", + source: resolve(androidResources, "drawable/ic_notification_small.png"), + }, + ]; + + return { + name: "tensamin-pwa-icons", + configureServer(server) { + server.middlewares.use((request, response, next) => { + const pathname = request.url + ? new URL(request.url, "http://localhost").pathname.slice(1) + : ""; + const icon = icons.find(({ fileName }) => fileName === pathname); + if (!icon) { + next(); + return; + } + response.statusCode = 200; + response.setHeader("Content-Type", "image/png"); + response.setHeader("Cache-Control", "no-cache"); + response.end(readFileSync(icon.source)); + }); + }, + generateBundle() { + for (const icon of icons) { + this.emitFile({ + type: "asset", + fileName: icon.fileName, + source: readFileSync(icon.source), + }); + } + }, + transformIndexHtml: { + order: "post", + handler() { + return [ + { + tag: "link", + attrs: { + rel: "apple-touch-icon", + sizes: "180x180", + href: "./icons/icon-180.png", + }, + injectTo: "head", + }, + { + tag: "meta", + attrs: { name: "apple-mobile-web-app-capable", content: "yes" }, + injectTo: "head", + }, + { + tag: "meta", + attrs: { + name: "apple-mobile-web-app-status-bar-style", + content: "black-translucent", + }, + injectTo: "head", + }, + { + tag: "meta", + attrs: { + name: "apple-mobile-web-app-title", + content: "Tensamin", + }, + injectTo: "head", + }, + { + tag: "meta", + attrs: { name: "theme-color", content: "#006a67" }, + injectTo: "head", + }, + ]; + }, + }, + }; +} + +export function tensaminPwa(): Plugin[] { + return [ + emitIcons(), + ...VitePWA({ + strategies: "injectManifest", + srcDir: resolve(pwaDirectory, "src"), + filename: "serviceWorker.ts", + injectRegister: null, + registerType: "prompt", + buildBase: "/", + manifestFilename: "manifest.json", + includeAssets: ["favicon.ico", "icons/*.png"], + manifest: { + id: "/", + name: "Tensamin", + short_name: "Tensamin", + description: "Private messaging and calls with Tensamin.", + start_url: "/", + scope: "/", + display: "standalone", + display_override: ["window-controls-overlay", "standalone"], + background_color: "#001f1e", + theme_color: "#006a67", + categories: ["social", "communication"], + orientation: "any", + launch_handler: { client_mode: "focus-existing" }, + icons: [ + { + src: "icons/icon-192.png", + sizes: "192x192", + type: "image/png", + purpose: "any", + }, + { + src: "icons/icon-512.png", + sizes: "512x512", + type: "image/png", + purpose: "any", + }, + { + src: "icons/icon-maskable-512.png", + sizes: "512x512", + type: "image/png", + purpose: "maskable", + }, + { + src: "icons/icon-monochrome-432.png", + sizes: "432x432", + type: "image/png", + purpose: "monochrome", + }, + ], + shortcuts: [ + { + name: "Chats", + short_name: "Chats", + url: "/", + icons: [ + { + src: "icons/icon-96.png", + sizes: "96x96", + type: "image/png", + }, + ], + }, + { + name: "Settings", + short_name: "Settings", + url: "/settings", + icons: [ + { + src: "icons/icon-96.png", + sizes: "96x96", + type: "image/png", + }, + ], + }, + ], + file_handlers: [ + { + action: "/login", + accept: { "application/x-tensamin-user": [".tu"] }, + }, + ], + }, + injectManifest: { + globPatterns: ["**/*.{js,css,html,ico,png,svg,woff2,wasm,mp3,wav}"], + globIgnores: ["assets/v2/**"], + maximumFileSizeToCacheInBytes: 15 * 1024 * 1024, + }, + devOptions: { + enabled: true, + type: "module", + }, + }), + ]; +} diff --git a/apps/pwa/todo.md b/apps/pwa/todo.md new file mode 100644 index 0000000..b5b2055 --- /dev/null +++ b/apps/pwa/todo.md @@ -0,0 +1,33 @@ +# Web Push Backend TODO + +The client can subscribe and decrypt version 1 push payloads, but reliable delivery requires backend support. + +- Generate and securely store a VAPID key pair. Expose only the public key to the web build as `VITE_WEB_PUSH_PUBLIC_KEY`. +- Add authenticated MTP requests for registering, replacing, and deleting a browser `PushSubscription` per user and installation. +- Persist the endpoint, `p256dh`, `auth`, expiration time, stable installation ID, and last-seen time. +- Remove subscriptions when a push service returns HTTP 404 or 410 and rate-limit registrations per user. +- Send pushes when an encrypted live message cannot be delivered to an active browser client. Define duplicate suppression for clients that receive both MTP and Web Push. +- Keep the JSON payload within push-provider limits and use this version 1 shape: + +```json +{ + "version": 1, + "senderId": 123, + "sender": "Display name", + "avatar": "https://optional.example/avatar", + "message": { "content": "base64 encrypted message content" }, + "secret": { + "chatId": "123:456", + "secretId": "chat:123:456:main", + "version": 1, + "encryptedSecret": "base64 wrapped chat secret", + "kemCiphertext": "base64 KEM ciphertext", + "wrappingScheme": "mtp-chat-secret-kem-chacha20poly1305-hkdf-sha256-v1" + } +} +``` + +- Ensure the wrapped secret is intended for the receiving user's MTP keyring. The server must never receive plaintext message content or plaintext chat secrets. +- Decide how edits, deletions, reactions, calls, read states, and per-chat notification cancellation map to push events. +- Add subscription rotation handling and unregister subscriptions when a user logs out or clears application data. +- Configure production HTTPS, SPA route fallback, `application/manifest+json` for `manifest.json`, and `Cache-Control: no-cache` for the service worker. diff --git a/apps/pwa/tsconfig.json b/apps/pwa/tsconfig.json new file mode 100644 index 0000000..1e7cfde --- /dev/null +++ b/apps/pwa/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client", "vite-plugin-pwa/client", "node"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/runtime.tsx", "src/vite.ts"] +} diff --git a/apps/pwa/tsconfig.worker.json b/apps/pwa/tsconfig.worker.json new file mode 100644 index 0000000..a478894 --- /dev/null +++ b/apps/pwa/tsconfig.worker.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022", "WebWorker"], + "types": ["vite-plugin-pwa/client"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/serviceWorker.ts"] +} diff --git a/apps/tauri/.cargo/config.toml b/apps/tauri/.cargo/config.toml deleted file mode 100644 index 161678f..0000000 --- a/apps/tauri/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[env] -MTP_TYPE_MAPS = { value = "../../mtp-type-maps/type-maps.yaml", relative = true } diff --git a/apps/tauri/package.json b/apps/tauri/package.json index 8eab808..36937be 100644 --- a/apps/tauri/package.json +++ b/apps/tauri/package.json @@ -7,10 +7,6 @@ "./deeplinkHandler": { "types": "./src/deeplinkHandler.tsx", "default": "./src/deeplinkHandler.tsx" - }, - "./qrCodeScanner": { - "types": "./src/qrCodeScanner.tsx", - "default": "./src/qrCodeScanner.tsx" } }, "scripts": { @@ -28,7 +24,6 @@ "dependencies": { "@methanium/ui": "*", "@tauri-apps/api": "^2.11.1", - "@tauri-apps/plugin-barcode-scanner": "~2.4.5", "@tauri-apps/plugin-deep-link": "~2.4.9", "@tensamin/shared": "workspace:*", "react": "^19.2.8", diff --git a/apps/tauri/src-tauri/Cargo.lock b/apps/tauri/src-tauri/Cargo.lock index 744d5a2..d404d61 100644 --- a/apps/tauri/src-tauri/Cargo.lock +++ b/apps/tauri/src-tauri/Cargo.lock @@ -674,12 +674,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "const-oid" version = "0.10.2" @@ -889,7 +883,6 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest 0.10.7", "fiat-crypto 0.2.9", "rustc_version", "subtle", @@ -974,25 +967,14 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid 0.9.6", - "pem-rfc7468 0.7.0", - "zeroize", -] - [[package]] name = "der" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" dependencies = [ - "const-oid 0.10.2", - "pem-rfc7468 1.0.0", + "const-oid", + "pem-rfc7468", "zeroize", ] @@ -1057,7 +1039,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.1", - "const-oid 0.10.2", + "const-oid", "crypto-common 0.2.2", "ctutils", ] @@ -1204,38 +1186,14 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8 0.10.2", - "signature 2.2.0", -] - [[package]] name = "ed25519" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" dependencies = [ - "pkcs8 0.11.0", - "signature 3.0.0", -] - -[[package]] -name = "ed25519-dalek" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek 4.1.3", - "ed25519 2.2.3", - "serde", - "sha2 0.10.9", - "subtle", - "zeroize", + "pkcs8", + "signature", ] [[package]] @@ -1245,10 +1203,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" dependencies = [ "curve25519-dalek 5.0.0", - "ed25519 3.0.0", + "ed25519", "serde", "sha2 0.11.0", - "signature 3.0.0", + "signature", "subtle", "zeroize", ] @@ -2737,14 +2695,14 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "add6b9d92e496f16f4526d68ff29da1483aba4b119baeab8bed3b9e3544a6f3d" dependencies = [ - "const-oid 0.10.2", + "const-oid", "crypto-common 0.2.2", "ctutils", "hybrid-array", "module-lattice", - "pkcs8 0.11.0", + "pkcs8", "shake", - "signature 3.0.0", + "signature", ] [[package]] @@ -2786,72 +2744,50 @@ dependencies = [ [[package]] name = "mtp" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "mtp-client", - "mtp-codec 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-common 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-crypto 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", + "mtp-codec", + "mtp-common", + "mtp-crypto", "mtp-host", - "mtp-transport 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-type-map 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", + "mtp-transport", + "mtp-type-map", "mtp-webserver", ] [[package]] name = "mtp-client" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ - "mtp-codec 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-common 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-crypto 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-transport 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", + "mtp-codec", + "mtp-common", + "mtp-crypto", + "mtp-transport", "rand 0.10.2", "tokio", ] [[package]] name = "mtp-codec" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "base64 0.23.1", "byteorder", - "mtp-common 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-crypto 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-type-map 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", + "mtp-common", + "mtp-crypto", + "mtp-type-map", "rand 0.10.2", -] - -[[package]] -name = "mtp-codec" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b#b067614a684eb1856bc5db7b3fd82148c036ce6b" -dependencies = [ - "base64 0.23.1", - "byteorder", - "mtp-common 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b)", - "mtp-type-map 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b)", - "rand 0.10.2", -] - -[[package]] -name = "mtp-common" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" -dependencies = [ - "quinn", - "rustls", "thiserror 2.0.19", - "wtransport", ] [[package]] name = "mtp-common" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b#b067614a684eb1856bc5db7b3fd82148c036ce6b" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "quinn", "rustls", @@ -2861,18 +2797,18 @@ dependencies = [ [[package]] name = "mtp-crypto" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ - "base64 0.23.1", + "base64 0.22.1", "chacha20poly1305", - "ed25519-dalek 2.2.0", + "ed25519-dalek", "getrandom 0.4.3", "hkdf", "ml-dsa", "mlkem-tls", "rand 0.10.2", - "rand_core 0.10.1", + "rand_core 0.6.4", "rustls", "serde", "sha2 0.11.0", @@ -2882,89 +2818,45 @@ dependencies = [ ] [[package]] -name = "mtp-crypto" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b#b067614a684eb1856bc5db7b3fd82148c036ce6b" +name = "mtp-host" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ - "base64 0.23.1", - "chacha20poly1305", - "ed25519-dalek 3.0.0", - "getrandom 0.4.3", - "hkdf", - "ml-dsa", + "mtp-codec", + "mtp-common", + "mtp-crypto", + "mtp-transport", "rand 0.10.2", - "rand_core 0.10.1", - "rustls", - "sha2 0.11.0", "thiserror 2.0.19", "tokio", + "tracing", + "wtransport", +] + +[[package]] +name = "mtp-transport" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" +dependencies = [ + "async-trait", + "mtp-codec", + "mtp-common", + "mtp-crypto", + "rand 0.10.2", + "rcgen", + "rustls", + "rustls-native-certs", + "sha2 0.11.0", + "tokio", + "tracing", + "wtransport", "zeroize", ] -[[package]] -name = "mtp-host" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" -dependencies = [ - "mtp-codec 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-common 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-crypto 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-transport 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "rand 0.10.2", - "tokio", - "tracing", - "wtransport", -] - -[[package]] -name = "mtp-transport" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" -dependencies = [ - "async-trait", - "mtp-codec 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-common 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-crypto 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "rcgen", - "rustls", - "rustls-native-certs", - "sha2 0.11.0", - "tokio", - "tracing", - "wtransport", -] - -[[package]] -name = "mtp-transport" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b#b067614a684eb1856bc5db7b3fd82148c036ce6b" -dependencies = [ - "async-trait", - "mtp-codec 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b)", - "mtp-common 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b)", - "mtp-crypto 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b)", - "rcgen", - "rustls", - "rustls-native-certs", - "sha2 0.11.0", - "tokio", - "tracing", - "wtransport", -] - [[package]] name = "mtp-type-map" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" -dependencies = [ - "serde", - "serde_yaml", -] - -[[package]] -name = "mtp-type-map" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b#b067614a684eb1856bc5db7b3fd82148c036ce6b" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "serde", "serde_yaml", @@ -2972,8 +2864,8 @@ dependencies = [ [[package]] name = "mtp-webserver" -version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" +version = "0.3.0" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "async-trait", "bytes", @@ -2984,13 +2876,12 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", - "mtp-codec 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-common 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", - "mtp-crypto 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", + "mtp-codec", + "mtp-common", + "mtp-crypto", "mtp-host", - "mtp-transport 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22)", + "mtp-transport", "quinn", - "rand 0.10.2", "rustls", "thiserror 2.0.19", "tokio", @@ -3492,15 +3383,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - [[package]] name = "pem-rfc7468" version = "1.0.0" @@ -3586,24 +3468,14 @@ dependencies = [ "futures-io", ] -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der 0.7.10", - "spki 0.7.3", -] - [[package]] name = "pkcs8" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" dependencies = [ - "der 0.8.1", - "spki 0.8.0", + "der", + "spki", ] [[package]] @@ -4592,15 +4464,6 @@ dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "rand_core 0.6.4", -] - [[package]] name = "signature" version = "3.0.0" @@ -4709,16 +4572,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der 0.7.10", -] - [[package]] name = "spki" version = "0.8.0" @@ -4726,7 +4579,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" dependencies = [ "base64ct", - "der 0.8.1", + "der", ] [[package]] @@ -5035,20 +4888,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "tauri-plugin-barcode-scanner" -version = "2.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b68f0e3782b61a6e16a67380be40226e2b7233f3e36dadf6e5d03f07d2e7b3" -dependencies = [ - "log", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.19", -] - [[package]] name = "tauri-plugin-deep-link" version = "2.4.9" @@ -5298,18 +5137,16 @@ dependencies = [ [[package]] name = "tensamin" -version = "0.0.11" +version = "0.0.0" dependencies = [ "base64 0.22.1", "jni 0.22.4", "mtp", - "mtp-transport 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b)", "reqwest", "serde", "serde_json", "tauri", "tauri-build", - "tauri-plugin-barcode-scanner", "tauri-plugin-deep-link", "tauri-plugin-log", "tauri-plugin-notification", diff --git a/apps/tauri/src-tauri/Cargo.toml b/apps/tauri/src-tauri/Cargo.toml index 3b7e6b5..2a68e95 100644 --- a/apps/tauri/src-tauri/Cargo.toml +++ b/apps/tauri/src-tauri/Cargo.toml @@ -24,8 +24,7 @@ serde_json = "1" base64 = "0.22" reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"] } tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] } -mtp = { git = "https://git.methanium.net/methanium/mtp.git", rev = "7182272e3edb0079d7b74b45b2f62a298d2fee22", features = ["client", "crypto"] } -mtp-transport = { git = "https://git.methanium.net/methanium/mtp.git", rev = "b067614a684eb1856bc5db7b3fd82148c036ce6b" } +mtp = { git = "https://git.methanium.net/Methanium/mtp.git", rev = "a5c8d4f0c898c78351e9d54124886c86e789a22a", features = ["client", "crypto"] } webpki-root-certs = "1" tauri-plugin-deep-link = "2" tauri-plugin-notification = "2" @@ -36,12 +35,14 @@ version = "2" features = [] default-features = true +[target.'cfg(not(target_os = "android"))'.dependencies.tauri] +version = "2" +features = [] +default-features = true + [target.'cfg(target_os = "android")'.dependencies] jni = "0.22" -[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] -tauri-plugin-barcode-scanner = "2" - [patch.crates-io.tauri] git = "https://github.com/tauri-apps/tauri" rev = "4af26a3f7f8b692d62cca549bbacd93f5ce90b41" diff --git a/apps/tauri/src-tauri/capabilities/mobile.json b/apps/tauri/src-tauri/capabilities/mobile.json index 7121f98..91f1d4e 100644 --- a/apps/tauri/src-tauri/capabilities/mobile.json +++ b/apps/tauri/src-tauri/capabilities/mobile.json @@ -5,9 +5,6 @@ "permissions": [ "core:event:default", "deep-link:default", - "barcode-scanner:default", - "barcode-scanner:allow-scan", - "barcode-scanner:allow-cancel", "notification:default", "log:default" ] diff --git a/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MainActivity.kt b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MainActivity.kt index fc3be18..66df13a 100644 --- a/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MainActivity.kt +++ b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MainActivity.kt @@ -5,6 +5,7 @@ import android.app.Activity import android.content.pm.PackageManager import android.media.projection.MediaProjectionManager import android.os.Bundle +import android.view.MotionEvent import android.view.ViewTreeObserver import android.view.WindowManager import android.webkit.JavascriptInterface @@ -54,6 +55,32 @@ class MainActivity : TauriActivity() { } override fun onWebViewCreate(webView: WebView) { + webView.settings.apply { + setSupportZoom(false) + builtInZoomControls = false + displayZoomControls = false + } + + var blockingMultiTouch = false + webView.setOnTouchListener { _, event -> + val shouldBlock = blockingMultiTouch || event.pointerCount > 1 + + when (event.actionMasked) { + MotionEvent.ACTION_POINTER_DOWN -> { + // Cancel the one-finger gesture before consuming the rest of the pinch. + MotionEvent.obtain(event).let { cancelEvent -> + cancelEvent.action = MotionEvent.ACTION_CANCEL + webView.onTouchEvent(cancelEvent) + cancelEvent.recycle() + } + blockingMultiTouch = true + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> blockingMultiTouch = false + } + + shouldBlock + } + NativeAccessibilityBridge.attach(webView) mediaWebView = webView MobileMediaEvents.attach(webView) diff --git a/apps/tauri/src-tauri/src/lib.rs b/apps/tauri/src-tauri/src/lib.rs index e81a830..6636682 100644 --- a/apps/tauri/src-tauri/src/lib.rs +++ b/apps/tauri/src-tauri/src/lib.rs @@ -15,9 +15,6 @@ pub fn run() { .plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_opener::init()); - #[cfg(any(target_os = "ios", target_os = "android"))] - let builder = builder.plugin(tauri_plugin_barcode_scanner::init()); - let app = builder .invoke_handler(tauri::generate_handler![ accessibility_backend::accessibility_get_initial_scale, diff --git a/apps/tauri/src-tauri/src/main.rs b/apps/tauri/src-tauri/src/main.rs index e78a26a..272583a 100644 --- a/apps/tauri/src-tauri/src/main.rs +++ b/apps/tauri/src-tauri/src/main.rs @@ -1,9 +1,6 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use lib::log; - fn main() { mobile_lib::run(); - log("Test test 123") } diff --git a/apps/tauri/src-tauri/src/mtp_backend.rs b/apps/tauri/src-tauri/src/mtp_backend.rs index b343337..fa28597 100644 --- a/apps/tauri/src-tauri/src/mtp_backend.rs +++ b/apps/tauri/src-tauri/src/mtp_backend.rs @@ -2,7 +2,7 @@ use std::sync::{ atomic::{AtomicBool, AtomicU64, Ordering}, Arc, Mutex, OnceLock, RwLock, }; -use std::time::Duration; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use base64::{ engine::general_purpose::{STANDARD, STANDARD_NO_PAD}, @@ -16,6 +16,7 @@ use mtp::crypto::{ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use tauri::{AppHandle, Emitter}; +use tokio::sync::mpsc; const EVENT_NAME: &str = "mtp://event"; const DISCONNECTED: u8 = 0; @@ -24,6 +25,9 @@ const CONNECTED: u8 = 2; const CHAT_SECRET_SALT: &[u8] = b"tensamin-chat-secret-v1"; const CHAT_MESSAGE_SALT: &[u8] = b"tensamin-chat-message-v1"; const CHAT_SECRET_SCHEME: &str = "mtp-chat-secret-kem-chacha20poly1305-hkdf-sha256-v1"; +const INITIAL_SYNC_TIMEOUT: Duration = Duration::from_secs(30); +const MAX_BUFFERED_INITIAL_FRAMES: usize = 1_000; +const NOTIFICATION_QUEUE_CAPACITY: usize = 32; #[cfg(target_os = "android")] const ROOT_YE_PEM: &[u8] = b"-----BEGIN CERTIFICATE-----\n\ MIIB2TCCAWCgAwIBAgIRAKQCa6LvbHwg1AR+XmWmk4AwCgYIKoZIzj0EAwMwLjEL\n\ @@ -95,7 +99,7 @@ enum MtpEvent { pub struct MtpManager { runtime: tokio::runtime::Runtime, config: RwLock>, - connection: RwLock>>, + connection: RwLock>>, snapshot: RwLock, generation: AtomicU64, enabled: AtomicBool, @@ -104,6 +108,46 @@ pub struct MtpManager { start_lock: Mutex<()>, } +struct RequestIdAllocator { + next: AtomicU64, +} + +impl RequestIdAllocator { + fn new() -> Self { + Self { + next: AtomicU64::new(1), + } + } + + fn next(&self) -> Result { + let value = self.next.fetch_add(1, Ordering::Relaxed); + u32::try_from(value) + .map_err(|_| "MTP request ID space exhausted for this connection".to_string()) + } +} + +struct ManagedConnection { + mtp: Arc, + request_ids: RequestIdAllocator, +} + +impl ManagedConnection { + async fn next_request_id(&self) -> Result { + let id = self.request_ids.next(); + if id.is_err() { + self.mtp.sender.close().await; + } + id + } +} + +struct PreparedConnection { + connection: MTPConnection, + request_ids: RequestIdAllocator, + initial_state: Value, + buffered_frames: Vec, +} + static MANAGER: OnceLock = OnceLock::new(); pub fn manager() -> &'static MtpManager { @@ -149,7 +193,7 @@ impl MtpManager { .take() { self.runtime - .spawn(async move { connection.sender.close().await }); + .spawn(async move { connection.mtp.sender.close().await }); } self.set_snapshot(MtpSnapshot { generation, @@ -171,7 +215,7 @@ impl MtpManager { .take() { self.runtime - .spawn(async move { connection.sender.close().await }); + .spawn(async move { connection.mtp.sender.close().await }); } self.set_snapshot(MtpSnapshot { generation: self.generation.load(Ordering::SeqCst), @@ -231,20 +275,16 @@ impl MtpManager { self.enabled.load(Ordering::SeqCst) && self.generation.load(Ordering::SeqCst) == generation } - async fn request( - &self, - type_name: &str, - data: Value, - id: Option, - ) -> Result { + async fn request(&self, type_name: &str, data: Value) -> Result { let connection = self .connection .read() .map_err(|_| "MTP connection lock is unavailable")? .clone() .ok_or_else(|| "MTP is not connected".to_string())?; - let request = json_to_frame(type_name, data, id)?; + let request = json_to_frame(type_name, data, connection.next_request_id().await?)?; let response = connection + .mtp .request(&request, None) .await .map_err(|error| error.to_string())?; @@ -261,9 +301,12 @@ async fn supervise(generation: u64) { manager.log(2, "Starting native MTP connection", None); android_status("Connecting"); match connect(&config).await { - Ok((connection, state)) => { + Ok(prepared) => { delay = Duration::from_secs(1); - let connection = Arc::new(connection); + let connection = Arc::new(ManagedConnection { + mtp: Arc::new(prepared.connection), + request_ids: prepared.request_ids, + }); let stale = { let _guard = manager.start_lock.lock().expect("start lock poisoned"); let mut current = manager @@ -279,22 +322,47 @@ async fn supervise(generation: u64) { generation, ready_state: CONNECTED, identified: true, - state: Some(state), + state: Some(prepared.initial_state), error: None, }); false } }; if stale { - connection.sender.close().await; + connection.mtp.sender.close().await; break; } android_status("Connected"); manager.log(2, "Native MTP connection established", None); + let (notification_tx, mut notification_rx) = + mpsc::channel(NOTIFICATION_QUEUE_CAPACITY); + let notification_connection = connection.clone(); + let notification_config = config.clone(); + let notification_worker = tokio::spawn(async move { + while let Some(frame) = notification_rx.recv().await { + if !manager.is_current(generation) { + break; + } + if let Err(error) = notify_message( + ¬ification_config, + notification_connection.clone(), + &frame, + ) + .await + { + eprintln!("failed to create background message notification: {error}"); + } + } + }); + + for frame in prepared.buffered_frames { + handle_push(generation, ¬ification_tx, frame).await; + } + while manager.is_current(generation) { - match connection.receive().await { - Ok(frame) => handle_push(generation, connection.clone(), frame).await, + match connection.mtp.receive().await { + Ok(frame) => handle_push(generation, ¬ification_tx, frame).await, Err(error) => { let _guard = manager.start_lock.lock().expect("start lock poisoned"); if manager.is_current(generation) { @@ -310,6 +378,13 @@ async fn supervise(generation: u64) { } } } + drop(notification_tx); + notification_worker.abort(); + if let Err(error) = notification_worker.await { + if !error.is_cancelled() { + eprintln!("background notification worker failed: {error}"); + } + } let mut current = manager .connection .write() @@ -339,12 +414,21 @@ async fn supervise(generation: u64) { break; } android_status("Reconnecting"); - tokio::time::sleep(delay).await; + tokio::time::sleep(jittered_retry_delay(delay)).await; delay = (delay * 2).min(Duration::from_secs(60)); } } -async fn connect(config: &MtpConfig) -> Result<(MTPConnection, Value), String> { +fn jittered_retry_delay(delay: Duration) -> Duration { + let entropy = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.subsec_nanos()) + .unwrap_or_default(); + let percent = 80 + entropy % 41; + delay.mul_f64(percent as f64 / 100.0) +} + +async fn connect(config: &MtpConfig) -> Result { let (url, public_key) = resolve_endpoint(config) .await .map_err(|error| format!("endpoint discovery failed: {error}"))?; @@ -381,39 +465,10 @@ async fn connect(config: &MtpConfig) -> Result<(MTPConnection, Value), String> { .map_err(|error| format!("transport authentication failed: {error}"))?; manager().log(2, "Native MTP authentication completed", None); - let connected = CommunicationValue::new(CommunicationType::ClientConnected) - .add_typed_default( - DataType::SessionId, - DataValue::UnsignedNumber(current_millis() as u128), - ) - .add_typed_default(DataType::VersionNumber, DataValue::UnsignedNumber(0)) - .add_typed_default(DataType::CacheValid, DataValue::BoolFalse) - .add_typed_default(DataType::CacheSchemaVersion, DataValue::UnsignedNumber(0)); - let state = tokio::time::timeout( - Duration::from_secs(30), - connection.request(&connected, None), - ) - .await - .map_err(|_| "initial state synchronization timed out".to_string())? - .map_err(|error| format!("initial state synchronization failed: {error}"))?; - if !state.is_type(CommunicationType::ClientStateSync) { - return Err(format!( - "expected ClientStateSync, received {}", - state.get_type_name().unwrap_or("unknown") - )); - } - let session_id = state - .get_data(DataType::SessionId) - .as_number() - .ok_or("ClientStateSync omitted SessionId")?; - let version = state - .get_data(DataType::VersionNumber) - .as_number() - .ok_or("ClientStateSync omitted VersionNumber")?; - let ack = CommunicationValue::new(CommunicationType::ClientStateAck) - .add_typed_default(DataType::SessionId, number_to_data(session_id)) - .add_typed_default(DataType::VersionNumber, number_to_data(version)); - let response = tokio::time::timeout(Duration::from_secs(30), connection.request(&ack, None)) + let (state, buffered_frames) = await_initial_state(&connection).await?; + let request_ids = RequestIdAllocator::new(); + let (initial_state, ack) = prepare_initial_state_ack(&state, &request_ids)?; + let response = tokio::time::timeout(INITIAL_SYNC_TIMEOUT, connection.request(&ack, None)) .await .map_err(|_| "state acknowledgement timed out".to_string())? .map_err(|error| format!("state acknowledgement failed: {error}"))?; @@ -423,7 +478,182 @@ async fn connect(config: &MtpConfig) -> Result<(MTPConnection, Value), String> { { return Err(format!("ClientStateAck failed: {response}")); } - Ok((connection, frame_data_to_json(&state)?)) + Ok(PreparedConnection { + connection, + request_ids, + initial_state, + buffered_frames, + }) +} + +fn prepare_initial_state_ack( + state: &CommunicationValue, + request_ids: &RequestIdAllocator, +) -> Result<(Value, CommunicationValue), String> { + let initial_state = frame_data_to_json(state)?; + validate_client_state_sync(&initial_state)?; + let data = initial_state + .as_object() + .ok_or("ClientStateSync payload is not an object")?; + let session_id = required_integer(data, "SessionId")?; + let version = required_integer(data, "VersionNumber")?; + let ack = CommunicationValue::new(communication_type("ClientStateAck")?) + .with_id(request_ids.next()?) + .add_typed_default(DataType::SessionId, number_to_data(session_id)) + .add_typed_default(DataType::VersionNumber, number_to_data(version)); + Ok((initial_state, ack)) +} + +fn validate_client_state_sync(state: &Value) -> Result<(), String> { + let data = state + .as_object() + .ok_or("ClientStateSync payload is not an object")?; + if required_integer(data, "SessionId")? <= 0 { + return Err("ClientStateSync SessionId must be a positive integer".into()); + } + for field in ["VersionNumber", "CacheSchemaVersion"] { + if required_integer(data, field)? < 0 { + return Err(format!("ClientStateSync {field} must be nonnegative")); + } + } + match data.get("SyncMode").and_then(Value::as_str) { + Some("full" | "delta") => {} + _ => return Err("ClientStateSync SyncMode must be 'full' or 'delta'".into()), + } + for field in ["Contacts", "Communities", "Calls", "Messages"] { + if !data.get(field).is_some_and(Value::is_array) { + return Err(format!("ClientStateSync {field} must be an array")); + } + } + for field in ["DeletedMessageIds", "DeletedContactIds"] { + if let Some(value) = data.get(field) { + let values = value + .as_array() + .ok_or_else(|| format!("ClientStateSync {field} must be an array"))?; + if values.iter().any(|value| !value.is_number()) { + return Err(format!("ClientStateSync {field} must contain numbers")); + } + } + } + validate_object_array(data, "Communities", |_| Ok(()))?; + validate_object_array(data, "Contacts", validate_contact)?; + validate_object_array(data, "Calls", validate_call)?; + validate_object_array(data, "Messages", validate_message)?; + Ok(()) +} + +fn validate_object_array( + data: &Map, + field: &str, + validate: impl Fn(&Map) -> Result<(), String>, +) -> Result<(), String> { + let values = data + .get(field) + .and_then(Value::as_array) + .ok_or_else(|| format!("ClientStateSync {field} must be an array"))?; + for value in values { + let object = value + .as_object() + .ok_or_else(|| format!("ClientStateSync {field} entries must be objects"))?; + validate(object)?; + } + Ok(()) +} + +fn validate_contact(contact: &Map) -> Result<(), String> { + if !contact.get("UserId").is_some_and(Value::is_number) { + return Err("ClientStateSync contact omitted numeric UserId".into()); + } + if let Some(messages) = contact.get("Messages") { + let messages = messages + .as_array() + .ok_or("ClientStateSync contact Messages must be an array")?; + for message in messages { + validate_message( + message + .as_object() + .ok_or("ClientStateSync contact message must be an object")?, + )?; + } + } + Ok(()) +} + +fn validate_call(call: &Map) -> Result<(), String> { + if !call.get("CallId").is_some_and(Value::is_string) { + return Err("ClientStateSync call omitted string CallId".into()); + } + let members = call + .get("CallMembers") + .and_then(Value::as_array) + .ok_or("ClientStateSync call omitted CallMembers array")?; + if members.iter().any(|member| !member.is_number()) { + return Err("ClientStateSync CallMembers must contain numbers".into()); + } + Ok(()) +} + +fn validate_message(message: &Map) -> Result<(), String> { + for field in ["SenderId", "SendTime"] { + if !message.get(field).is_some_and(Value::is_number) { + return Err(format!("ClientStateSync message omitted numeric {field}")); + } + } + let content = message + .get("Content") + .and_then(Value::as_str) + .ok_or("ClientStateSync message omitted string Content")?; + STANDARD + .decode(content) + .or_else(|_| STANDARD_NO_PAD.decode(content)) + .map_err(|_| "ClientStateSync message Content must be base64".to_string())?; + if let Some(state) = message.get("MessageState") { + match state.as_str() { + Some("read" | "received" | "sent" | "sending" | "awaiting") => {} + _ => return Err("ClientStateSync message has invalid MessageState".into()), + } + } + Ok(()) +} + +fn required_integer(data: &Map, field: &str) -> Result { + let value = data + .get(field) + .ok_or_else(|| format!("ClientStateSync omitted {field}"))?; + if let Some(value) = value.as_i64() { + return Ok(value as i128); + } + value + .as_u64() + .map(|value| value as i128) + .ok_or_else(|| format!("ClientStateSync {field} must be an integer")) +} + +async fn await_initial_state( + connection: &MTPConnection, +) -> Result<(CommunicationValue, Vec), String> { + let mut buffered = Vec::new(); + let deadline = tokio::time::Instant::now() + INITIAL_SYNC_TIMEOUT; + + loop { + let frame = tokio::time::timeout_at(deadline, connection.receive()) + .await + .map_err(|_| "initial state synchronization timed out".to_string())? + .map_err(|error| format!("initial state synchronization failed: {error}"))?; + + if frame.is_type(CommunicationType::ErrorNoIota) { + return Err("No Iota is currently connected".into()); + } + + if frame.get_type_name() == Some("ClientStateSync") { + return Ok((frame, buffered)); + } + + if buffered.len() == MAX_BUFFERED_INITIAL_FRAMES { + return Err("initial state synchronization buffered too many frames".into()); + } + buffered.push(frame); + } } async fn resolve_endpoint(config: &MtpConfig) -> Result<(String, String), String> { @@ -465,7 +695,11 @@ async fn resolve_endpoint(config: &MtpConfig) -> Result<(String, String), String )) } -async fn handle_push(generation: u64, connection: Arc, frame: CommunicationValue) { +async fn handle_push( + generation: u64, + notification_tx: &mpsc::Sender, + frame: CommunicationValue, +) { let manager = manager(); if let Ok(message) = frame_to_json(&frame) { manager.emit(MtpEvent::Message { @@ -478,7 +712,7 @@ async fn handle_push(generation: u64, connection: Arc, frame: Com { if let Some(partner_id) = frame .get_data(DataType::ChatPartnerId) - .as_number() + .and_then(DataValue::as_number) .and_then(|value| u64::try_from(value).ok()) { if let Err(error) = android_cancel_notification(partner_id) { @@ -487,40 +721,39 @@ async fn handle_push(generation: u64, connection: Arc, frame: Com } } if frame.is_type(CommunicationType::MessageLive) && !manager.ui_visible.load(Ordering::SeqCst) { - if let Err(error) = notify_message(connection, &frame).await { - eprintln!("failed to create background message notification: {error}"); + if notification_tx.try_send(frame).is_err() { + eprintln!("background message notification queue is full"); } } } async fn notify_message( - connection: Arc, + config: &MtpConfig, + connection: Arc, frame: &CommunicationValue, ) -> Result<(), String> { let sender_id = frame .get_data(DataType::SenderId) - .as_number() + .and_then(DataValue::as_number) .and_then(|value| u64::try_from(value).ok()) .ok_or("MessageLive omitted SenderId")?; - let message = frame.get_data(DataType::Message); - let content = container_value(message, DataType::Content) + let message = frame + .get_data(DataType::Message) + .ok_or("MessageLive omitted Message")?; + let content = container_value_by_name(message, "Content") .and_then(DataValue::as_str) .ok_or("MessageLive omitted Content")?; - let config = manager() - .config - .read() - .expect("config lock poisoned") - .clone() - .ok_or("missing config")?; let keyring_bytes = decode_browser_base64(&config.keyring)?; let keyring = Keyring::from_bytes(&keyring_bytes).map_err(|error| error.to_string())?; let chat_id = derive_chat_id(config.user_id, sender_id); let secret_id = format!("chat:{chat_id}:main"); let secret_request = CommunicationValue::new(CommunicationType::GetChatSecret) + .with_id(connection.next_request_id().await?) .add_typed_default(DataType::UserId, DataValue::Str(config.user_id.to_string())) .add_typed_default(DataType::ChatId, DataValue::Str(chat_id.clone())) .add_typed_default(DataType::SecretId, DataValue::Str(secret_id.clone())); let secret = connection + .mtp .request(&secret_request, None) .await .map_err(|error| error.to_string())?; @@ -532,7 +765,7 @@ async fn notify_message( } let version = secret .get_data(DataType::VersionNumber) - .as_number() + .and_then(DataValue::as_number) .ok_or("missing secret version")?; let encrypted_secret = secret .get_bytes(DataType::EncryptedSecret) @@ -560,11 +793,14 @@ async fn notify_message( .decrypt(&ciphertext, b"") .map_err(|error| error.to_string())?; - let user_request = CommunicationValue::new(CommunicationType::GetUserData).add_typed_default( - DataType::UserId, - DataValue::UnsignedNumber(sender_id as u128), - ); + let user_request = CommunicationValue::new(CommunicationType::GetUserData) + .with_id(connection.next_request_id().await?) + .add_typed_default( + DataType::UserId, + DataValue::UnsignedNumber(sender_id as u128), + ); let user = connection + .mtp .request(&user_request, None) .await .map_err(|error| error.to_string())?; @@ -652,17 +888,17 @@ fn container_value(value: &DataValue, field: DataType) -> Option<&DataValue> { value.get_field(id) } -fn json_to_frame( - type_name: &str, - data: Value, - id: Option, -) -> Result { - let comm_type = CommunicationType::from_name(type_name) - .ok_or_else(|| format!("unknown communication type: {type_name}"))?; - let mut frame = CommunicationValue::new(comm_type); - if let Some(id) = id { - frame = frame.with_id(id); - } +fn container_value_by_name<'a>(value: &'a DataValue, field: &str) -> Option<&'a DataValue> { + container_value(value, DataType::from_name(field)?) +} + +fn communication_type(name: &str) -> Result { + CommunicationType::from_name(name).ok_or_else(|| format!("unknown communication type: {name}")) +} + +fn json_to_frame(type_name: &str, data: Value, id: u32) -> Result { + let comm_type = communication_type(type_name)?; + let mut frame = CommunicationValue::new(comm_type).with_id(id); let Value::Object(fields) = data else { return Err("MTP request data must be an object".into()); }; @@ -737,8 +973,8 @@ fn is_bytes_field(field: &str) -> bool { fn frame_to_json(frame: &CommunicationValue) -> Result { let mut result = Map::new(); - if frame.get_id() != 0 { - result.insert("id".into(), Value::from(frame.get_id())); + if let Some(id) = frame.id() { + result.insert("id".into(), Value::from(id)); } result.insert( "type".into(), @@ -751,7 +987,10 @@ fn frame_to_json(frame: &CommunicationValue) -> Result { fn frame_data_to_json(frame: &CommunicationValue) -> Result { let map = frame.type_map().cloned().unwrap_or_else(TypeMap::latest); let mut result = Map::new(); - for (id, value) in frame.data() { + let entries = frame + .data() + .ok_or("MTP frame payload is not a data container")?; + for (id, value) in entries { let name = map .data_type_name(id.0) .ok_or_else(|| format!("unknown data type id: {}", id.0))?; @@ -801,16 +1040,15 @@ fn number_to_json(value: i128) -> Result { .map_err(|_| "number exceeds JSON range".into()) } -fn current_millis() -> u64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as u64 -} - #[cfg(test)] mod tests { - use super::{decode_browser_base64, decode_sdk_bytes}; + use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; + use serde_json::json; + + use super::{ + container_value_by_name, decode_browser_base64, decode_sdk_bytes, frame_to_json, + jittered_retry_delay, json_to_frame, prepare_initial_state_ack, RequestIdAllocator, + }; #[test] fn browser_base64_accepts_file_whitespace_and_missing_padding() { @@ -823,11 +1061,137 @@ mod tests { assert_eq!(decode_sdk_bytes("0x01:02-ff").unwrap(), [1, 2, 255]); assert_eq!(decode_sdk_bytes("AQI=").unwrap(), [1, 2]); } + + #[test] + fn request_ids_are_nonzero_and_monotonic() { + let ids = RequestIdAllocator::new(); + + assert_eq!(ids.next().unwrap(), 1); + assert_eq!(ids.next().unwrap(), 2); + } + + #[test] + fn retry_jitter_stays_within_policy_bounds() { + let delay = jittered_retry_delay(std::time::Duration::from_secs(10)); + + assert!(delay >= std::time::Duration::from_secs(8)); + assert!(delay <= std::time::Duration::from_secs(12)); + } + + #[test] + fn json_content_uses_content_wire_type() { + let frame = json_to_frame( + "MessageEdit", + json!({ + "Content": "ciphertext", + "ChatPartnerId": 42, + "SendTime": 10, + }), + 1, + ) + .unwrap(); + + assert_eq!( + frame + .get_data(DataType::Content) + .and_then(DataValue::as_str), + Some("ciphertext") + ); + } + + #[test] + fn nested_json_content_uses_content_wire_type() { + let frame = json_to_frame( + "MessageEdit", + json!({ + "Message": { "Content": "ciphertext" }, + }), + 1, + ) + .unwrap(); + let message = frame.get_data(DataType::Message).unwrap(); + + assert_eq!( + container_value_by_name(message, "Content").and_then(DataValue::as_str), + Some("ciphertext") + ); + } + + #[test] + fn content_is_exposed_to_frontend() { + let frame = CommunicationValue::new(CommunicationType::MessageEditLive) + .with_id(1) + .add_typed_default(DataType::Content, DataValue::Str("ciphertext".into())); + + let json = frame_to_json(&frame).unwrap(); + + assert_eq!(json["data"]["Content"], "ciphertext"); + } + + fn valid_initial_state() -> CommunicationValue { + CommunicationValue::new(communication_type("ClientStateSync").unwrap()) + .add_typed_default(DataType::SessionId, DataValue::UnsignedNumber(1)) + .add_typed_default(DataType::VersionNumber, DataValue::UnsignedNumber(0)) + .add_typed_default(DataType::CacheSchemaVersion, DataValue::UnsignedNumber(0)) + .add_typed_default(DataType::SyncMode, DataValue::Str("full".into())) + .add_typed_default(DataType::Contacts, DataValue::Array(vec![])) + .add_typed_default(DataType::Communities, DataValue::Array(vec![])) + .add_typed_default(DataType::Calls, DataValue::Array(vec![])) + .add_typed_default(DataType::Messages, DataValue::Array(vec![])) + } + + #[test] + fn valid_initial_state_is_prepared_before_ack() { + let ids = RequestIdAllocator::new(); + let (state, ack) = prepare_initial_state_ack(&valid_initial_state(), &ids).unwrap(); + + assert_eq!(state["SyncMode"], "full"); + assert_eq!(ack.get_type_name(), Some("ClientStateAck")); + assert_eq!(ack.id(), Some(1)); + } + + #[test] + fn malformed_initial_state_does_not_prepare_ack() { + let ids = RequestIdAllocator::new(); + let malformed = valid_initial_state() + .add_typed_default(DataType::SyncMode, DataValue::Str("invalid".into())); + + assert!(prepare_initial_state_ack(&malformed, &ids).is_err()); + assert_eq!( + ids.next().unwrap(), + 1, + "no acknowledgement ID was allocated" + ); + } + + #[test] + fn malformed_nested_initial_state_does_not_prepare_ack() { + let ids = RequestIdAllocator::new(); + let malformed = CommunicationValue::new(communication_type("ClientStateSync").unwrap()) + .add_typed_default(DataType::SessionId, DataValue::UnsignedNumber(1)) + .add_typed_default(DataType::VersionNumber, DataValue::UnsignedNumber(0)) + .add_typed_default(DataType::CacheSchemaVersion, DataValue::UnsignedNumber(0)) + .add_typed_default(DataType::SyncMode, DataValue::Str("full".into())) + .add_typed_default( + DataType::Contacts, + DataValue::Array(vec![DataValue::Container(vec![])]), + ) + .add_typed_default(DataType::Communities, DataValue::Array(vec![])) + .add_typed_default(DataType::Calls, DataValue::Array(vec![])) + .add_typed_default(DataType::Messages, DataValue::Array(vec![])); + + assert!(prepare_initial_state_ack(&malformed, &ids).is_err()); + assert_eq!( + ids.next().unwrap(), + 1, + "no acknowledgement ID was allocated" + ); + } } #[tauri::command] -pub async fn mtp_request(type_name: String, data: Value, id: Option) -> Result { - manager().request(&type_name, data, id).await +pub async fn mtp_request(type_name: String, data: Value) -> Result { + manager().request(&type_name, data).await } #[tauri::command] diff --git a/apps/tauri/src/qrCodeScanner.tsx b/apps/tauri/src/qrCodeScanner.tsx deleted file mode 100644 index fa3f3c7..0000000 --- a/apps/tauri/src/qrCodeScanner.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { - scan, - Format, - requestPermissions, -} from "@tauri-apps/plugin-barcode-scanner"; -import { Button } from "@methanium/ui"; - -import { toast } from "@tensamin/shared/log"; - -export default function QrCodeScanner({ - onData, -}: { - onData: (data: string) => void; -}) { - return ( - - ); -} diff --git a/apps/web/index.html b/apps/web/index.html index 01335eb..0c0644f 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -3,7 +3,10 @@ - + Tensamin diff --git a/apps/web/package.json b/apps/web/package.json index 15a4c00..a6b3951 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -8,7 +8,7 @@ "lint": "eslint src", "dev": "vite", "test": "vitest run --passWithNoTests", - "build": "pnpm run test && tsc -b && vite build", + "build": "pnpm --filter @tensamin/pwa build && pnpm run test && tsc -b && vite build", "preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .." }, "dependencies": { @@ -18,6 +18,7 @@ "@tanstack/react-router": "^1.170.21", "@tanstack/react-virtual": "^3.14.9", "@tauri-apps/api": "^2.11.1", + "@tensamin/pwa": "workspace:*", "@tensamin/cache": "workspace:*", "@tensamin/call": "workspace:*", "@tensamin/chat": "workspace:*", @@ -54,6 +55,7 @@ "esbuild": "^0.28.1", "eslint": "^10.8.0", "globals": "^17.9.0", + "mtp": "*", "typescript": "~6.0.3", "typescript-eslint": "^8.66.0", "vite": "^8.2.1" diff --git a/apps/web/src/components/navbar.tsx b/apps/web/src/components/navbar.tsx index 2f1c115..122f188 100644 --- a/apps/web/src/components/navbar.tsx +++ b/apps/web/src/components/navbar.tsx @@ -52,6 +52,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) { return (
diff --git a/apps/web/src/components/screens/login/form.tsx b/apps/web/src/components/screens/login/form.tsx index 72db8f7..c9fa041 100644 --- a/apps/web/src/components/screens/login/form.tsx +++ b/apps/web/src/components/screens/login/form.tsx @@ -13,8 +13,11 @@ import { useState, } from "react"; import { z } from "zod"; -import { invoke, isTauri } from "@tauri-apps/api/core"; -import QrCodeScanner from "@tensamin/tauri/qrCodeScanner"; +import { subscribeTuFileLaunch } from "@tensamin/pwa/runtime"; +import { + parseTuFileContent, + persistMtpCredentials, +} from "@tensamin/storage/credentials"; import { useNavigate } from "@tanstack/react-router"; const fetchedUser = z.object({ @@ -35,50 +38,6 @@ const formSchema = z.object({ mtp_keyring: z.string().min(1).max(92), }); -/** - * Parses a .tu file payload into credentials. - * @param rawFileContent UTF-8 file content from an uploaded .tu file. - * @returns Parsed user id and private key credentials. - */ -function parseTuFileContent(rawFileContent: string): { - userId: number; - privateKey: string; - domain: string | null; -} { - if (rawFileContent.trim().length === 0) { - throw new Error("File is empty"); - } else if (!rawFileContent.includes("::")) { - throw new Error("Invalid file"); - } else if (rawFileContent.split("::").length !== 2) { - throw new Error("Invalid file"); - } - - const left = rawFileContent.split("::")[0]; - const right = rawFileContent.split("::")[1]; - - if (left.length === 0) { - throw new Error("Invalid file"); - } else if (right.length === 0) { - throw new Error("Invalid file"); - } else if (isNaN(Number(left)) && !left.includes("@")) { - throw new Error("Invalid file"); - } - - const [userIdString, privateKeyValue] = rawFileContent.split("::"); - const privateKey = privateKeyValue.trim(); - const userId = isNaN(Number(userIdString)) - ? Number(userIdString.split("@")[0]) - : Number(userIdString); - - const domain = userIdString.includes("@") ? userIdString.split("@")[1] : null; - - if (!userId || !privateKey) { - throw new Error("Invalid file"); - } - - return { userId, privateKey, domain }; -} - export default function Form() { const isMobile = useIsMobile(); const uploadRef = useRef(null); @@ -92,27 +51,12 @@ export default function Form() { if (loginPendingRef.current) return false; loginPendingRef.current = true; try { - if (domain) await save("omega_url", `https://${domain}/`); - if (isTauri()) { - const [omegaUrl, forcedOmikronUrl, forcedOmikronPublicKey] = - await Promise.all([ - domain ? `https://${domain}/` : load("omega_url"), - load("forced_omikron_url"), - load("forced_omikron_public_key"), - ]); - await invoke("mtp_store_credentials", { - config: { - userId, - keyring: privateKey, - omegaUrl, - forcedOmikronUrl, - forcedOmikronPublicKey, - }, - }); - } - await save("mtp_keyring", privateKey, { secure: true }); - await save("session_id", Date.now()); - await save("user_id", userId); + await persistMtpCredentials({ + storage: { load, save }, + userId, + keyring: privateKey, + domain, + }); await navigate({ to: "/", replace: true }); return true; } finally { @@ -143,6 +87,11 @@ export default function Form() { [persistLogin], ); + useEffect( + () => subscribeTuFileLaunch(processDroppedFile), + [processDroppedFile], + ); + // Handle .tu files const handleFileInputChange = useCallback( async (event: ChangeEvent): Promise => { @@ -281,32 +230,10 @@ export default function Form() { return (
- {isTauri() && isMobile ? ( - <> - { - if (!data.startsWith("tensamin://tu::")) { - toast("error", "Invalid QR code"); - return; - } - - const decoded = data.replace("tensamin://tu::", ""); - - try { - const { userId, privateKey, domain } = - parseTuFileContent(decoded); - - await persistLogin(userId, privateKey, domain); - } catch (error) { - log(0, "login", "red", error); - toast("error", "Failed to parse QR code data"); - } - }} - /> - - + {isMobile ? ( + ) : (
uploadRef.current?.click()} diff --git a/apps/web/src/components/sidebar.tsx b/apps/web/src/components/sidebar.tsx index 6c3de28..d56a4f1 100644 --- a/apps/web/src/components/sidebar.tsx +++ b/apps/web/src/components/sidebar.tsx @@ -27,7 +27,6 @@ import { SelectContent, SelectItem, } from "@methanium/ui"; -import { isTauri } from "@tauri-apps/api/core"; import { useIsMobile } from "@methanium/ui"; import { MobileNavbar } from "./navbar"; @@ -194,11 +193,7 @@ export default function Sidebar() { const content = ( <> - +
-
- +
+ + diff --git a/apps/web/src/routes/app/layout.tsx b/apps/web/src/routes/app/layout.tsx index 9a46805..9f165b4 100644 --- a/apps/web/src/routes/app/layout.tsx +++ b/apps/web/src/routes/app/layout.tsx @@ -19,6 +19,7 @@ export default function Layout({ children }: { children: ReactNode }) {
&2 - exit 1 - '' - else - packageDeb ( - builtins.path { - path = localDebPath; - name = debArtifact; - } - ); + }; + desktopItem = pkgs.makeDesktopItem { + name = "tensamin"; + desktopName = "Tensamin"; + exec = "tensamin"; + icon = "tensamin"; + startupWMClass = "Tensamin"; + categories = [ "Network" ]; + }; + defaultPackage = pkgs.stdenv.mkDerivation (finalAttrs: { + pname = "tensamin"; + inherit version; + src = self; + + pnpmDeps = pkgs.fetchPnpmDeps { + inherit (finalAttrs) pname version src; + inherit pnpm; + fetcherVersion = 4; + hash = "sha256-imP3MTr1YLc28Z9n617m0Wt/6vPirFznzoriKzojlEg="; + }; + + nativeBuildInputs = with pkgs; [ + copyDesktopItems + makeWrapper + nodejs_22 + pnpm + pnpmConfigHook + cargo + lld + rustc + wasm-pack + wasmBindgenCli + binaryen + ]; + + env.ELECTRON_SKIP_BINARY_DOWNLOAD = 1; + + postPatch = '' + rm -rf mtp-type-maps + ln -s ${mtpTypeMaps} mtp-type-maps + + node -e ' + const fs = require("fs"); + const path = "apps/electron/package.json"; + const pkg = JSON.parse(fs.readFileSync(path, "utf8")); + pkg.version = "${version}"; + fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n"); + ' + ''; + + buildPhase = '' + runHook preBuild + + mkdir -p "$HOME/.cargo" + substitute ${mtpCargoDeps}/.cargo/config.toml "$HOME/.cargo/config.toml" \ + --replace-fail @vendor@ ${mtpCargoDeps} + + pnpm run copy-licenses + pnpm run build:packages + pnpm run build:web + pnpm --dir apps/tauri run gen-icons + pnpm --dir apps/electron run build + pnpm --dir apps/electron exec electron-builder --dir --linux --publish never \ + --config.electronDist=${electron.dist} \ + --config.electronVersion=${electron.version} + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + mkdir -p "$out/lib/tensamin" "$out/bin" + cp -r apps/electron/release/linux-unpacked/. "$out/lib/tensamin/" + + makeWrapper "$out/lib/tensamin/tensamin" "$out/bin/tensamin" \ + --prefix LD_LIBRARY_PATH : "${pkgs.lib.makeLibraryPath electronRuntimeLibs}" + + install -Dm644 apps/electron/build/icons/icon.png \ + "$out/share/icons/hicolor/512x512/apps/tensamin.png" + + runHook postInstall + ''; + + desktopItems = [ desktopItem ]; + + meta = { + description = "Tensamin desktop client"; + homepage = "https://git.methanium.net/tensamin/client"; + mainProgram = "tensamin"; + platforms = pkgs.lib.platforms.linux; + }; + }); in { default = defaultPackage; tensamin = defaultPackage; electron = defaultPackage; - localPathForDev = localPathPackage; } ); diff --git a/licenses/@tauri-apps_plugin-barcode-scanner@2.4.5/LICENSE.spdx b/licenses/@tauri-apps_plugin-barcode-scanner@2.4.5/LICENSE.spdx deleted file mode 100644 index cdd0df5..0000000 --- a/licenses/@tauri-apps_plugin-barcode-scanner@2.4.5/LICENSE.spdx +++ /dev/null @@ -1,20 +0,0 @@ -SPDXVersion: SPDX-2.1 -DataLicense: CC0-1.0 -PackageName: tauri -DataFormat: SPDXRef-1 -PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy -PackageHomePage: https://tauri.app -PackageLicenseDeclared: Apache-2.0 -PackageLicenseDeclared: MIT -PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy -PackageSummary: Tauri is a rust project that enables developers to make secure -and small desktop applications using a web frontend. - -PackageComment: The package includes the following libraries; see -Relationship information. - -Created: 2019-05-20T09:00:00Z -PackageDownloadLocation: git://github.com/tauri-apps/tauri -PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git -PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git -Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/licenses/THIRD_PARTY_NOTICES.md b/licenses/THIRD_PARTY_NOTICES.md index 2094186..3d61ec4 100644 --- a/licenses/THIRD_PARTY_NOTICES.md +++ b/licenses/THIRD_PARTY_NOTICES.md @@ -117,15 +117,6 @@ Generated from pnpm-lock.yaml and installed packages in workspace node_modules f - Folder: `licenses/@tauri-apps_cli@2.11.4` - Source package dir: `apps/tauri/node_modules/@tauri-apps/cli` -## @tauri-apps/plugin-barcode-scanner@2.4.5 - -- License: MIT OR Apache-2.0 -- Repository: https://github.com/tauri-apps/plugins-workspace -- Description: Scan QR codes, EAN-13 and other kinds of barcodes on Android and iOS -- Included files: LICENSE.spdx -- Folder: `licenses/@tauri-apps_plugin-barcode-scanner@2.4.5` -- Source package dir: `apps/tauri/node_modules/@tauri-apps/plugin-barcode-scanner` - ## @tauri-apps/plugin-deep-link@2.4.9 - License: MIT OR Apache-2.0 diff --git a/licenses/sbom.cyclonedx.json b/licenses/sbom.cyclonedx.json index ebb655c..2fca23f 100644 --- a/licenses/sbom.cyclonedx.json +++ b/licenses/sbom.cyclonedx.json @@ -414,37 +414,6 @@ } ] }, - { - "type": "library", - "bomRef": "pkg:npm/%40tauri-apps/plugin-barcode-scanner@2.4.5", - "name": "@tauri-apps/plugin-barcode-scanner", - "version": "2.4.5", - "purl": "pkg:npm/%40tauri-apps/plugin-barcode-scanner@2.4.5", - "description": "Scan QR codes, EAN-13 and other kinds of barcodes on Android and iOS", - "licenses": [ - { - "license": { - "name": "MIT OR Apache-2.0" - } - } - ], - "externalReferences": [ - { - "type": "vcs", - "url": "https://github.com/tauri-apps/plugins-workspace" - } - ], - "properties": [ - { - "name": "local:licenseFolder", - "value": "licenses/@tauri-apps_plugin-barcode-scanner@2.4.5" - }, - { - "name": "local:sourcePackageDir", - "value": "apps/tauri/node_modules/@tauri-apps/plugin-barcode-scanner" - } - ] - }, { "type": "library", "bomRef": "pkg:npm/%40tauri-apps/plugin-deep-link@2.4.9", @@ -1754,4 +1723,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/licenses/third-party-credits.json b/licenses/third-party-credits.json index 5ea2d3d..6188b40 100644 --- a/licenses/third-party-credits.json +++ b/licenses/third-party-credits.json @@ -160,19 +160,6 @@ "licenseFolder": "licenses/@tauri-apps_cli@2.11.4", "sourcePackageDir": "apps/tauri/node_modules/@tauri-apps/cli" }, - { - "name": "@tauri-apps/plugin-barcode-scanner", - "version": "2.4.5", - "license": "MIT OR Apache-2.0", - "homepage": null, - "repository": "https://github.com/tauri-apps/plugins-workspace", - "description": "Scan QR codes, EAN-13 and other kinds of barcodes on Android and iOS", - "files": [ - "LICENSE.spdx" - ], - "licenseFolder": "licenses/@tauri-apps_plugin-barcode-scanner@2.4.5", - "sourcePackageDir": "apps/tauri/node_modules/@tauri-apps/plugin-barcode-scanner" - }, { "name": "@tauri-apps/plugin-deep-link", "version": "2.4.9", @@ -678,4 +665,4 @@ "sourcePackageDir": "packages/call/node_modules/zustand" } ] -} \ No newline at end of file +} diff --git a/mtp-type-maps b/mtp-type-maps index a297dcc..f4e45aa 160000 --- a/mtp-type-maps +++ b/mtp-type-maps @@ -1 +1 @@ -Subproject commit a297dcce60bc6e84696c6a16f5fd510beb2ca643 +Subproject commit f4e45aa3a3ad0e3c3a257f66857b904a1af7901c diff --git a/package.json b/package.json index 05856f5..1975028 100644 --- a/package.json +++ b/package.json @@ -25,26 +25,27 @@ "start-adb:mobile": "cd apps/tauri && pnpm run start-adb:mobile", "dev:desktop": "cd apps/electron && pnpm run dev", "build:desktop": "cd apps/electron && pnpm run package", - "delete:mobile": "nix develop .#tauri --command node apps/tauri/scripts/delete-mobile.ts" + "delete:mobile": "nix develop .#tauri --command node apps/tauri/scripts/delete-mobile.ts", + "update-submodules": "git submodule update --remote --force --recursive" }, "devDependencies": { "@eslint/js": "^10.0.1", - "@types/node": "^26.1.2", + "@types/node": "^26.2.0", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", - "@typescript-eslint/parser": "^8.66.0", - "eslint": "^10.8.0", + "@typescript-eslint/parser": "^8.67.0", + "eslint": "^10.8.1", "eslint-plugin-react-hooks": "^7.1.1", - "fallow": "^3.14.0", - "globals": "^17.9.0", + "fallow": "^3.17.0", + "globals": "^17.11.0", "prettier": "^3.9.6", "typescript": "^6.0.3", - "typescript-eslint": "^8.66.0", - "vitest": "^4.1.10" + "typescript-eslint": "^8.67.0", + "vitest": "^4.1.11" }, "dependencies": { - "@methanium/ui": "*", - "mtp": "*", - "sonner": "^2.0.7" + "@methanium/ui": "https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz", + "mtp": "https://git.methanium.net/methanium/mtp/releases/download/0.3.0-b331b9f6a3/mtp-0.3.0.tgz", + "sonner": "^2.0.8" } } diff --git a/packages/cache/package.json b/packages/cache/package.json index f8ef91b..2a69aa9 100644 --- a/packages/cache/package.json +++ b/packages/cache/package.json @@ -12,8 +12,7 @@ "scripts": { "format": "pnpm exec prettier --write .", "lint": "eslint src", - "test": "vitest run", - "build": "pnpm run test && tsc -p tsconfig.json --noEmit" + "build": "tsc -p tsconfig.json --noEmit" }, "dependencies": { "@tensamin/mtp": "workspace:*", diff --git a/packages/cache/src/helpers.test.ts b/packages/cache/src/helpers.test.ts deleted file mode 100644 index 035c285..0000000 --- a/packages/cache/src/helpers.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - replaceConversation, - selectConversationWindows, - trimMessages, -} from "./helpers"; -import type { CachedMessage, ConversationWindow } from "./schemas"; - -const message = (SendTime: number): CachedMessage => ({ - SenderId: 1, - SendTime, - Content: "Y2lwaGVydGV4dA==", - MessageState: "received", -}); -const window = (UserId: number, LastMessageAt: number): ConversationWindow => ({ - UserId, - LastMessageAt, - Messages: [], -}); - -describe("conversation cache helpers", () => { - it("selects the five most recent windows", () => { - const selected = selectConversationWindows( - [ - window(1, 1), - window(2, 6), - window(3, 3), - window(4, 4), - window(5, 5), - window(6, 2), - ], - 5, - ); - expect(selected.map(({ UserId }) => UserId)).toEqual([2, 5, 4, 3, 6]); - }); - - it("replaces only the matching conversation", () => { - expect( - replaceConversation([window(1, 1), window(2, 2)], window(1, 9)), - ).toEqual([window(1, 9), window(2, 2)]); - }); - - it("retains the newest messages in chronological order", () => { - expect( - trimMessages([message(2), message(3), message(1)], 2).map( - (item) => item.SendTime, - ), - ).toEqual([2, 3]); - }); -}); diff --git a/packages/cache/src/sync.tsx b/packages/cache/src/sync.tsx index 37076c1..8928e23 100644 --- a/packages/cache/src/sync.tsx +++ b/packages/cache/src/sync.tsx @@ -21,8 +21,7 @@ export function removeMissingContactSnapshots( } export default function CacheSync() { - const { addInterceptor, contextReady, freshContacts, subscribePush } = - useMTP(); + const { addInterceptor, contextReady, freshContacts, subscribe } = useMTP(); const { load } = useStorage(); const [accountId, setAccountId] = useState(0); const queueRef = useRef(Promise.resolve()); @@ -321,10 +320,19 @@ export default function CacheSync() { useEffect(() => { if (!accountId || !contextReady) return; - return subscribePush((message) => { + const handleMessage = (message: ProtocolMessage) => { void enqueue(() => synchronizePush(message)); - }); - }, [accountId, contextReady, enqueue, subscribePush, synchronizePush]); + }; + const unsubscribers = [ + subscribe("GetStates", handleMessage), + subscribe("MessageLive", handleMessage), + subscribe("MessageEditLive", handleMessage), + subscribe("MessageDeleteLive", handleMessage), + subscribe("MessageState", handleMessage), + subscribe("MessageReactionLive", handleMessage), + ]; + return () => unsubscribers.forEach((unsubscribe) => unsubscribe()); + }, [accountId, contextReady, enqueue, subscribe, synchronizePush]); return null; } diff --git a/packages/call/package.json b/packages/call/package.json index d4f7c10..7171f50 100644 --- a/packages/call/package.json +++ b/packages/call/package.json @@ -28,6 +28,7 @@ "deepfilternet3-noise-filter": "1.3.0", "livekit-client": "^2.21.0", "lucide-react": "^1.29.0", + "mtp": "*", "react": "^19.2.8", "react-dom": "^19.2.8", "recharts": "^3.10.1", diff --git a/packages/call/src/mediaShare/index.ts b/packages/call/src/mediaShare/index.ts index 6d2c47f..9706d4d 100644 --- a/packages/call/src/mediaShare/index.ts +++ b/packages/call/src/mediaShare/index.ts @@ -19,7 +19,6 @@ export function getMediaShareAdapter(): MediaShareAdapter { export type { MediaShareAdapter, - MediaShareCapabilities, MediaShareKind, MediaShareRequest, MediaShareSession, diff --git a/packages/call/src/store.tsx b/packages/call/src/store.tsx index 6f7f524..bbf3f4e 100644 --- a/packages/call/src/store.tsx +++ b/packages/call/src/store.tsx @@ -1223,7 +1223,7 @@ export const useCall = create<{ export function useInitializeCall() { const navigate = useNavigate(); const location = useLocation(); - const { send, subscribePush } = useMTP(); + const { send, subscribe } = useMTP(); const { load } = useStorage(); const { insertCall } = useSession(); const { get } = useUser(); @@ -1313,14 +1313,9 @@ export function useInitializeCall() { // listen to call invites useEffect(() => { - return subscribePush(async (message) => { - if (message.type !== "CallInvite") return; - - const { CallId, CallSecret, SenderId } = message.data as { - CallId: string; - CallSecret: ProtocolCallSecret; - SenderId: number; - }; + return subscribe("CallInvite", async ({ data }) => { + const { CallId, CallSecret, SenderId } = data; + if (!CallId || !CallSecret || !SenderId) return; if (SenderId === Number(await load("user_id"))) { return; @@ -1337,7 +1332,7 @@ export function useInitializeCall() { SenderId, ); }); - }, [load, subscribePush, showCallingScreen]); + }, [load, subscribe, showCallingScreen]); // get callId from url useEffect(() => { diff --git a/packages/chat/src/components/input.tsx b/packages/chat/src/components/input.tsx index 15c5c55..469c28a 100644 --- a/packages/chat/src/components/input.tsx +++ b/packages/chat/src/components/input.tsx @@ -2,6 +2,9 @@ import { Input, type InputController } from "@methanium/ui/markdown"; import { Card, CardHeader, + Drawer, + DrawerContent, + DrawerTrigger, Popover, PopoverContent, PopoverTrigger, @@ -331,42 +334,65 @@ export default function InputComponent({ /> - - ( - - )} - /> - event.stopPropagation()} - onWheelCapture={(event) => event.stopPropagation()} - style={{ - ...gifPopoverSize, - maxHeight: gifPopoverSize?.height, - }} - > -
+ +
+ +
+
+ +
+ { + void handleSubmit(url, true); + setGifPopoverOpen(false); + }} + /> +
+
+ + ) : ( + + ( + + )} /> - { - void handleSubmit(url, true); - setGifPopoverOpen(false); + event.stopPropagation()} + onWheelCapture={(event) => event.stopPropagation()} + style={{ + ...gifPopoverSize, + maxHeight: gifPopoverSize?.height, }} - /> - - + > +
+ { + void handleSubmit(url, true); + setGifPopoverOpen(false); + }} + /> + + + )}
diff --git a/packages/chat/src/components/media/media.tsx b/packages/chat/src/components/media/media.tsx index 02ee6ef..1072308 100644 --- a/packages/chat/src/components/media/media.tsx +++ b/packages/chat/src/components/media/media.tsx @@ -166,9 +166,9 @@ export default function Media({ -
+
{ - return subscribePush((message) => { - if (message.type === "MessageEditLive") { - if (!currentChatSecret) return; - - const rawData = message.data as { - ChatPartnerId: unknown; - SendTime: unknown; - Content: string; - }; - - const chatPartnerId = Number(rawData.ChatPartnerId); - const sendTime = Number(rawData.SendTime); - - if (!Number.isFinite(chatPartnerId) || !Number.isFinite(sendTime)) { - log( - 3, - "chat", - "yellow", - "Cancel message edit update due to invalid data", - ); - return; - } - - if (chatPartnerId !== userIdValue) { - log( - 3, - "chat", - "yellow", - "Cancel message edit update due to user ID mismatch", - { - expected: userIdValue, - received: chatPartnerId, - }, - ); - return; - } - - void decryptChatText(currentChatSecret, rawData.Content) - .then((content) => { - editMessage(sendTime, { Content: content, Edited: true }); - }) - .catch((err) => { - log(1, "chat", "red", "Failed to decrypt message edit", err, { - SendTime: sendTime, - }); - }); - return; - } - - if (message.type === "MessageReactionLive") { - const rawData = message.data as { - ChatPartnerId: unknown; - SendTime: unknown; - Reaction: string; - SenderId: unknown; - Accepted: boolean; - }; - const chatPartnerId = Number(rawData.ChatPartnerId); - const sendTime = Number(rawData.SendTime); - const senderId = Number(rawData.SenderId); - - if ( - chatPartnerId !== userIdValue || - !Number.isFinite(sendTime) || - !Number.isFinite(senderId) - ) { - return; - } - - applyLiveReaction( - sendTime, - rawData.Reaction, - senderId, - rawData.Accepted, - ); - return; - } - - if (message.type === "MessageDeleteLive") { - const data = message.data as { - ChatPartnerId: number; - SendTime: number; - }; - - if (data.ChatPartnerId !== userIdValue) return; - - removeMessage(data.SendTime); - return; - } - - if (message.type !== "MessageState") return; - - const rawData = message.data as { - ChatPartnerId: unknown; - SendTime: unknown; - MessageState: RawMessage["MessageState"]; - }; - - const nextState = { - ChatPartnerId: Number(rawData.ChatPartnerId), - SendTime: Number(rawData.SendTime), - MessageState: rawData.MessageState, - }; - - if ( - !Number.isFinite(nextState.ChatPartnerId) || - !Number.isFinite(nextState.SendTime) - ) { + const unsubscribeEdit = subscribe("MessageEditLive", ({ data }) => { + if (!currentChatSecret) return; + if (data.ChatPartnerId !== userIdValue) { log( 3, "chat", "yellow", - "Cancel message state update due to invalid data", + "Cancel message edit update due to user ID mismatch", + { + expected: userIdValue, + received: data.ChatPartnerId, + }, ); return; } - if (nextState.ChatPartnerId !== userIdValue) { + void decryptChatText(currentChatSecret, data.Content) + .then((content) => { + editMessage(data.SendTime, { Content: content, Edited: true }); + }) + .catch((err) => { + log(1, "chat", "red", "Failed to decrypt message edit", err, { + SendTime: data.SendTime, + }); + }); + }); + const unsubscribeReaction = subscribe("MessageReactionLive", ({ data }) => { + if (data.ChatPartnerId !== userIdValue) return; + applyLiveReaction( + data.SendTime, + data.Reaction, + data.SenderId, + data.Accepted, + ); + }); + const unsubscribeDelete = subscribe("MessageDeleteLive", ({ data }) => { + if (data.ChatPartnerId !== userIdValue) return; + removeMessage(data.SendTime); + }); + const unsubscribeState = subscribe("MessageState", ({ data }) => { + if (data.ChatPartnerId !== userIdValue) { log( 3, "chat", @@ -1037,22 +960,27 @@ export default function Provider({ children }: { children: ReactNode }) { "Cancel message state update due to user ID mismatch", { expected: userIdValue, - received: nextState.ChatPartnerId, + received: data.ChatPartnerId, }, ); return; } - - editMessage(nextState.SendTime, { - MessageState: nextState.MessageState, + editMessage(data.SendTime, { + MessageState: data.MessageState, }); }); + return () => { + unsubscribeEdit(); + unsubscribeReaction(); + unsubscribeDelete(); + unsubscribeState(); + }; }, [ currentChatSecret, applyLiveReaction, editMessage, removeMessage, - subscribePush, + subscribe, userIdValue, ]); diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 6477f6e..2b40fdf 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -12,9 +12,10 @@ "format": "pnpm exec prettier --write .", "lint": "eslint src", "test": "vitest run", - "build": "pnpm run test && tsc -p tsconfig.json --noEmit" + "build": "tsc -p tsconfig.json --noEmit" }, "dependencies": { + "mtp": "*", "react": "^19.2.8", "react-dom": "^19.2.8" } diff --git a/packages/crypto/src/callSecret.ts b/packages/crypto/src/callSecret.ts index 97ef318..556d131 100644 --- a/packages/crypto/src/callSecret.ts +++ b/packages/crypto/src/callSecret.ts @@ -13,11 +13,13 @@ export function deriveCallSecretId(callId: string): string { } export function ownKemPublicKeyFromKeyring(keyring: string): Uint8Array { - return crypto.keyringToKeys(keyring).kemPublicKey; + return crypto.keyringToKeys({ value: keyring, encoding: "base64" }) + .kemPublicKey; } export function kemPublicKeyFromPublicKeyBundle(publicKey: string): Uint8Array { - return crypto.publicKeyBundleToKeys(publicKey).kemPublicKey; + return crypto.publicKeyBundleToKeys({ value: publicKey, encoding: "base64" }) + .kemPublicKey; } export async function wrapCallSecret(args: { @@ -72,7 +74,10 @@ export async function unwrapCallSecret(args: { ); } - const ownKeys = crypto.keyringToKeys(args.keyring); + const ownKeys = crypto.keyringToKeys({ + value: args.keyring, + encoding: "base64", + }); const sharedSecret = crypto.decapsulate( ownKeys.kemSecretKey, args.kemCiphertext, diff --git a/packages/crypto/src/chatSecret.ts b/packages/crypto/src/chatSecret.ts index e6e836c..cbac59a 100644 --- a/packages/crypto/src/chatSecret.ts +++ b/packages/crypto/src/chatSecret.ts @@ -23,11 +23,13 @@ export function randomChatSecret(): Uint8Array { } export function ownKemPublicKeyFromKeyring(keyring: string): Uint8Array { - return crypto.keyringToKeys(keyring).kemPublicKey; + return crypto.keyringToKeys({ value: keyring, encoding: "base64" }) + .kemPublicKey; } export function kemPublicKeyFromPublicKeyBundle(publicKey: string): Uint8Array { - return crypto.publicKeyBundleToKeys(publicKey).kemPublicKey; + return crypto.publicKeyBundleToKeys({ value: publicKey, encoding: "base64" }) + .kemPublicKey; } export async function wrapChatSecret(args: { @@ -79,7 +81,10 @@ export async function unwrapChatSecret(args: { ); } - const ownKeys = crypto.keyringToKeys(args.keyring); + const ownKeys = crypto.keyringToKeys({ + value: args.keyring, + encoding: "base64", + }); const sharedSecret = crypto.decapsulate( ownKeys.kemSecretKey, args.kemCiphertext, diff --git a/packages/crypto/src/context.test.ts b/packages/crypto/src/context.test.ts deleted file mode 100644 index e6c2178..0000000 --- a/packages/crypto/src/context.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { describe, expect, test, vi } from "vitest"; - -vi.mock("mtp", () => ({ - crypto: {}, -})); - -import { createCryptoActions } from "./context"; - -/** - * Creates a rejected API getter used to verify initialization guards. - * @returns Null API reference. - */ -function getUninitializedApi(): null { - return null; -} - -describe("createCryptoActions", () => { - const textEncoder = new TextEncoder(); - const textDecoder = new TextDecoder(); - - test("throws when API is not initialized", async () => { - const actions = createCryptoActions(getUninitializedApi); - - let failed = false; - try { - await actions.encrypt("ab", new TextEncoder().encode("plain")); - } catch (error) { - failed = (error as Error).message.includes("API not initialized"); - } - - expect(failed).toBe(true); - }); - - test("delegates encrypt/decrypt/getSharedSecret to API reference", async () => { - const api = { - encrypt: async ( - secret: string, - input: Uint8Array, - ): Promise> => - textEncoder.encode(`${secret}:${textDecoder.decode(input)}`), - decrypt: async ( - secret: string, - input: Uint8Array, - ): Promise> => - textEncoder.encode(`${secret}|${textDecoder.decode(input)}`), - encryptText: async (secret: string, plaintext: string): Promise => - `${secret}:${plaintext}`, - decryptText: async ( - secret: string, - ciphertext: string, - ): Promise => `${secret}|${ciphertext}`, - }; - - const actions = createCryptoActions(() => api); - - expect( - textDecoder.decode(await actions.encrypt("s", textEncoder.encode("p"))), - ).toBe("s:p"); - expect( - textDecoder.decode(await actions.decrypt("s", textEncoder.encode("c"))), - ).toBe("s|c"); - expect(await actions.encryptText("s", "p")).toBe("s:p"); - expect(await actions.decryptText("s", "c")).toBe("s|c"); - }); -}); diff --git a/packages/markdown/package.json b/packages/markdown/package.json new file mode 100644 index 0000000..247d715 --- /dev/null +++ b/packages/markdown/package.json @@ -0,0 +1,30 @@ +{ + "name": "@tensamin/markdown", + "private": true, + "version": "0.0.0", + "type": "module", + "exports": { + "./text": "./src/text.tsx", + "./input": "./src/input.tsx", + "./emoji": "./src/emoji.tsx" + }, + "scripts": { + "format": "pnpm exec prettier --write .", + "lint": "eslint src", + "build": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "@codemirror/autocomplete": "^6.20.3", + "@codemirror/commands": "^6.10.4", + "@codemirror/lang-markdown": "^6.5.2", + "@codemirror/language": "^6.12.4", + "@codemirror/state": "^6.7.1", + "@codemirror/view": "^6.43.8", + "@methanium/ui": "*", + "@twemoji/api": "^17.0.3", + "emojibase-data": "^17.0.0", + "lucide-react": "^1.30.0", + "react": "^19.2.8", + "react-dom": "^19.2.8" + } +} diff --git a/packages/markdown/src/emoji.tsx b/packages/markdown/src/emoji.tsx new file mode 100644 index 0000000..c0eb397 --- /dev/null +++ b/packages/markdown/src/emoji.tsx @@ -0,0 +1,50 @@ +import twemoji from "@twemoji/api"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@methanium/ui"; +import { resolveEmoji } from "./emojiData"; + +export { + emojis, + findEmojiShortcodes, + normalizeShortcode, + resolveEmoji, + searchEmojis, +} from "./emojiData"; +export type { EmojiDefinition } from "./emojiData"; + +export function getEmojiUrl(shortcode: string): string | undefined { + const emoji = resolveEmoji(shortcode); + return emoji ? `${twemoji.base}svg/${emoji.hexcode}.svg` : undefined; +} + +export default function Emoji({ + className = "h-6 w-6", + shortcode, + tooltip = true, +}: { + className?: string; + shortcode: string; + tooltip?: boolean; +}) { + const emoji = resolveEmoji(shortcode); + if (!emoji) return {shortcode}; + + const image = ( + {emoji.shortcode} + ); + + if (!tooltip) return image; + + return ( + + + {emoji.shortcode} + + ); +} diff --git a/packages/markdown/src/emojiData.ts b/packages/markdown/src/emojiData.ts new file mode 100644 index 0000000..158b5b0 --- /dev/null +++ b/packages/markdown/src/emojiData.ts @@ -0,0 +1,94 @@ +import shortcodeData from "emojibase-data/en/shortcodes/joypixels.json"; + +export type EmojiDefinition = { + aliases: readonly string[]; + hexcode: string; + name: string; + shortcode: string; +}; + +function normalizeName(value: string) { + return value + .trim() + .replace(/^:+|:+$/g, "") + .toLowerCase(); +} + +export const emojis: readonly EmojiDefinition[] = Object.entries( + shortcodeData as Record, +).map(([hexcode, value]) => { + const aliases = Array.isArray(value) ? value : [value]; + const name = aliases[0]; + + return { + aliases, + hexcode: hexcode.toLowerCase().replaceAll("_", "-"), + name, + shortcode: `:${name}:`, + }; +}); + +const emojiByName = new Map(); +for (const emoji of emojis) { + for (const alias of emoji.aliases) { + emojiByName.set(normalizeName(alias), emoji); + } +} + +export function resolveEmoji(value: string): EmojiDefinition | undefined { + return emojiByName.get(normalizeName(value)); +} + +export function normalizeShortcode(value: string): string | undefined { + return resolveEmoji(value)?.shortcode; +} + +export function findEmojiShortcodes(value: string) { + const matches: Array<{ + emoji: EmojiDefinition; + from: number; + to: number; + }> = []; + let searchFrom = 0; + + while (searchFrom < value.length) { + const from = value.indexOf(":", searchFrom); + if (from === -1) break; + + const candidate = value.slice(from).match(/^:([a-z0-9_+-]+):/i); + if (!candidate) { + searchFrom = from + 1; + continue; + } + + const emoji = resolveEmoji(candidate[1]); + if (!emoji) { + // The closing colon may also open the next valid shortcode. + searchFrom = from + candidate[0].length - 1; + continue; + } + + const to = from + candidate[0].length; + matches.push({ emoji, from, to }); + searchFrom = to; + } + + return matches; +} + +export function searchEmojis(query: string): EmojiDefinition[] { + const normalizedQuery = normalizeName(query); + if (!normalizedQuery) return [...emojis]; + + return emojis + .map((emoji) => { + const names = emoji.aliases.map(normalizeName); + const exact = names.includes(normalizedQuery); + const prefix = names.some((name) => name.startsWith(normalizedQuery)); + const contains = names.some((name) => name.includes(normalizedQuery)); + return { emoji, rank: exact ? 0 : prefix ? 1 : contains ? 2 : 3 }; + }) + .filter(({ rank }) => rank < 3) + .sort((a, b) => a.rank - b.rank || a.emoji.name.localeCompare(b.emoji.name)) + .map(({ emoji }) => emoji); +} diff --git a/packages/markdown/src/input.tsx b/packages/markdown/src/input.tsx new file mode 100644 index 0000000..e16c84d --- /dev/null +++ b/packages/markdown/src/input.tsx @@ -0,0 +1,850 @@ +import { markdown } from "@codemirror/lang-markdown"; +import { syntaxTree } from "@codemirror/language"; +import { + acceptCompletion, + autocompletion, + completionStatus, + pickedCompletion, + startCompletion, + type Completion, + type CompletionContext, + type CompletionResult, +} from "@codemirror/autocomplete"; +import { + EditorState, + EditorSelection, + Annotation, + Compartment, + Prec, + Transaction, + type Extension, + type Range, + type SelectionRange, +} from "@codemirror/state"; +import { + Decoration, + EditorView, + keymap, + placeholder, + ViewPlugin, + WidgetType, + type DecorationSet, + type KeyBinding, + type ViewUpdate, +} from "@codemirror/view"; +import { + defaultKeymap, + history, + historyKeymap, + indentWithTab, +} from "@codemirror/commands"; +import { useEffect, useRef } from "react"; +import type { CSSProperties } from "react"; +import { createRoot, type Root } from "react-dom/client"; + +import { collectInlineRanges, ensureMarkdownStyles } from "./markdown"; +import Emoji, { + findEmojiShortcodes, + getEmojiUrl, + resolveEmoji, + searchEmojis, +} from "./emoji"; + +export const MAX_RENDERED_EMOJI_OPTIONS = 100; + +export type InputController = { + focus: () => void; + hasFocus: () => boolean; + insertText: (text: string) => void; +}; + +export type InputProps = { + ref?: HTMLDivElement; + placeholder?: string; + value: string; + setValue: (value: string) => void; + onSubmit?: () => void; + invertEnterBehavior?: boolean; + styled?: boolean; + fontSize?: CSSProperties["fontSize"]; + paddingX?: CSSProperties["padding"]; + paddingY?: CSSProperties["padding"]; + className?: string; + emojiFrequencies?: Readonly>; + onEmojiSelect?: (shortcode: string) => void; + autoFocus?: boolean; + onControllerChange?: (controller: InputController | null) => void; +}; + +function toCssLength(value: CSSProperties["padding"]): string | undefined { + if (value === undefined) { + return undefined; + } + + return typeof value === "number" ? `${value}px` : value; +} + +function toCssPadding( + vertical: CSSProperties["padding"], + horizontal: CSSProperties["padding"], + styled: boolean, +): string { + const defaultVertical = styled ? "0.25rem" : "0"; + const defaultHorizontal = styled ? "0.625rem" : "0"; + + return `${toCssLength(vertical) ?? defaultVertical} ${toCssLength(horizontal) ?? defaultHorizontal}`; +} + +const hiddenTokenDecoration = Decoration.mark({ class: "tm-md-hidden-token" }); +const strongDecoration = Decoration.mark({ class: "tm-md-strong" }); +const emDecoration = Decoration.mark({ class: "tm-md-em" }); +const delDecoration = Decoration.mark({ class: "tm-md-del" }); +const codeDecoration = Decoration.mark({ class: "tm-md-code" }); +const linkDecoration = Decoration.mark({ class: "tm-md-link" }); +const codeLineDecoration = Decoration.line({ class: "tm-md-code-line" }); +const externalValueSync = Annotation.define(); +const widgetRoots = new WeakMap(); + +type EmojiRange = { + from: number; + shortcode: string; + to: number; + url: string; +}; + +class EmojiWidget extends WidgetType { + readonly shortcode: string; + readonly url: string; + + constructor(shortcode: string, url: string) { + super(); + this.shortcode = shortcode; + this.url = url; + } + + eq(other: EmojiWidget) { + return other.shortcode === this.shortcode && other.url === this.url; + } + + toDOM() { + const container = document.createElement("span"); + const root = createRoot(container); + root.render( + , + ); + widgetRoots.set(container, root); + return container; + } + + destroy(dom: HTMLElement) { + widgetRoots.get(dom)?.unmount(); + widgetRoots.delete(dom); + } + + ignoreEvent() { + return true; + } +} + +function codeRanges(state: EditorState) { + const ranges: Array<{ from: number; to: number }> = []; + + syntaxTree(state).iterate({ + enter(node) { + if ( + node.name === "InlineCode" || + node.name === "FencedCode" || + node.name === "CodeBlock" + ) { + ranges.push({ from: node.from, to: node.to }); + return false; + } + }, + }); + + return ranges; +} + +export function findEmojiRanges(state: EditorState): EmojiRange[] { + const document = state.doc.toString(); + const excluded = codeRanges(state); + const ranges: EmojiRange[] = []; + + for (const match of findEmojiShortcodes(document)) { + const { from, to } = match; + const inCode = excluded.some((range) => from < range.to && to > range.from); + const emoji = inCode ? undefined : match.emoji; + const url = emoji ? getEmojiUrl(emoji.shortcode) : undefined; + + if (emoji && url) { + ranges.push({ from, shortcode: emoji.shortcode, to, url }); + } + } + + return ranges; +} + +class EmojiPluginValue { + decorations: DecorationSet; + ranges: EmojiRange[]; + + constructor(view: EditorView) { + this.ranges = findEmojiRanges(view.state); + this.decorations = this.buildDecorations(); + } + + update(update: ViewUpdate) { + if ( + update.docChanged || + syntaxTree(update.startState) !== syntaxTree(update.state) + ) { + this.ranges = findEmojiRanges(update.state); + this.decorations = this.buildDecorations(); + } + } + + private buildDecorations() { + return Decoration.set( + this.ranges.map((range) => + Decoration.replace({ + inclusive: false, + widget: new EmojiWidget(range.shortcode, range.url), + }).range(range.from, range.to), + ), + true, + ); + } +} + +const emojiDecorations = ViewPlugin.fromClass(EmojiPluginValue, { + decorations: (instance) => instance.decorations, + provide: (plugin) => + EditorView.atomicRanges.of( + (view) => view.plugin(plugin)?.decorations ?? Decoration.none, + ), +}); + +function deleteEmoji(view: EditorView, direction: "backward" | "forward") { + const ranges = view.plugin(emojiDecorations)?.ranges ?? []; + const deletions: Array<{ from: number; to: number }> = []; + + for (const selection of view.state.selection.ranges) { + if (selection.empty) { + const emoji = ranges.find((range) => + direction === "backward" + ? selection.from > range.from && selection.from <= range.to + : selection.from >= range.from && selection.from < range.to, + ); + if (emoji) deletions.push({ from: emoji.from, to: emoji.to }); + continue; + } + + let from = selection.from; + let to = selection.to; + let changed = false; + + for (const emoji of ranges) { + if (from < emoji.to && to > emoji.from) { + from = Math.min(from, emoji.from); + to = Math.max(to, emoji.to); + changed = true; + } + } + + if (changed) deletions.push({ from, to }); + } + + if (deletions.length === 0) return false; + + const merged = deletions + .sort((a, b) => a.from - b.from) + .reduce>((result, deletion) => { + const previous = result.at(-1); + if (previous && deletion.from <= previous.to) { + previous.to = Math.max(previous.to, deletion.to); + } else { + result.push({ ...deletion }); + } + return result; + }, []); + + view.dispatch({ + changes: merged.map((range) => ({ from: range.from, to: range.to })), + selection: EditorSelection.cursor(merged[0].from), + scrollIntoView: true, + userEvent: direction === "backward" ? "delete.backward" : "delete.forward", + }); + return true; +} + +/** + * Builds markdown styling decorations every time the document or cursor selection changes. + * Token delimiters are hidden unless the cursor is currently intersecting that token range. + */ +const markdownDecorations = ViewPlugin.fromClass( + class { + decorations: DecorationSet; + + constructor(view: EditorView) { + this.decorations = buildDecorations(view); + } + + update(update: ViewUpdate) { + if (update.docChanged || update.selectionSet || update.viewportChanged) { + this.decorations = buildDecorations(update.view); + } + } + }, + { + decorations: (instance: { decorations: DecorationSet }) => + instance.decorations, + }, +); + +/** + * Executes Input. + * @param props Parameter props. + * @returns unknown. + */ +export default function Input(props: InputProps) { + ensureMarkdownStyles(); + + const shellClassName = props.styled + ? "min-h-8 w-full min-w-0 rounded-lg border border-input bg-transparent text-base transition-colors outline-none placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40" + : ""; + + const elementRef = useRef(null); + const viewRef = useRef(undefined); + const setValueRef = useRef(props.setValue); + const onSubmitRef = useRef(props.onSubmit); + const onEmojiSelectRef = useRef( + props.onEmojiSelect, + ); + const invertEnterBehaviorRef = useRef(Boolean(props.invertEnterBehavior)); + const completionCompartmentRef = useRef(null); + completionCompartmentRef.current ??= new Compartment(); + const completionCompartment = completionCompartmentRef.current; + + useEffect(() => { + setValueRef.current = props.setValue; + onSubmitRef.current = props.onSubmit; + onEmojiSelectRef.current = props.onEmojiSelect; + invertEnterBehaviorRef.current = Boolean(props.invertEnterBehavior); + }, [ + props.onEmojiSelect, + props.onSubmit, + props.invertEnterBehavior, + props.setValue, + ]); + + useEffect(() => { + if (!elementRef.current) return; + + const state = EditorState.create({ + doc: props.value, + extensions: createEditorExtensions( + (value) => { + setValueRef.current(value); + }, + () => props.placeholder, + () => invertEnterBehaviorRef.current, + () => onSubmitRef.current?.(), + completionCompartment, + props.emojiFrequencies, + (shortcode) => onEmojiSelectRef.current?.(shortcode), + ), + }); + + viewRef.current = new EditorView({ + state, + parent: elementRef.current, + }); + props.onControllerChange?.({ + focus: () => viewRef.current?.contentDOM.focus({ preventScroll: true }), + hasFocus: () => viewRef.current?.hasFocus ?? false, + insertText: (text) => { + const editor = viewRef.current; + if (!editor) return; + editor.dispatch({ + ...editor.state.replaceSelection(text), + annotations: Transaction.userEvent.of("input.type"), + scrollIntoView: true, + }); + }, + }); + if (props.autoFocus) viewRef.current.focus(); + + return () => { + props.onControllerChange?.(null); + viewRef.current?.destroy(); + viewRef.current = undefined; + }; + // Run once to initialize/destroy the editor instance. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + const editor = viewRef.current; + if (!editor) return; + + const next = props.value; + const current = editor.state.doc.toString(); + + if (next === current) return; + + editor.dispatch({ + changes: { + from: 0, + to: current.length, + insert: next, + }, + annotations: [ + externalValueSync.of(true), + Transaction.addToHistory.of(false), + ], + filter: false, + }); + }, [props.value]); + + useEffect(() => { + const editor = viewRef.current; + const compartment = completionCompartmentRef.current; + if (!editor || !compartment) return; + + const wasActive = completionStatus(editor.state) === "active"; + editor.dispatch({ + effects: compartment.reconfigure( + createEmojiAutocomplete(props.emojiFrequencies, (shortcode) => + onEmojiSelectRef.current?.(shortcode), + ), + ), + }); + if (wasActive) startCompletion(editor); + }, [props.emojiFrequencies]); + + return ( +
+ ); +} + +/** + * Executes createEditorExtensions. + * @param onChange Parameter onChange. + * @param getPlaceholder Parameter getPlaceholder. + * @param getInvertEnterBehavior Parameter getInvertEnterBehavior. + * @param onSubmit Parameter onSubmit. + * @returns Extension[]. + */ +function createEditorExtensions( + onChange: (value: string) => void, + getPlaceholder: () => string | undefined, + getInvertEnterBehavior: () => boolean, + onSubmit: () => void, + completionCompartment: Compartment, + emojiFrequencies: Readonly> | undefined, + onEmojiSelect: (shortcode: string) => void, +): Extension[] { + const editorKeymap = [ + ...defaultKeymap, + ...historyKeymap, + indentWithTab, + ] as unknown as readonly KeyBinding[]; + + const customEnterKeymap = keymap.of([ + { + key: "Shift-Enter", + run: (view) => { + if (completionStatus(view.state) === "active") { + return acceptCompletion(view); + } + if (!getInvertEnterBehavior()) { + return false; + } + + onSubmit(); + return true; + }, + }, + { + key: "Enter", + run: (view) => { + if (completionStatus(view.state) === "active") { + return acceptCompletion(view); + } + if (getInvertEnterBehavior()) { + return false; + } + + onSubmit(); + return true; + }, + }, + ]); + const completionTabKeymap = keymap.of([ + { + key: "Tab", + run: (view) => + completionStatus(view.state) === "active" + ? acceptCompletion(view) + : false, + }, + ]); + const emojiDeletionKeymap = keymap.of([ + { + key: "Backspace", + run: (view) => deleteEmoji(view, "backward"), + }, + { + key: "Delete", + run: (view) => deleteEmoji(view, "forward"), + }, + ]); + + return [ + history(), + markdown(), + completionCompartment.of( + createEmojiAutocomplete(emojiFrequencies, onEmojiSelect), + ), + emojiDecorations, + keymap.of(editorKeymap), + Prec.highest(completionTabKeymap), + Prec.highest(emojiDeletionKeymap), + Prec.highest(customEnterKeymap), + EditorView.lineWrapping, + placeholder(getPlaceholder() ?? ""), + EditorView.updateListener.of((update: ViewUpdate) => { + if (!update.docChanged) return; + if ( + update.transactions.some( + (transaction) => transaction.annotation(externalValueSync) === true, + ) + ) { + return; + } + onChange(update.state.doc.toString()); + }), + EditorView.theme({ + "&": { + fontSize: "inherit", + }, + "&.cm-editor": { + width: "100%", + }, + }), + EditorView.editorAttributes.of({ + class: "tm-md-editor", + spellcheck: "true", + "aria-label": "Markdown input", + }), + markdownDecorations, + ]; +} + +function createEmojiAutocomplete( + frequencies: Readonly> | undefined, + onEmojiSelect: (shortcode: string) => void, +) { + return autocompletion({ + activateOnTyping: true, + addToOptions: [ + { + position: 20, + render(completion) { + const container = document.createElement("span"); + createRoot(container).render( + , + ); + return container; + }, + }, + ], + maxRenderedOptions: MAX_RENDERED_EMOJI_OPTIONS, + override: [createEmojiCompletionSource(frequencies, onEmojiSelect)], + }); +} + +function normalizedFrequencies( + frequencies: Readonly> | undefined, +) { + const normalized = new Map(); + for (const [value, frequency] of Object.entries(frequencies ?? {})) { + const shortcode = resolveEmoji(value)?.shortcode; + if (!shortcode || !Number.isFinite(frequency) || frequency <= 0) continue; + normalized.set(shortcode, (normalized.get(shortcode) ?? 0) + frequency); + } + return normalized; +} + +export function createEmojiCompletionSource( + frequencies?: Readonly>, + onEmojiSelect: (shortcode: string) => void = () => undefined, +) { + const normalized = normalizedFrequencies(frequencies); + const maxFrequency = Math.max(0, ...normalized.values()); + + return (context: CompletionContext): CompletionResult | null => { + const token = context.matchBefore(/:[a-z0-9_+-]*$/i); + if (!token) return null; + if ( + codeRanges(context.state).some( + (range) => token.from < range.to && token.to > range.from, + ) + ) { + return null; + } + + const characterBefore = context.state.sliceDoc( + Math.max(0, token.from - 1), + token.from, + ); + if (characterBefore && /[a-z0-9_]/i.test(characterBefore)) return null; + + const query = token.text.slice(1).toLowerCase(); + const options: Completion[] = searchEmojis(query) + .map((emoji) => { + const aliases = emoji.aliases.map((alias) => alias.toLowerCase()); + const matchedAlias = + aliases.find((alias) => alias === query) ?? + aliases.find((alias) => alias.startsWith(query)) ?? + aliases.find((alias) => alias.includes(query)) ?? + emoji.name; + const relevance = !query + ? 0 + : matchedAlias === query + ? 80 + : matchedAlias.startsWith(query) + ? 40 + : 0; + const frequency = normalized.get(emoji.shortcode) ?? 0; + const usage = + maxFrequency > 0 + ? (15 * Math.log1p(frequency)) / Math.log1p(maxFrequency) + : 0; + + return { + apply(view, completion, from, to) { + view.dispatch({ + annotations: pickedCompletion.of(completion), + changes: { from, insert: `${emoji.shortcode} `, to }, + selection: EditorSelection.cursor( + from + emoji.shortcode.length + 1, + ), + }); + onEmojiSelect(emoji.shortcode); + }, + boost: relevance + usage, + displayLabel: emoji.shortcode, + label: `:${matchedAlias}:`, + type: "text", + frequency, + relevance, + } satisfies Completion & { frequency: number; relevance: number }; + }) + .sort( + (a, b) => + b.relevance - a.relevance || + b.frequency - a.frequency || + (a.displayLabel ?? a.label).localeCompare(b.displayLabel ?? b.label), + ); + + return { + from: token.from, + options, + validFor: /^:[a-z0-9_+-]*$/i, + }; + }; +} + +export const emojiCompletionSource = createEmojiCompletionSource(); + +/** + * Executes buildDecorations. + * @param view Parameter view. + * @returns DecorationSet. + */ +function buildDecorations(view: EditorView): DecorationSet { + const builder: Range[] = []; + const selections = view.state.selection.ranges.map( + (range: SelectionRange) => ({ + from: range.from, + to: range.to, + }), + ); + + let codeFenceOpen = false; + + for ( + let lineNumber = 1; + lineNumber <= view.state.doc.lines; + lineNumber += 1 + ) { + const line = view.state.doc.line(lineNumber); + const text = line.text; + const lineFrom = line.from; + const trimmed = text.trim(); + + const fence = text.match(/^```\s*([^`]*)$/); + if (fence) { + const ticksStart = lineFrom + text.indexOf("```"); + const ticksEnd = ticksStart + 3; + addHiddenToken(builder, selections, { from: ticksStart, to: ticksEnd }); + if (trimmed.length > 3) { + addHiddenToken(builder, selections, { + from: ticksEnd, + to: line.to, + }); + } + codeFenceOpen = !codeFenceOpen; + continue; + } + + if (codeFenceOpen) { + builder.push(codeLineDecoration.range(lineFrom)); + continue; + } + + const heading = text.match(/^(#{1,6})\s+/); + if (heading) { + const markerLength = heading[0].length; + addHiddenToken(builder, selections, { + from: lineFrom, + to: lineFrom + markerLength, + }); + + const level = heading[1].length; + const headingClass = Decoration.mark({ + class: `tm-md-heading tm-md-h${String(level)}`, + }); + const contentFrom = lineFrom + markerLength; + if (contentFrom < line.to) { + builder.push(headingClass.range(contentFrom, line.to)); + } + } + + const quote = text.match(/^>\s?/); + if (quote) { + addHiddenToken(builder, selections, { + from: lineFrom, + to: lineFrom + quote[0].length, + }); + } + + const unordered = text.match(/^(\s*)([-+*])\s+(?:\[( |x|X)\]\s+)?/); + if (unordered) { + const markerStart = lineFrom + unordered[1].length; + const markerEnd = markerStart + unordered[2].length + 1; + addHiddenToken(builder, selections, { from: markerStart, to: markerEnd }); + + const checkbox = unordered[0].match(/\[( |x|X)\]\s+$/); + if (checkbox) { + const checkboxStart = lineFrom + unordered[0].lastIndexOf("["); + addHiddenToken(builder, selections, { + from: checkboxStart, + to: checkboxStart + checkbox[0].length, + }); + } + } + + const ordered = text.match(/^(\s*)(\d+\.)\s+/); + if (ordered) { + const markerStart = lineFrom + ordered[1].length; + addHiddenToken(builder, selections, { + from: markerStart, + to: markerStart + ordered[2].length + 1, + }); + } + + if (/^(?:\*\s*){3,}$|^(?:-\s*){3,}$|^(?:_\s*){3,}$/.test(trimmed)) { + if (lineFrom < line.to) { + builder.push( + Decoration.mark({ class: "tm-md-hr" }).range(lineFrom, line.to), + ); + } + continue; + } + + const tableSeparator = /^\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?$/.test( + text, + ); + if (tableSeparator) { + if (lineFrom < line.to) { + builder.push( + Decoration.mark({ class: "tm-md-del" }).range(lineFrom, line.to), + ); + } + continue; + } + + const { styleRanges, tokenRanges } = collectInlineRanges(text, lineFrom); + + for (const range of styleRanges) { + if (range.from >= range.to) continue; + + if (range.className === "tm-md-strong") { + builder.push(strongDecoration.range(range.from, range.to)); + } else if (range.className === "tm-md-em") { + builder.push(emDecoration.range(range.from, range.to)); + } else if (range.className === "tm-md-del") { + builder.push(delDecoration.range(range.from, range.to)); + } else if (range.className === "tm-md-code") { + builder.push(codeDecoration.range(range.from, range.to)); + } else if (range.className === "tm-md-link") { + builder.push(linkDecoration.range(range.from, range.to)); + } + } + + for (const token of tokenRanges) { + addHiddenToken(builder, selections, token); + } + } + + return Decoration.set(builder, true); +} + +/** + * Keeps markdown syntax visible only when user selection intersects the token. + * This preserves cursor predictability and cross-token selection while still hiding syntax during reading. + */ +function addHiddenToken( + builder: Range[], + selections: ReadonlyArray<{ from: number; to: number }>, + token: { + from: number; + to: number; + }, +): void { + if (token.from >= token.to) return; + + const overlapsSelection = selections.some((selection) => { + const selectionFrom = Math.min(selection.from, selection.to); + const selectionTo = Math.max(selection.from, selection.to); + + if (selectionFrom === selectionTo) { + return selectionFrom >= token.from && selectionFrom <= token.to; + } + + return selectionFrom < token.to && selectionTo > token.from; + }); + + if (overlapsSelection) return; + builder.push(hiddenTokenDecoration.range(token.from, token.to)); +} diff --git a/packages/markdown/src/markdown.tsx b/packages/markdown/src/markdown.tsx new file mode 100644 index 0000000..f8684c4 --- /dev/null +++ b/packages/markdown/src/markdown.tsx @@ -0,0 +1,815 @@ +import { + Fragment, + useEffect, + useRef, + useState, + type ReactElement, + type ReactNode, +} from "react"; +import Emoji from "./emoji"; +import { findEmojiShortcodes } from "./emojiData"; +import { Check } from "lucide-react"; + +type InlineNode = + | { type: "text"; value: string } + | { type: "emoji"; shortcode: string } + | { type: "strong"; value: string } + | { type: "em"; value: string } + | { type: "del"; value: string } + | { type: "code"; value: string } + | { type: "link"; label: string; href: string } + | { type: "image"; alt: string; src: string }; + +type InlineDecorationRange = { + from: number; + to: number; + className: string; +}; + +type InlineTokenRange = { + from: number; + to: number; +}; + +type ListItem = { + text: string; + checked: boolean | null; +}; + +type TableBlock = { + type: "table"; + headers: string[]; + rows: string[][]; +}; + +type MarkdownBlock = + | { + type: "paragraph"; + text: string; + } + | { + type: "heading"; + level: number; + text: string; + } + | { + type: "hr"; + } + | { + type: "blockquote"; + text: string; + } + | { + type: "code"; + language: string; + code: string; + } + | { + type: "list"; + ordered: boolean; + items: ListItem[]; + } + | TableBlock; + +const INLINE_TOKEN_REGEX = + /!\[([^\]]*)\]\(([^)\s]+(?:\s+"[^"]*")?)\)|\[([^\]]+)\]\(([^)\s]+(?:\s+"[^"]*")?)\)|`([^`\n]+)`|~~([^~\n]+)~~|\*\*([^*\n]+)\*\*|__([^_\n]+)__|\*([^*\n]+)\*|(? + + + ); +} + +function CopyableCode({ + block = false, + language, + value, +}: { + block?: boolean; + language?: string; + value: string; +}) { + const [copied, setCopied] = useState(false); + const copiedTimer = useRef | undefined>( + undefined, + ); + + useEffect( + () => () => { + clearTimeout(copiedTimer.current); + }, + [], + ); + + async function copy() { + await navigator.clipboard.writeText(value); + setCopied(true); + clearTimeout(copiedTimer.current); + copiedTimer.current = setTimeout(() => setCopied(false), 1200); + } + + const code = ( + void copy()} + onKeyDown={(event) => { + if (event.key !== "Enter" && event.key !== " ") return; + event.preventDefault(); + void copy(); + }} + > + {value} + + ); + + if (block) { + return ( +
+
{code}
+ +
+ ); + } + + return ( + <> + {code} + + + ); +} + +/** + * Executes parseInlineNodes. + * @param input Parameter input. + * @returns InlineNode[]. + */ +function parseInlineNodes(input: string): InlineNode[] { + const nodes: InlineNode[] = []; + + let cursor = 0; + let match = INLINE_TOKEN_REGEX.exec(input); + + while (match) { + const index = match.index; + const raw = match[0]; + + if (index > cursor) { + nodes.push(...parseEmojiText(input.slice(cursor, index))); + } + + if (match[1] !== undefined && match[2] !== undefined) { + nodes.push({ type: "image", alt: match[1], src: normalizeUrl(match[2]) }); + } else if (match[3] !== undefined && match[4] !== undefined) { + nodes.push({ + type: "link", + label: match[3], + href: normalizeUrl(match[4]), + }); + } else if (match[5] !== undefined) { + nodes.push({ type: "code", value: match[5] }); + } else if (match[6] !== undefined) { + nodes.push({ type: "del", value: match[6] }); + } else if (match[7] !== undefined || match[8] !== undefined) { + nodes.push({ type: "strong", value: match[7] ?? match[8] ?? "" }); + } else if (match[9] !== undefined || match[10] !== undefined) { + nodes.push({ type: "em", value: match[9] ?? match[10] ?? "" }); + } else { + nodes.push(...parseEmojiText(raw)); + } + + cursor = index + raw.length; + match = INLINE_TOKEN_REGEX.exec(input); + } + + if (cursor < input.length) { + nodes.push(...parseEmojiText(input.slice(cursor))); + } + + INLINE_TOKEN_REGEX.lastIndex = 0; + return nodes; +} + +function parseEmojiText(input: string): InlineNode[] { + const nodes: InlineNode[] = []; + let cursor = 0; + + for (const match of findEmojiShortcodes(input)) { + if (match.from > cursor) { + nodes.push({ type: "text", value: input.slice(cursor, match.from) }); + } + + nodes.push({ type: "emoji", shortcode: match.emoji.shortcode }); + cursor = match.to; + } + + if (cursor < input.length) { + nodes.push({ type: "text", value: input.slice(cursor) }); + } + + return nodes; +} + +/** + * Executes collectInlineRanges. + * @param input Parameter input. + * @param offset Parameter offset. + * @returns { + styleRanges: InlineDecorationRange[]; + tokenRanges: InlineTokenRange[]; +}. + */ +export function collectInlineRanges( + input: string, + offset = 0, +): { + styleRanges: InlineDecorationRange[]; + tokenRanges: InlineTokenRange[]; +} { + const styleRanges: InlineDecorationRange[] = []; + const tokenRanges: InlineTokenRange[] = []; + + let match = INLINE_TOKEN_REGEX.exec(input); + + while (match) { + const raw = match[0]; + const start = offset + match.index; + const end = start + raw.length; + + if (match[1] !== undefined && match[2] !== undefined) { + const openLength = 2; + const closeLength = raw.endsWith(")") ? 1 : 0; + + const imageEnd = start + openLength + match[1].length; + tokenRanges.push({ from: start, to: start + openLength }); + tokenRanges.push({ from: imageEnd, to: imageEnd + 1 }); + + const srcStart = imageEnd + 1; + const srcEnd = end - closeLength; + tokenRanges.push({ from: srcStart, to: srcStart + 1 }); + tokenRanges.push({ from: srcEnd, to: srcEnd + closeLength }); + } else if (match[3] !== undefined && match[4] !== undefined) { + const label = match[3]; + const labelStart = start + 1; + const labelEnd = labelStart + label.length; + + tokenRanges.push({ from: start, to: start + 1 }); + tokenRanges.push({ from: labelEnd, to: labelEnd + 1 }); + tokenRanges.push({ from: labelEnd + 1, to: labelEnd + 2 }); + tokenRanges.push({ from: end - 1, to: end }); + + styleRanges.push({ + from: labelStart, + to: labelEnd, + className: "tm-md-link", + }); + } else if (match[5] !== undefined) { + const codeStart = start + 1; + const codeEnd = end - 1; + + tokenRanges.push({ from: start, to: start + 1 }); + tokenRanges.push({ from: end - 1, to: end }); + styleRanges.push({ + from: codeStart, + to: codeEnd, + className: "tm-md-code", + }); + } else if (match[6] !== undefined) { + const contentStart = start + 2; + const contentEnd = end - 2; + + tokenRanges.push({ from: start, to: start + 2 }); + tokenRanges.push({ from: end - 2, to: end }); + styleRanges.push({ + from: contentStart, + to: contentEnd, + className: "tm-md-del", + }); + } else if (match[7] !== undefined || match[8] !== undefined) { + const contentStart = start + 2; + const contentEnd = end - 2; + + tokenRanges.push({ from: start, to: start + 2 }); + tokenRanges.push({ from: end - 2, to: end }); + styleRanges.push({ + from: contentStart, + to: contentEnd, + className: "tm-md-strong", + }); + } else if (match[9] !== undefined || match[10] !== undefined) { + const contentStart = start + 1; + const contentEnd = end - 1; + + tokenRanges.push({ from: start, to: start + 1 }); + tokenRanges.push({ from: end - 1, to: end }); + styleRanges.push({ + from: contentStart, + to: contentEnd, + className: "tm-md-em", + }); + } + + match = INLINE_TOKEN_REGEX.exec(input); + } + + INLINE_TOKEN_REGEX.lastIndex = 0; + return { styleRanges, tokenRanges }; +} + +/** + * Executes parseMarkdownBlocks. + * @param markdown Parameter markdown. + * @returns MarkdownBlock[]. + */ +export function parseMarkdownBlocks(markdown: string): MarkdownBlock[] { + const lines = markdown.replace(/\r\n/g, "\n").split("\n"); + const blocks: MarkdownBlock[] = []; + + let index = 0; + + while (index < lines.length) { + const line = lines[index]; + + if (!line.trim()) { + index += 1; + continue; + } + + const codeFence = line.match(/^```\s*([^`]*)$/); + if (codeFence) { + const language = (codeFence[1] ?? "").trim(); + const codeLines: string[] = []; + index += 1; + + while (index < lines.length && !/^```\s*$/.test(lines[index])) { + codeLines.push(lines[index]); + index += 1; + } + + if (index < lines.length) { + index += 1; + } + + blocks.push({ type: "code", language, code: codeLines.join("\n") }); + continue; + } + + if (/^(?:\*\s*){3,}$|^(?:-\s*){3,}$|^(?:_\s*){3,}$/.test(line.trim())) { + blocks.push({ type: "hr" }); + index += 1; + continue; + } + + const heading = line.match(/^(#{1,6})\s+(.+)$/); + if (heading) { + blocks.push({ + type: "heading", + level: heading[1].length, + text: heading[2], + }); + index += 1; + continue; + } + + const quote = line.match(/^>\s?(.*)$/); + if (quote) { + const quoteLines: string[] = [quote[1]]; + index += 1; + + while (index < lines.length) { + const next = lines[index].match(/^>\s?(.*)$/); + if (!next) break; + quoteLines.push(next[1]); + index += 1; + } + + blocks.push({ type: "blockquote", text: quoteLines.join("\n") }); + continue; + } + + const tableCandidate = readTable(lines, index); + if (tableCandidate) { + blocks.push(tableCandidate.block); + index = tableCandidate.nextIndex; + continue; + } + + const unordered = line.match(/^\s*[-*+]\s+(.*)$/); + const ordered = line.match(/^\s*\d+\.\s+(.*)$/); + if (unordered || ordered) { + const orderedList = Boolean(ordered); + const items: ListItem[] = []; + + while (index < lines.length) { + const current = lines[index]; + const match = orderedList + ? current.match(/^\s*\d+\.\s+(.*)$/) + : current.match(/^\s*[-*+]\s+(.*)$/); + + if (!match) break; + + const task = match[1].match(/^\[( |x|X)\]\s+(.*)$/); + if (task) { + items.push({ + text: task[2], + checked: task[1].toLowerCase() === "x", + }); + } else { + items.push({ text: match[1], checked: null }); + } + + index += 1; + } + + blocks.push({ type: "list", ordered: orderedList, items }); + continue; + } + + const paragraphLines = [line]; + index += 1; + + while ( + index < lines.length && + lines[index].trim() && + !/^(#{1,6})\s+/.test(lines[index]) && + !/^```\s*/.test(lines[index]) && + !/^>\s?/.test(lines[index]) && + !/^\s*[-*+]\s+/.test(lines[index]) && + !/^\s*\d+\.\s+/.test(lines[index]) && + !/^(?:\*\s*){3,}$|^(?:-\s*){3,}$|^(?:_\s*){3,}$/.test(lines[index].trim()) + ) { + paragraphLines.push(lines[index]); + index += 1; + } + + blocks.push({ type: "paragraph", text: paragraphLines.join("\n") }); + } + + return blocks; +} + +/** + * Executes renderInline. + * @param nodes Parameter nodes. + * @returns React.ReactNode[]. + */ +function renderInline(nodes: InlineNode[]): ReactNode[] { + return nodes.map((node, index) => { + if (node.type === "text") { + return node.value; + } + + if (node.type === "emoji") { + return ( + + ); + } + + if (node.type === "strong") { + return ( + + {renderInline(parseEmojiText(node.value))} + + ); + } + + if (node.type === "em") { + return ( + + {renderInline(parseEmojiText(node.value))} + + ); + } + + if (node.type === "del") { + return ( + + {renderInline(parseEmojiText(node.value))} + + ); + } + + if (node.type === "code") { + return ; + } + + if (node.type === "link") { + return ( + + {renderInline(parseEmojiText(node.label))} + + ); + } + + return ( + {node.alt} + ); + }); +} + +/** + * Executes renderBlocks. + * @param blocks Parameter blocks. + * @returns React.ReactElement. + */ +export function renderBlocks(blocks: MarkdownBlock[]): ReactElement { + return ( + <> + {blocks.map((block, blockIndex) => { + if (block.type === "heading") { + const className = `tm-md-heading tm-md-h${String(block.level)}`; + if (block.level === 1) + return ( +

+ {renderInline(parseInlineNodes(block.text))} +

+ ); + if (block.level === 2) + return ( +

+ {renderInline(parseInlineNodes(block.text))} +

+ ); + if (block.level === 3) + return ( +

+ {renderInline(parseInlineNodes(block.text))} +

+ ); + if (block.level === 4) + return ( +

+ {renderInline(parseInlineNodes(block.text))} +

+ ); + if (block.level === 5) + return ( +
+ {renderInline(parseInlineNodes(block.text))} +
+ ); + return ( +
+ {renderInline(parseInlineNodes(block.text))} +
+ ); + } + + if (block.type === "blockquote") { + return ( +
+ {block.text.split("\n").map((line, lineIndex) => ( +

{renderInline(parseInlineNodes(line))}

+ ))} +
+ ); + } + + if (block.type === "code") { + return ( + + ); + } + + if (block.type === "list") { + const Tag = block.ordered ? "ol" : "ul"; + return ( + + {block.items.map((item, itemIndex) => ( +
  • + {item.checked !== null ? ( + + ) : null} + {renderInline(parseInlineNodes(item.text))} +
  • + ))} +
    + ); + } + + if (block.type === "table") { + return ( +
    + + + + {block.headers.map((header, headerIndex) => ( + + ))} + + + + {block.rows.map((row, rowIndex) => ( + + {row.map((cell, cellIndex) => ( + + ))} + + ))} + +
    + {renderInline(parseInlineNodes(header))} +
    + {renderInline(parseInlineNodes(cell))} +
    +
    + ); + } + + if (block.type === "hr") { + return
    ; + } + + return ( +

    + {block.text.split("\n").map((line, lineIndex) => ( + + {lineIndex > 0 ?
    : null} + {renderInline(parseInlineNodes(line))} +
    + ))} +

    + ); + })} + + ); +} + +/** + * Executes normalizeUrl. + * @param input Parameter input. + * @returns string. + */ +function normalizeUrl(input: string): string { + const value = input.trim(); + if (/^(https?:|mailto:|tel:|\/)/i.test(value)) { + return value; + } + + return "#"; +} + +/** + * Executes splitTableRow. + * @param row Parameter row. + * @returns string[]. + */ +function splitTableRow(row: string): string[] { + const cleaned = row.trim().replace(/^\|/, "").replace(/\|$/, ""); + return cleaned.split("|").map((cell) => cell.trim()); +} + +/** + * Executes readTable. + * @param lines Parameter lines. + * @param index Parameter index. + * @returns { block: TableBlock; nextIndex: number } | null. + */ +function readTable( + lines: string[], + index: number, +): { block: TableBlock; nextIndex: number } | null { + const header = lines[index] ?? ""; + const separator = lines[index + 1] ?? ""; + + if (!header.includes("|") || !separator.includes("|")) { + return null; + } + + const separatorCells = splitTableRow(separator); + const isSeparator = separatorCells.every((cell) => /^:?-{3,}:?$/.test(cell)); + if (!isSeparator) { + return null; + } + + const headers = splitTableRow(header); + const rows: string[][] = []; + let cursor = index + 2; + + while (cursor < lines.length && lines[cursor].includes("|")) { + rows.push(splitTableRow(lines[cursor])); + cursor += 1; + } + + return { + block: { type: "table", headers, rows }, + nextIndex: cursor, + }; +} + +const markdownStyles = ` +.tm-md-root { color: var(--foreground); line-height: 1.65; font-size: 1rem; } +.tm-md-heading { margin: 0.2rem 0 0.35rem; font-weight: 700; line-height: 1.25; } +.tm-md-h1 { font-size: 1.65rem; } +.tm-md-h2 { font-size: 1.45rem; } +.tm-md-h3 { font-size: 1.25rem; } +.tm-md-h4 { font-size: 1.1rem; } +.tm-md-h5 { font-size: 1rem; } +.tm-md-h6 { font-size: 0.95rem; opacity: 0.9; } +.tm-md-blockquote { margin: 0.45rem 0; padding-left: 0.75rem; opacity: 0.95; } +.tm-md-blockquote p { margin: 0.2rem 0; } +.tm-md-pre { margin: 0.45rem 0; padding: 0.65rem 0.75rem; border-radius: 0.5rem; background: var(--muted); overflow-x: auto; } +.tm-md-code, .tm-md-codeblock { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.87em; cursor: pointer; } +.tm-md-code { padding: 0.08rem 0.32rem; border: 1px solid var(--border); border-radius: 0.28rem; background: var(--muted); } +.tm-md-codeblock { display: block; } +.tm-md-code:focus-visible, .tm-md-codeblock:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; } +.tm-md-strong { font-weight: 700; } +.tm-md-em { font-style: italic; } +.tm-md-del { text-decoration: line-through; } +.tm-md-link { color: var(--primary); text-decoration: underline; text-underline-offset: 0.14rem; } +.tm-md-image { display: block; max-width: 100%; border-radius: 0.4rem; margin: 0.5rem 0; } +.tm-md-emoji { display: inline-block; width: 1.15em; height: 1.15em; vertical-align: -0.18em; } +.tm-md-ul, .tm-md-ol { margin: 0.3rem 0 0.35rem 1.2rem; padding: 0; } +.tm-md-li { margin: 0.2rem 0; } +.tm-md-checkbox { margin-right: 0.5rem; vertical-align: middle; } +.tm-md-table-wrap { overflow-x: auto; margin: 0.45rem 0; } +.tm-md-table { border-collapse: collapse; width: 100%; min-width: 16rem; } +.tm-md-table th, .tm-md-table td { padding: 0.4rem 0.5rem; text-align: left; } +.tm-md-table th { background: var(--muted); font-weight: 600; } +.tm-md-hr { margin: 0.55rem 0; } + +.cm-editor.tm-md-editor { border-radius: inherit; background: transparent; caret-color: var(--foreground); } +.cm-editor.tm-md-editor.cm-focused { outline: none; box-shadow: none; } +.cm-editor.tm-md-editor .cm-scroller { font-family: inherit; line-height: 1.55; max-height: 30vh; overflow-y: auto; overflow-x: hidden; } +.cm-editor.tm-md-editor .cm-content { caret-color: var(--foreground); } +.cm-editor.tm-md-editor .cm-content { padding: var(--tm-md-content-padding, 0.25rem 0.625rem); min-height: 2rem; } +.cm-editor.tm-md-editor .cm-line { padding: 0; color: var(--foreground); } +.cm-editor.tm-md-editor .tm-md-editor-emoji { display: inline-block; width: 1.15em; height: 1.15em; vertical-align: -0.18em; object-fit: contain; pointer-events: none; } +.cm-editor.tm-md-editor .tm-md-hidden-token { color: transparent; opacity: 0; font-size: inherit; } +.cm-editor.tm-md-editor .tm-md-code-line { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; background: var(--muted); border-radius: 0.3rem; } +.cm-tooltip.cm-tooltip-autocomplete { min-width: 18rem; max-width: min(26rem, calc(100vw - 1rem)); overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius); background: var(--popover); color: var(--popover-foreground); box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); font-family: "Public Sans Variable", sans-serif; font-size: 0.875rem; } +.cm-editor.tm-md-editor .cm-tooltip.cm-tooltip-autocomplete > ul { max-height: min(20rem, 45vh); padding: 0.25rem; font-family: "Public Sans Variable", sans-serif; scrollbar-width: thin; scrollbar-color: var(--border) transparent; } +.cm-tooltip.cm-tooltip-autocomplete > ul::-webkit-scrollbar { width: 6px; height: 6px; } +.cm-tooltip.cm-tooltip-autocomplete > ul::-webkit-scrollbar-track { background: transparent; } +.cm-tooltip.cm-tooltip-autocomplete > ul::-webkit-scrollbar-thumb { border-radius: 9999px; background: var(--border); } +.cm-tooltip.cm-tooltip-autocomplete > ul > li { display: flex; min-height: 2.25rem; align-items: center; border-radius: calc(var(--radius) * 0.8); padding: 0.3rem 0.5rem; color: var(--popover-foreground); } +.cm-tooltip.cm-tooltip-autocomplete > ul > li:hover, +.cm-tooltip.cm-tooltip-autocomplete > ul > li[aria-selected] { background: var(--accent); color: var(--accent-foreground); } +.cm-tooltip.cm-tooltip-autocomplete .cm-completionIcon { display: none; } +.cm-tooltip.cm-tooltip-autocomplete .cm-completionLabel { overflow: hidden; text-overflow: ellipsis; } +.cm-tooltip.cm-tooltip-autocomplete .cm-completionMatchedText { color: inherit; text-decoration: none; font-weight: 600; } +.cm-tooltip-autocomplete .tm-md-completion-emoji { display: inline-block; width: 1.35rem; height: 1.35rem; flex: 0 0 auto; margin-right: 0.5rem; vertical-align: middle; } +`; + +/** + * Executes ensureMarkdownStyles. + * @param none This function has no parameters. + * @returns void. + */ +export function ensureMarkdownStyles(): void { + if (typeof document === "undefined") return; + + const styleId = "tensamin-markdown-styles"; + let style = document.getElementById(styleId) as HTMLStyleElement | null; + + if (!style) { + style = document.createElement("style"); + style.id = styleId; + document.head.appendChild(style); + } + + if (style.textContent !== markdownStyles) { + style.textContent = markdownStyles; + } +} diff --git a/packages/markdown/src/text.tsx b/packages/markdown/src/text.tsx new file mode 100644 index 0000000..ca65fe0 --- /dev/null +++ b/packages/markdown/src/text.tsx @@ -0,0 +1,30 @@ +import { useMemo, type CSSProperties } from "react"; + +import { + ensureMarkdownStyles, + parseMarkdownBlocks, + renderBlocks, +} from "./markdown"; + +export type TextProps = { + value: string; + fontSize?: CSSProperties["fontSize"]; +}; + +/** + * Executes Text. + * @param props Parameter props. + * @returns unknown. + */ +export default function Text(props: TextProps) { + ensureMarkdownStyles(); + + const blocks = useMemo(() => parseMarkdownBlocks(props.value), [props.value]); + const renderedBlocks = useMemo(() => renderBlocks(blocks), [blocks]); + + return ( +
    + {renderedBlocks} +
    + ); +} diff --git a/packages/markdown/todo.md b/packages/markdown/todo.md new file mode 100644 index 0000000..b59da98 --- /dev/null +++ b/packages/markdown/todo.md @@ -0,0 +1 @@ +- Improve the Input box diff --git a/packages/markdown/tsconfig.json b/packages/markdown/tsconfig.json new file mode 100644 index 0000000..9714625 --- /dev/null +++ b/packages/markdown/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src"] +} diff --git a/packages/mtp/package.json b/packages/mtp/package.json index 982593a..44f5750 100644 --- a/packages/mtp/package.json +++ b/packages/mtp/package.json @@ -9,18 +9,16 @@ "scripts": { "format": "pnpm exec prettier --write .", "lint": "eslint src --ext .ts,.tsx", - "build": "tsc -p tsconfig.json --noEmit" + "test": "vitest run --passWithNoTests", + "build": "pnpm run test && tsc -p tsconfig.json --noEmit" }, "dependencies": { "@methanium/ui": "*", "@tauri-apps/api": "^2.11.1", - "@tensamin/crypto": "workspace:*", "@tensamin/shared": "workspace:*", "@tensamin/storage": "workspace:*", "mtp": "*", - "react": "^19.2.8", - "react-dom": "^19.2.8", - "zod": "^4.4.3" + "react": "^19.2.8" }, "devDependencies": { "eslint": "^10.8.0" diff --git a/packages/mtp/src/browser.tsx b/packages/mtp/src/browser.tsx new file mode 100644 index 0000000..755a021 --- /dev/null +++ b/packages/mtp/src/browser.tsx @@ -0,0 +1,529 @@ +import { type ReactNode, useEffect, useMemo, useRef, useState } from "react"; +import { toast as sonnerToast } from "@methanium/ui"; +import { base64ToBytes, ConnectionState, MTPClient } from "mtp"; +import createAsyncQueue from "@tensamin/shared/asyncQueue"; +import { + mtp as mtpSchemas, + type Calls, + type Communities, + type Contacts, +} from "@tensamin/shared/data"; +import { log } from "@tensamin/shared/log"; +import { useStorage } from "@tensamin/storage/context"; + +import { + type BoundSendFn, + MTPContext, + type MTPContextType, + type ProtocolMessage, + removeMissingContacts, + useMessageHandlers, +} from "./mtpContext"; +import { + DISCOVERY_TIMEOUT, + INITIAL_SYNC_TIMEOUT, + RECONNECT_JITTER, + RECONNECT_LONG_INTERVAL, + RECONNECT_RESET, + RECONNECT_TRIES, + RETRY_INTERVAL, + STATE_ACK_TIMEOUT, +} from "./values"; + +type BrowserMtpClient = Awaited>; + +function createBrowserClient( + options: Omit[0], "schemas">, +) { + return MTPClient.create({ + ...options, + schemas: mtpSchemas, + throwProtocolErrors: true, + onValidationError: (error) => { + log(1, "mtp", "red", "Failed to validate push message", error); + }, + }); +} + +function abortError(signal: AbortSignal): Error { + return signal.reason instanceof Error + ? signal.reason + : new Error("Initial state synchronization was cancelled"); +} + +function withDeadline( + promise: Promise, + timeoutMs: number, + timeoutMessage: string, + signal: AbortSignal, +): Promise { + return new Promise((resolve, reject) => { + if (signal.aborted) { + reject(abortError(signal)); + return; + } + let settled = false; + const finish = (complete: () => void) => { + if (settled) return; + settled = true; + clearTimeout(timeout); + signal.removeEventListener("abort", onAbort); + complete(); + }; + const timeout = setTimeout( + () => finish(() => reject(new Error(timeoutMessage))), + timeoutMs, + ); + const onAbort = () => finish(() => reject(abortError(signal))); + signal.addEventListener("abort", onAbort, { once: true }); + promise.then( + (value) => finish(() => resolve(value)), + (error: unknown) => finish(() => reject(error)), + ); + }); +} + +async function completeInitialSynchronization( + client: BrowserMtpClient, + subscribe: MTPContextType["subscribe"], + signal: AbortSignal, + syncTimeoutMs = INITIAL_SYNC_TIMEOUT, + ackTimeoutMs = STATE_ACK_TIMEOUT, +): Promise> { + const stateSync = new Promise>( + (resolve, reject) => { + let unsubscribeStateSync = () => {}; + let unsubscribeNoIota = () => {}; + const cleanup = () => { + clearTimeout(timeout); + unsubscribeStateSync(); + unsubscribeNoIota(); + signal.removeEventListener("abort", onAbort); + }; + const onAbort = () => { + cleanup(); + reject(abortError(signal)); + }; + const timeout = setTimeout(() => { + cleanup(); + reject(new Error("Initial state synchronization timed out")); + }, syncTimeoutMs); + signal.addEventListener("abort", onAbort, { once: true }); + if (signal.aborted) { + onAbort(); + return; + } + unsubscribeStateSync = subscribe("ClientStateSync", (message) => { + cleanup(); + resolve(message); + }); + unsubscribeNoIota = subscribe("ErrorNoIota", () => { + cleanup(); + reject(new Error("No Iota is currently connected")); + }); + }, + ); + const [, state] = await Promise.all([ + withDeadline( + client.auth(), + syncTimeoutMs, + "MTP authentication timed out", + signal, + ), + stateSync, + ]); + await withDeadline( + client.request("ClientStateAck", { + SessionId: state.data.SessionId, + VersionNumber: state.data.VersionNumber, + }), + ackTimeoutMs, + "State acknowledgement timed out", + signal, + ); + if (signal.aborted) throw abortError(signal); + return state; +} + +function protocolErrorDetails(error: unknown) { + if (typeof error !== "object" || error === null || !("type" in error)) { + return null; + } + const protocolError = error as { + id?: unknown; + type?: unknown; + frame?: unknown; + }; + return { + id: protocolError.id, + type: protocolError.type, + frame: protocolError.frame, + }; +} + +export function BrowserProvider(props: { + children: ReactNode; + blockConnection?: boolean; +}) { + const { load } = useStorage(); + const [readyState, setReadyState] = useState( + ConnectionState.Disconnected, + ); + const [identified, setIdentified] = useState(false); + const [identifying, setIdentifying] = useState(false); + const [freshCommunities, setFreshCommunities] = useState([]); + const [freshContacts, setFreshContacts] = useState([]); + const [freshCalls, setFreshCalls] = useState([]); + const clientRef = useRef(null); + const { addInterceptor, attachSubscriptions, interceptorsRef, subscribe } = + useMessageHandlers(); + const connected = readyState === ConnectionState.Connected; + + const [mtpUrl, setMtpUrl] = useState(null); + useEffect(() => { + load("omega_url").then(setMtpUrl); + }, [load]); + + const send: BoundSendFn = useMemo( + () => async (type, data, options) => { + const client = clientRef.current; + if (!client) throw new Error("mtp is not connected"); + const response = await client.request(type, data, options); + if (response.type === "GetStates") { + setFreshContacts((contacts) => + removeMissingContacts( + contacts, + response as ProtocolMessage<"GetStates">, + ), + ); + } + return response; + }, + [], + ); + + const resolveConnectionRef = useRef(() => {}); + useEffect(() => { + if (!mtpUrl) return; + let attempts = 0; + let reconnectTimer: ReturnType | null = null; + let reconnectResetTimer: ReturnType | null = null; + let reconnectScheduled = false; + let disposed = false; + let connectionGeneration = 0; + let cleanupConnection = () => {}; + + const clearReconnectTimer = () => { + if (!reconnectTimer) return; + clearTimeout(reconnectTimer); + reconnectTimer = null; + reconnectScheduled = false; + }; + const clearReconnectResetTimer = () => { + if (!reconnectResetTimer) return; + clearTimeout(reconnectResetTimer); + reconnectResetTimer = null; + }; + const scheduleReconnect = (error: unknown) => { + if (disposed || reconnectScheduled) return; + attempts += 1; + const shortRetry = attempts <= RECONNECT_TRIES; + if (!shortRetry) { + log(0, "mtp", "red", "Reconnection attempts exhausted", error); + sonnerToast.error("Connection failed", { + id: "mtp-connection-toast", + description: + error instanceof Error + ? `${error.message.split(":")[0]}. Retrying in the background.` + : "Connection lost. Retrying in the background.", + icon: null, + duration: Infinity, + closeButton: true, + promise: null, + } as unknown as Parameters[1]); + } else { + sonnerToast.loading( + `Reconnecting to server... (attempt ${attempts} of ${RECONNECT_TRIES})`, + { id: "mtp-connection-toast" }, + ); + } + const baseDelay = shortRetry ? RETRY_INTERVAL : RECONNECT_LONG_INTERVAL; + const jitter = 1 + (Math.random() * 2 - 1) * RECONNECT_JITTER; + reconnectScheduled = true; + reconnectTimer = setTimeout( + () => { + reconnectScheduled = false; + reconnectTimer = null; + void connect(); + }, + Math.round(baseDelay * jitter), + ); + }; + + async function connect() { + if (disposed || props.blockConnection) return; + const generation = ++connectionGeneration; + let client: BrowserMtpClient | null = null; + let failed = false; + let connectionReady = false; + let detachSubscriptions = () => {}; + let unsubscribeNoIota = () => {}; + const attemptAbort = new AbortController(); + const cleanup = () => { + attemptAbort.abort( + new Error("Initial state synchronization was cancelled"), + ); + unsubscribeNoIota(); + detachSubscriptions(); + client?.disconnect(); + if (clientRef.current === client) clientRef.current = null; + clearReconnectResetTimer(); + if (generation === connectionGeneration) { + setReadyState(ConnectionState.Disconnected); + setIdentified(false); + setIdentifying(false); + } + }; + cleanupConnection = cleanup; + try { + setIdentified(false); + setIdentifying(false); + const [userId, keyring] = await Promise.all([ + load("user_id"), + load("mtp_keyring"), + ]); + if (!userId || !keyring) throw new Error("Missing login credentials"); + const forcedOmikronUrl = await load("forced_omikron_url"); + const forcedOmikronPublicKey = await load("forced_omikron_public_key"); + let url = null; + let omikronPublicKey = null; + if (forcedOmikronUrl && forcedOmikronPublicKey) { + url = forcedOmikronUrl; + omikronPublicKey = forcedOmikronPublicKey; + } else { + log(2, "mtp", "purple", "Fetching Omikron data."); + const data = await fetch(`${mtpUrl}api/get/omikron/${userId}`, { + signal: AbortSignal.any([ + attemptAbort.signal, + AbortSignal.timeout(DISCOVERY_TIMEOUT), + ]), + }); + if (data.status === 404) { + throw new Error("No Omikron assignment is currently available"); + } + if (!data.ok) + throw new Error(`Omikron discovery failed: HTTP ${data.status}`); + const omikronData = (await data.json()) as { + ip_address: string; + port: number; + public_key: string; + }; + if ( + !omikronData.ip_address || + !omikronData.port || + !omikronData.public_key + ) { + throw new Error("Invalid Omikron data"); + } + url = `https://${omikronData.ip_address}:${omikronData.port}`; + omikronPublicKey = omikronData.public_key; + } + if (!url || !omikronPublicKey) { + throw new Error("Missing Omikron URL or Public Key"); + } + log(2, "mtp", "green", "Connecting to: " + url); + client = await createBrowserClient({ + url, + credentials: { clientId: userId, keyring: base64ToBytes(keyring) }, + hostPublicKey: { value: omikronPublicKey, encoding: "base64" }, + descriptor: "client", + pings: true, + logger: (event) => { + if (event.type === "state") { + if (generation !== connectionGeneration) return; + const state = client?.state ?? ConnectionState.Disconnected; + setReadyState(state); + if ( + state === ConnectionState.Disconnected && + clientRef.current === client && + !failed + ) { + failed = true; + const error = new Error("MTP connection lost"); + attemptAbort.abort(error); + if (connectionReady) { + cleanup(); + scheduleReconnect(error); + } + } + } + if (event.type !== "Pong" && event.type !== "Ping") { + log( + 2, + "mtp", + event.type === "state" + ? "purple" + : event.direction === "recv" + ? "cyan" + : event.direction === "send" + ? "gray" + : "blue", + event.type === "state" + ? event.data + : event.direction === "recv" + ? "< " + event.type + : event.direction === "send" + ? "> " + event.type + : event.type, + event, + ); + } + }, + }); + if (disposed || generation !== connectionGeneration) { + client.disconnect(); + return; + } + const activeClient = client; + clientRef.current = activeClient; + detachSubscriptions = attachSubscriptions(activeClient); + unsubscribeNoIota = subscribe("ErrorNoIota", () => { + if (clientRef.current !== activeClient || failed) return; + failed = true; + const error = new Error("No Iota is currently connected"); + attemptAbort.abort(error); + cleanup(); + scheduleReconnect(error); + }); + setReadyState(activeClient.state); + setIdentifying(true); + const finalResponse = await completeInitialSynchronization( + activeClient, + subscribe, + attemptAbort.signal, + ); + if (disposed || clientRef.current !== activeClient) return; + setFreshContacts(finalResponse.data.Contacts); + setFreshCommunities(finalResponse.data.Communities); + setFreshCalls(finalResponse.data.Calls); + connectionReady = true; + setIdentifying(false); + setIdentified(true); + clearReconnectTimer(); + clearReconnectResetTimer(); + reconnectResetTimer = setTimeout(() => { + attempts = 0; + reconnectResetTimer = null; + }, RECONNECT_RESET * 1_000); + resolveConnectionRef.current?.(); + } catch (connectError) { + if (disposed || generation !== connectionGeneration) { + client?.disconnect(); + return; + } + failed = true; + cleanup(); + const message = + connectError instanceof Error + ? connectError.message + : String(connectError ?? "Unknown error"); + log( + 0, + "mtp", + "red", + `Connection/authentication attempt failed: ${message}`, + protocolErrorDetails(connectError) ?? connectError, + ); + scheduleReconnect(connectError); + } + } + + void connect(); + return () => { + disposed = true; + clearReconnectTimer(); + clearReconnectResetTimer(); + cleanupConnection(); + setReadyState(ConnectionState.Disconnected); + setIdentified(false); + setIdentifying(false); + sonnerToast.dismiss("mtp-connection-toast"); + }; + }, [attachSubscriptions, load, mtpUrl, props.blockConnection, subscribe]); + + useEffect(() => { + return subscribe("ErrorNoIota", () => { + setIdentified(false); + setIdentifying(false); + sonnerToast.error("We couldn't reach your Iota", { + description: + "Check your network connection and try restarting your Iota", + icon: null, + duration: Infinity, + closeButton: true, + }); + resolveConnectionRef.current?.(); + }); + }, [subscribe]); + + useEffect( + () => + subscribe("GetStates", (message) => { + setFreshContacts((contacts) => + removeMissingContacts(contacts, message), + ); + }), + [subscribe], + ); + + const loadingDescription = useMemo(() => { + if (!mtpUrl) return "Loading connection details"; + if (readyState === ConnectionState.Connecting || !connected) { + return "Establishing transport channel"; + } + if (identifying || !identified) return "Waiting for authenticated session"; + return "Loading..."; + }, [connected, identified, identifying, readyState, mtpUrl]); + const contextReady = connected && identified && mtpUrl !== null; + const mtpRef = useMemo(() => createAsyncQueue<{ send: typeof send }>(), []); + useEffect(() => { + if (connected && identified && mtpUrl) { + mtpRef.set({ send }); + } + }, [connected, identified, mtpUrl, send, mtpRef]); + + const sendQueued: BoundSendFn = useMemo( + () => async (type, data, options) => { + const mtp = await mtpRef.get(); + const response = await mtp.send(type, data, options); + for (const interceptor of interceptorsRef.current) { + void Promise.resolve(interceptor({ type, data, response })).catch( + (error) => { + log(1, "mtp", "yellow", "MTP interceptor failed", error, { type }); + }, + ); + } + return response; + }, + [interceptorsRef, mtpRef], + ); + + return ( + + {props.children} + + ); +} diff --git a/packages/mtp/src/context.test.tsx b/packages/mtp/src/context.test.tsx deleted file mode 100644 index 7f50aba..0000000 --- a/packages/mtp/src/context.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { isPushType, validateResponse } from "./context"; - -describe("MTP protocol dispatch", () => { - it("preserves protocol errors for the request layer", () => { - const error = validateResponse("GetStates", { - id: 12, - type: "ErrorInternal", - data: { ErrorType: "temporary" }, - }); - expect(error.type).toBe("ErrorInternal"); - expect(error.id).toBe(12); - }); - - it("recognizes initial and live presence pushes", () => { - expect(isPushType("GetStates")).toBe(true); - expect(isPushType("ClientChanged")).toBe(true); - expect(isPushType("UnknownMessage")).toBe(false); - }); -}); diff --git a/packages/mtp/src/context.tsx b/packages/mtp/src/context.tsx index af06ee0..c07579f 100644 --- a/packages/mtp/src/context.tsx +++ b/packages/mtp/src/context.tsx @@ -1,957 +1,41 @@ -import { - createContext, - type ReactNode, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { invoke, isTauri } from "@tauri-apps/api/core"; -import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +import { type ReactNode, useContext, useEffect, useState } from "react"; +import { isTauri } from "@tauri-apps/api/core"; import { MTPClient } from "mtp"; -import { type z } from "zod"; -import { ConnectionState } from "mtp"; -import createAsyncQueue from "@tensamin/shared/asyncQueue"; -import { toast as sonnerToast } from "@methanium/ui"; -import { - type Calls, - type Communities, - type Contacts, - mtp as schemas, - type MTP as Schemas, -} from "@tensamin/shared/data"; -import { log } from "@tensamin/shared/log"; -import { ProtocolError } from "@tensamin/shared/errors"; -import { useStorage } from "@tensamin/storage/context"; - -import { RECONNECT_RESET, RECONNECT_TRIES, RETRY_INTERVAL } from "./values"; - -function base64ToUint8Array(b64: string) { - const bin = atob(b64); - const out = new Uint8Array(bin.length); - - for (let i = 0; i < bin.length; i++) { - out[i] = bin.charCodeAt(i); - } - - return out; -} - -export type ProtocolMessage< - T extends keyof Schemas & string = keyof Schemas & string, -> = { - id?: number; - type: T | string; - data: z.infer; -}; - -export type BoundSendFn = ( - type: T, - data?: z.infer, - options?: { id?: number }, -) => Promise>; - -export type PushHandler = (message: ProtocolMessage) => void | Promise; - -const PUSH_TYPES = [ - "MessageLive", - "MessageEditLive", - "MessageReactionLive", - "MessageDeleteLive", - "MessageState", - "CallInvite", - "GetStates", - "ClientChanged", - "ErrorNoIota", -] as const; - -export function isPushType(type: string): boolean { - return (PUSH_TYPES as readonly string[]).includes(type); -} - -function removeMissingContacts( - contacts: Contacts, - message: ProtocolMessage, -): Contacts { - if (message.type !== "GetStates") return contacts; - const data = message.data as { MissingUserIds?: unknown }; - if (!Array.isArray(data.MissingUserIds)) return contacts; - const missing = new Set( - data.MissingUserIds.filter( - (userId): userId is number => typeof userId === "number", - ), - ); - return contacts.filter((contact) => !missing.has(contact.UserId)); -} - -export type MTPExchange = { - type: keyof Schemas & string; - data: unknown; - response: ProtocolMessage; -}; - -export type MTPInterceptor = (exchange: MTPExchange) => void | Promise; - -type ContextType = { - send: BoundSendFn; - subscribe: ( - type: T, - handler: (message: ProtocolMessage) => void, - ) => () => void; - subscribePush: (handler: PushHandler) => () => void; - addInterceptor: (interceptor: MTPInterceptor) => () => void; - readyState: number; - identified: boolean; - freshContacts: Contacts; - freshCommunities: Communities; - freshCalls: Calls; - contextReady: boolean; - loadingDescription: string; -}; - -const MTPContext = createContext(undefined); - -function getProtocolErrorDetails(error: unknown) { - if (typeof error !== "object" || error === null || !("type" in error)) { - return null; - } - - const protocolError = error as { - id?: unknown; - type?: unknown; - data?: unknown; - }; - return { - id: protocolError.id, - type: protocolError.type, - data: protocolError.data, - }; -} - -// Zod schema validation -export function validateResponse( - type: T, - message: { id?: number; type: string; data: unknown }, -): ProtocolMessage { - if (message.type.startsWith("Error")) { - return message as ProtocolMessage; - } - - const schema = - schemas[message.type as keyof Schemas & string]?.response ?? - schemas[type]?.response; - if (!schema) { - return message as ProtocolMessage; - } - - const parsed = schema.safeParse(message.data); - if (!parsed.success) { - throw new Error( - `Response validation failed for ${type}: ${parsed.error.message}`, - ); - } - - return { - id: message.id, - type: message.type, - data: parsed.data, - } as ProtocolMessage; -} - -function useMessageHandlers() { - const interceptorsRef = useRef(new Set()); - const pushHandlersRef = useRef(new Set()); - const lastInitialStateRef = useRef(null); - const subscribePush = useCallback((handler: PushHandler) => { - pushHandlersRef.current.add(handler); - const initialState = lastInitialStateRef.current; - if (initialState?.type === "GetStates") { - void Promise.resolve(handler(initialState)).catch(() => undefined); - } - return () => pushHandlersRef.current.delete(handler); - }, []); - const addInterceptor = useCallback((interceptor: MTPInterceptor) => { - interceptorsRef.current.add(interceptor); - return () => interceptorsRef.current.delete(interceptor); - }, []); - return { - addInterceptor, - interceptorsRef, - lastInitialStateRef, - pushHandlersRef, - subscribePush, - }; -} - -function BrowserProvider(props: { - children: ReactNode; - blockConnection?: boolean; -}) { - const { load } = useStorage(); - - const [readyState, setReadyState] = useState( - ConnectionState.Disconnected, - ); - const [identified, setIdentified] = useState(false); - const [identifying, setIdentifying] = useState(false); - - const [freshCommunities, setFreshCommunities] = useState([]); - const [freshContacts, setFreshContacts] = useState([]); - const [freshCalls, setFreshCalls] = useState([]); - - const clientRef = useRef> | null>( - null, - ); - const { - addInterceptor, - interceptorsRef, - lastInitialStateRef, - pushHandlersRef, - subscribePush, - } = useMessageHandlers(); - - const connected = readyState === ConnectionState.Connected; - - // MTP url - const [mtpUrl, setMtpUrl] = useState(null); - useEffect(() => { - load("omega_url").then(setMtpUrl); - }, [load]); - - // Validation override functions - const send: BoundSendFn = useMemo( - () => async (type, data, options) => { - const client = clientRef.current; - - if (!client) { - throw new Error("mtp is not connected"); - } - - const message = await client.request( - type, - (data ?? {}) as Record, - options, - ); - const response = validateResponse(type, message); - setFreshContacts((contacts) => removeMissingContacts(contacts, response)); - if (response.type.startsWith("Error")) { - const errorData = response.data as Record; - throw new ProtocolError({ - type: response.type, - requestId: response.id, - errorType: - typeof errorData.ErrorType === "string" - ? errorData.ErrorType - : undefined, - }); - } - return response; - }, - [], - ); - - const subscribe = useCallback((type, handler) => { - const client = clientRef.current; - if (!client) { - return () => {}; - } - - return client.subscribe(type, (message) => { - handler(validateResponse(type, message)); - }); - }, []); - - // Reconnect stuff - const resolveConnectionRef = useRef(() => {}); - useEffect(() => { - if (!mtpUrl) return; - - let attempts = 0; - let reconnectTimer: ReturnType | null = null; - let reconnectResetTimer: ReturnType | null = null; - let reconnectScheduled = false; - let disposed = false; - let connectionGeneration = 0; - - const clearReconnectTimer = () => { - if (!reconnectTimer) return; - clearTimeout(reconnectTimer); - reconnectTimer = null; - reconnectScheduled = false; - }; - - const clearReconnectResetTimer = () => { - if (!reconnectResetTimer) return; - clearTimeout(reconnectResetTimer); - reconnectResetTimer = null; - }; - - const scheduleReconnect = (error: unknown) => { - if (disposed || reconnectScheduled) return; - if (attempts >= RECONNECT_TRIES) { - log(0, "mtp", "red", "Reconnection attempts exhausted", error); - sonnerToast.error("Connection failed", { - id: "mtp-connection-toast", - description: - error instanceof Error - ? error.message.split(":")[0] - : "Connection lost", - icon: null, - duration: Infinity, - closeButton: true, - promise: null, - } as unknown as Parameters[1]); - return; - } - - attempts += 1; - sonnerToast.loading( - `Reconnecting to server... (attempt ${attempts} of ${RECONNECT_TRIES})`, - { id: "mtp-connection-toast" }, - ); - reconnectScheduled = true; - reconnectTimer = setTimeout(() => { - reconnectScheduled = false; - reconnectTimer = null; - void connect(); - }, RETRY_INTERVAL); - }; - - async function connect() { - if (disposed || props.blockConnection) return; - - const generation = ++connectionGeneration; - let client: Awaited> | null = null; - let failed = false; - const cleanup = () => { - client?.disconnect(); - if (clientRef.current === client) { - clientRef.current = null; - } - clearReconnectResetTimer(); - if (generation === connectionGeneration) { - setReadyState(ConnectionState.Disconnected); - setIdentified(false); - setIdentifying(false); - } - }; - try { - setIdentified(false); - setIdentifying(false); - - const [userId, keyring] = await Promise.all([ - load("user_id"), - load("mtp_keyring"), - ]); - if (!userId || !keyring) { - throw new Error("Missing login credentials"); - } - const forcedOmikronUrl = await load("forced_omikron_url"); - const forcedOmikronPublicKey = await load("forced_omikron_public_key"); - - let url = null; - let omikronPublicKey = null; - if (forcedOmikronUrl && forcedOmikronPublicKey) { - url = forcedOmikronUrl; - omikronPublicKey = forcedOmikronPublicKey; - } else { - log(2, "mtp", "purple", "Fetching Omikron data."); - const data = await fetch(`${mtpUrl}api/get/omikron/${userId}`); - - if (data.status === 404) { - sonnerToast.error("We couldn't reach your Iota", { - description: - "Check your network connection and try restarting your Iota", - icon: null, - duration: Infinity, - closeButton: true, - }); - resolveConnectionRef.current?.(); - cleanup(); - return; - } - const omikronData = (await data.json()) as { - id: number; - ip_address: string; - port: number; - public_key: string; - status: string; - }; - - if ( - !omikronData.ip_address || - !omikronData.port || - !omikronData.public_key - ) - throw new Error("Invalid Omikron data"); - - url = `https://${omikronData.ip_address}:${omikronData.port}`; - omikronPublicKey = omikronData.public_key; - } - //codec.decode(new Uint8Array(await res.arrayBuffer())), - - if (!url || !omikronPublicKey) - throw new Error("Missing Omikron URL or Public Key"); - - log(2, "mtp", "green", "Connecting to: " + url); - - client = await MTPClient.create({ - url, - credentials: { - clientId: userId, - keyring: base64ToUint8Array(keyring), - }, - hostPublicKey: omikronPublicKey, - descriptor: "client", - pings: true, - logger: (event) => { - if (event.type === "state") { - if (generation !== connectionGeneration) return; - const state = client?.state ?? ConnectionState.Disconnected; - setReadyState(state); - if ( - state === ConnectionState.Disconnected && - clientRef.current === client && - !failed - ) { - failed = true; - clientRef.current = null; - setIdentified(false); - setIdentifying(false); - scheduleReconnect(new Error("MTP connection lost")); - } - } - - if (event.type !== "Pong" && event.type !== "Ping") { - log( - 2, - "mtp", - event.type === "state" - ? "purple" - : event.direction === "recv" - ? "cyan" - : event.direction === "send" - ? "gray" - : "blue", - event.type === "state" - ? event.data - : event.direction === "recv" - ? "< " + event.type - : event.direction === "send" - ? "> " + event.type - : event.type, - event, - ); - } - }, - }); - - if (disposed || generation !== connectionGeneration) { - client.disconnect(); - return; - } - const activeClient = client; - - clientRef.current = activeClient; - for (const type of PUSH_TYPES) { - activeClient.subscribe(type, (message) => { - let validated: ProtocolMessage; - try { - validated = validateResponse(type, message); - } catch (error) { - log(1, "mtp", "red", "Failed to validate push message", error, { - type, - data: message.data, - }); - return; - } - - setFreshContacts((contacts) => - removeMissingContacts(contacts, validated), - ); - for (const handler of [...pushHandlersRef.current]) { - void Promise.resolve() - .then(() => handler(validated)) - .catch((error) => { - log(1, "mtp", "red", "Push handler failed", error, { type }); - }); - } - if (validated.type === "GetStates") { - lastInitialStateRef.current = validated; - } - }); - } - setReadyState(activeClient.state); - - clearReconnectTimer(); - - // Schedule reconnect reset - clearReconnectResetTimer(); - reconnectResetTimer = setTimeout(() => { - attempts = 0; - reconnectResetTimer = null; - }, RECONNECT_RESET * 1_000); - - setReadyState(activeClient.state); - setIdentifying(true); - - const stateSync = new Promise>( - (resolve, reject) => { - let unsubscribeStateSync = () => {}; - let unsubscribeNoIota = () => {}; - const cleanupStateSync = () => { - clearTimeout(timeout); - unsubscribeStateSync(); - unsubscribeNoIota(); - }; - const timeout = setTimeout(() => { - cleanupStateSync(); - reject(new Error("Initial state synchronization timed out")); - }, 120_000); - unsubscribeStateSync = activeClient.subscribe( - "ClientStateSync", - (message) => { - cleanupStateSync(); - try { - resolve(validateResponse("ClientStateSync", message)); - } catch (error) { - reject(error); - } - }, - ); - unsubscribeNoIota = activeClient.subscribe("ErrorNoIota", () => { - cleanupStateSync(); - reject(new Error("No Iota is currently connected")); - }); - }, - ); - const [, finalResponse] = await Promise.all([ - activeClient.auth(), - stateSync, - ]); - - if (finalResponse.type.startsWith("Error")) { - throw new Error( - `State synchronization failed: ${finalResponse.type}`, - ); - } - - const acknowledgement = await activeClient.request("ClientStateAck", { - SessionId: finalResponse.data.SessionId, - VersionNumber: finalResponse.data.VersionNumber, - }); - if (acknowledgement.type.startsWith("Error")) { - throw new Error( - `State acknowledgement failed: ${acknowledgement.type}`, - ); - } - - if (disposed || clientRef.current !== activeClient) return; - - setFreshContacts(finalResponse.data.Contacts); - setFreshCommunities(finalResponse.data.Communities); - setFreshCalls(finalResponse.data.Calls); - setIdentifying(false); - setIdentified(true); - resolveConnectionRef.current?.(); - } catch (connectError) { - if (disposed || generation !== connectionGeneration) { - client?.disconnect(); - return; - } - failed = true; - cleanup(); - const connectErrorMessage = - connectError instanceof Error - ? connectError.message - : String(connectError ?? "Unknown error"); - log( - 0, - "mtp", - "red", - `Connection/authentication attempt failed: ${connectErrorMessage}`, - getProtocolErrorDetails(connectError) ?? connectError, - ); - - scheduleReconnect(connectError); - } - } - - void connect(); - - return () => { - disposed = true; - clearReconnectTimer(); - clearReconnectResetTimer(); - - clientRef.current?.disconnect(); - clientRef.current = null; - setReadyState(ConnectionState.Disconnected); - setIdentified(false); - setIdentifying(false); - sonnerToast.dismiss("mtp-connection-toast"); - }; - }, [ - lastInitialStateRef, - mtpUrl, - props.blockConnection, - load, - pushHandlersRef, - ]); - - // No Iota check - useEffect(() => { - if (!connected) return; - - return subscribe("ErrorNoIota", () => { - setIdentified(false); - setIdentifying(false); - sonnerToast.error("We couldn't reach your Iota", { - description: - "Check your network connection and try restarting your Iota", - icon: null, - duration: Infinity, - closeButton: true, - }); - resolveConnectionRef.current?.(); - }); - }, [connected, subscribe]); - - // Async queue - const loadingDescription = useMemo(() => { - if (!mtpUrl) return "Loading connection details"; - if (readyState === ConnectionState.Connecting || !connected) { - return "Establishing transport channel"; - } - if (identifying || !identified) return "Waiting for authenticated session"; - return "Loading..."; - }, [connected, identified, identifying, readyState, mtpUrl]); - const contextReady = connected && identified && mtpUrl !== null; - const mtpRef = useMemo( - () => - createAsyncQueue<{ - send: typeof send; - subscribe: typeof subscribe; - subscribePush: typeof subscribePush; - }>(), - [], - ); - useEffect(() => { - if (connected && identified && mtpUrl) { - mtpRef.set({ - send, - subscribe, - subscribePush, - }); - } - }, [connected, identified, mtpUrl, send, subscribe, subscribePush, mtpRef]); - - const sendQueued: BoundSendFn = useMemo( - () => async (type, data, options) => { - const mtp = await mtpRef.get(); - const response = await mtp.send(type, data, options); - for (const interceptor of interceptorsRef.current) { - void Promise.resolve( - interceptor({ type, data, response: response as ProtocolMessage }), - ).catch((error) => { - log(1, "mtp", "yellow", "MTP interceptor failed", error, { type }); - }); - } - return response; - }, - [interceptorsRef, mtpRef], - ); - - return ( - - {props.children} - - ); -} - -type NativeSnapshot = { - generation: number; - readyState: number; - identified: boolean; - state?: unknown; - error?: string; -}; - -function TauriProvider(props: { - children: ReactNode; - blockConnection?: boolean; -}) { - const [snapshot, setSnapshot] = useState({ - generation: 0, - readyState: ConnectionState.Disconnected, - identified: false, - }); - const [freshContacts, setFreshContacts] = useState([]); - const [freshCommunities, setFreshCommunities] = useState([]); - const [freshCalls, setFreshCalls] = useState([]); - const generationRef = useRef(0); - const { - addInterceptor, - interceptorsRef, - lastInitialStateRef, - pushHandlersRef, - subscribePush, - } = useMessageHandlers(); - const subscriptionsRef = useRef( - new Map void>>(), - ); - - const applySnapshot = useCallback((next: NativeSnapshot) => { - if (next.generation < generationRef.current) return; - generationRef.current = next.generation; - if (next.error) { - log(0, "android", "orange", "MTP connection failed", next.error); - } - setSnapshot(next); - if (!next.identified || next.state === undefined) return; - const parsed = schemas.ClientStateSync.response.safeParse(next.state); - if (!parsed.success) { - log(0, "mtp", "red", "Invalid native MTP state", parsed.error); - return; - } - setFreshContacts(parsed.data.Contacts); - setFreshCommunities(parsed.data.Communities); - setFreshCalls(parsed.data.Calls); - }, []); - - const dispatchMessage = useCallback( - (raw: unknown) => { - if (!raw || typeof raw !== "object" || !("type" in raw)) return; - const message = raw as { id?: number; type: string; data: unknown }; - let validated: ProtocolMessage; - try { - validated = validateResponse( - message.type as keyof Schemas & string, - message, - ); - } catch (error) { - log(1, "mtp", "red", "Failed to validate native MTP message", error); - return; - } - for (const handler of subscriptionsRef.current.get(validated.type) ?? - []) { - handler(validated); - } - if (!isPushType(validated.type)) return; - setFreshContacts((contacts) => - removeMissingContacts(contacts, validated), - ); - for (const handler of [...pushHandlersRef.current]) { - void Promise.resolve(handler(validated)).catch((error) => { - log(1, "mtp", "red", "Native MTP push handler failed", error, { - type: validated.type, - }); - }); - } - if (validated.type === "GetStates") { - lastInitialStateRef.current = validated; - } - }, - [lastInitialStateRef, pushHandlersRef], - ); - - useEffect(() => { - if (props.blockConnection) return; - let disposed = false; - let unlisten: UnlistenFn | undefined; - void (async () => { - try { - const nextUnlisten = await listen< - | { kind: "state"; snapshot: NativeSnapshot } - | { kind: "message"; generation: number; message: unknown } - | { - kind: "log"; - level: number; - message: string; - details?: unknown; - } - >("mtp://event", ({ payload }) => { - if (disposed) return; - if (payload.kind === "state") { - applySnapshot(payload.snapshot); - return; - } - if (payload.kind === "message") { - if (payload.generation === generationRef.current) { - dispatchMessage(payload.message); - } - return; - } - log( - payload.level, - "android", - "orange", - payload.message, - payload.details, - ); - }); - if (disposed) nextUnlisten(); - else unlisten = nextUnlisten; - } catch (error) { - log(0, "mtp", "red", "Failed to subscribe to native MTP events", error); - } - - try { - const current = await invoke("mtp_status"); - if (!disposed) applySnapshot(current); - } catch (error) { - log(0, "mtp", "red", "Failed to load native MTP status", error); - } - })(); - return () => { - disposed = true; - unlisten?.(); - }; - }, [applySnapshot, dispatchMessage, props.blockConnection]); - - useEffect(() => { - if (props.blockConnection) return; - const updateVisibility = () => { - void invoke("mtp_set_ui_visible", { - visible: document.visibilityState === "visible" && document.hasFocus(), - }); - }; - updateVisibility(); - document.addEventListener("visibilitychange", updateVisibility); - window.addEventListener("focus", updateVisibility); - window.addEventListener("blur", updateVisibility); - return () => { - document.removeEventListener("visibilitychange", updateVisibility); - window.removeEventListener("focus", updateVisibility); - window.removeEventListener("blur", updateVisibility); - void invoke("mtp_set_ui_visible", { visible: false }); - }; - }, [props.blockConnection]); - - const send = useCallback( - async (type, data, options) => { - const response = await invoke("mtp_request", { - typeName: type, - data: data ?? {}, - id: options?.id, - }); - const validated = validateResponse(type, response); - setFreshContacts((contacts) => - removeMissingContacts(contacts, validated), - ); - if (validated.type.startsWith("Error")) { - const errorData = validated.data as Record; - throw new ProtocolError({ - type: validated.type, - requestId: validated.id, - errorType: - typeof errorData.ErrorType === "string" - ? errorData.ErrorType - : undefined, - }); - } - for (const interceptor of interceptorsRef.current) { - void Promise.resolve( - interceptor({ type, data, response: validated as ProtocolMessage }), - ).catch((error) => { - log(1, "mtp", "yellow", "MTP interceptor failed", error, { type }); - }); - } - return validated; - }, - [interceptorsRef], - ); - - const subscribe = useCallback((type, handler) => { - const handlers = - subscriptionsRef.current.get(type) ?? - new Set<(message: ProtocolMessage) => void>(); - handlers.add(handler as (message: ProtocolMessage) => void); - subscriptionsRef.current.set(type, handlers); - return () => { - handlers.delete(handler as (message: ProtocolMessage) => void); - if (handlers.size === 0) subscriptionsRef.current.delete(type); - }; - }, []); - const connected = snapshot.readyState === ConnectionState.Connected; - const contextReady = connected && snapshot.identified; - - return ( - - {props.children} - - ); -} +import { BrowserProvider } from "./browser"; +import { MTPContext, type MTPContextType } from "./mtpContext"; +import { TauriProvider } from "./tauri"; export function Provider(props: { children: ReactNode; blockConnection?: boolean; +}) { + if (isTauri()) return ; + return ; +} + +function BrowserWasmProvider(props: { + children: ReactNode; + blockConnection?: boolean; }) { const [wasmReady, setWasmReady] = useState(false); const [wasmError, setWasmError] = useState(); - useEffect(() => { let active = true; void MTPClient.init().then( - () => { - if (active) setWasmReady(true); - }, - (error: unknown) => { - if (active) setWasmError(() => error); - }, + () => active && setWasmReady(true), + (error: unknown) => active && setWasmError(() => error), ); return () => { active = false; }; }, []); - if (wasmError) throw wasmError; - if (!wasmReady) return null; - - return isTauri() ? ( - - ) : ( - - ); + return wasmReady ? : null; } -export function useMTP(): ContextType { +export function useMTP(): MTPContextType { const context = useContext(MTPContext); - if (!context) { - throw new Error("useMTP must be used within an MTPProvider"); - } + if (!context) throw new Error("useMTP must be used within an MTPProvider"); return context; } diff --git a/packages/mtp/src/index.ts b/packages/mtp/src/index.ts index 39bce46..a4f05c8 100644 --- a/packages/mtp/src/index.ts +++ b/packages/mtp/src/index.ts @@ -3,6 +3,5 @@ export type { BoundSendFn, MTPExchange, MTPInterceptor, - PushHandler, ProtocolMessage, -} from "./context"; +} from "./mtpContext"; diff --git a/packages/mtp/src/mtpContext.tsx b/packages/mtp/src/mtpContext.tsx new file mode 100644 index 0000000..1f1654d --- /dev/null +++ b/packages/mtp/src/mtpContext.tsx @@ -0,0 +1,159 @@ +import { createContext, useCallback, useRef } from "react"; +import type { + MTPRequestFunction, + MTPResponseFrame, + MTPSubscriptionFunction, +} from "mtp"; +import { + mtp as mtpSchemas, + type Calls, + type Communities, + type Contacts, +} from "@tensamin/shared/data"; +import { log } from "@tensamin/shared/log"; + +export type ProtocolMessage< + Type extends keyof typeof mtpSchemas & string = keyof typeof mtpSchemas & + string, +> = MTPResponseFrame; + +export type BoundSendFn = MTPRequestFunction; + +export type MTPExchange = { + type: keyof typeof mtpSchemas & string; + data: unknown; + response: ProtocolMessage; +}; + +export type MTPInterceptor = (exchange: MTPExchange) => void | Promise; + +export type MTPContextType = { + send: BoundSendFn; + subscribe: MTPSubscriptionFunction; + addInterceptor: (interceptor: MTPInterceptor) => () => void; + readyState: number; + identified: boolean; + freshContacts: Contacts; + freshCommunities: Communities; + freshCalls: Calls; + contextReady: boolean; + loadingDescription: string; +}; + +export const MTPContext = createContext(undefined); + +export function removeMissingContacts( + contacts: Contacts, + message: ProtocolMessage<"GetStates">, +): Contacts { + const missing = new Set(message.data.MissingUserIds ?? []); + return contacts.filter((contact) => !missing.has(contact.UserId)); +} + +export function useMessageHandlers() { + const interceptorsRef = useRef(new Set()); + const subscriptionHandlersRef = useRef( + new Map void | Promise>>(), + ); + const transportRef = useRef<{ + subscribe: MTPSubscriptionFunction; + } | null>(null); + const transportGenerationRef = useRef(0); + const transportUnsubscribersRef = useRef(new Map void>()); + const lastInitialStateRef = useRef | null>(null); + + const attachType = useCallback( + (type: Type) => { + const transport = transportRef.current; + if (!transport || transportUnsubscribersRef.current.has(type)) return; + const generation = transportGenerationRef.current; + const unsubscribe = transport.subscribe(type, (message) => { + if ( + transportRef.current !== transport || + transportGenerationRef.current !== generation + ) + return; + if (type === "GetStates") { + lastInitialStateRef.current = message as ProtocolMessage<"GetStates">; + } + for (const handler of [ + ...(subscriptionHandlersRef.current.get(type) ?? []), + ]) { + void Promise.resolve(handler(message as ProtocolMessage)).catch( + (error) => { + log(1, "mtp", "red", "Subscription handler failed", error, { + type, + }); + }, + ); + } + }); + transportUnsubscribersRef.current.set(type, unsubscribe); + }, + [], + ); + + const attachSubscriptions = useCallback( + (transport: { subscribe: MTPSubscriptionFunction }) => { + for (const unsubscribe of transportUnsubscribersRef.current.values()) { + unsubscribe(); + } + transportUnsubscribersRef.current.clear(); + transportRef.current = transport; + const generation = ++transportGenerationRef.current; + for (const type of subscriptionHandlersRef.current.keys()) { + attachType(type as keyof typeof mtpSchemas & string); + } + return () => { + if ( + transportRef.current !== transport || + transportGenerationRef.current !== generation + ) + return; + transportRef.current = null; + transportGenerationRef.current += 1; + for (const unsubscribe of transportUnsubscribersRef.current.values()) { + unsubscribe(); + } + transportUnsubscribersRef.current.clear(); + }; + }, + [attachType], + ); + + const subscribe = useCallback>( + (type, handler) => { + const handlers = subscriptionHandlersRef.current.get(type) ?? new Set(); + const untypedHandler = handler as ( + message: ProtocolMessage, + ) => void | Promise; + handlers.add(untypedHandler); + subscriptionHandlersRef.current.set(type, handlers); + attachType(type); + const initialState = lastInitialStateRef.current; + if (type === "GetStates" && initialState) { + void Promise.resolve(untypedHandler(initialState)).catch( + () => undefined, + ); + } + return () => { + handlers.delete(untypedHandler); + if (handlers.size !== 0) return; + subscriptionHandlersRef.current.delete(type); + transportUnsubscribersRef.current.get(type)?.(); + transportUnsubscribersRef.current.delete(type); + }; + }, + [attachType], + ); + const addInterceptor = useCallback((interceptor: MTPInterceptor) => { + interceptorsRef.current.add(interceptor); + return () => interceptorsRef.current.delete(interceptor); + }, []); + return { + addInterceptor, + attachSubscriptions, + interceptorsRef, + subscribe, + }; +} diff --git a/packages/mtp/src/tauri.tsx b/packages/mtp/src/tauri.tsx new file mode 100644 index 0000000..92ac209 --- /dev/null +++ b/packages/mtp/src/tauri.tsx @@ -0,0 +1,258 @@ +import { + type ReactNode, + useCallback, + useEffect, + useRef, + useState, +} from "react"; +import { invoke } from "@tauri-apps/api/core"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +import { + ConnectionState, + MTPProxyConnection, + type MTPFrame, + type MTPProxyAdapter, +} from "mtp"; +import { + mtp as mtpSchemas, + type Calls, + type Communities, + type Contacts, +} from "@tensamin/shared/data"; +import { log } from "@tensamin/shared/log"; + +import { + type BoundSendFn, + MTPContext, + type ProtocolMessage, + removeMissingContacts, + useMessageHandlers, +} from "./mtpContext"; + +type NativeSnapshot = { + generation: number; + readyState: number; + identified: boolean; + state?: unknown; + error?: string; +}; + +function createTauriAdapter() { + const subscriptions = new Map void>>(); + const adapter: MTPProxyAdapter = { + request: (type, data) => + invoke("mtp_request", { typeName: type, data }), + subscribe(type, handler) { + const handlers = subscriptions.get(type) ?? new Set(); + handlers.add(handler); + subscriptions.set(type, handlers); + return () => { + handlers.delete(handler); + if (handlers.size === 0) subscriptions.delete(type); + }; + }, + }; + return { + adapter, + dispatch(message: MTPFrame) { + for (const handler of subscriptions.get(message.type) ?? []) + handler(message); + }, + }; +} + +export function TauriProvider(props: { + children: ReactNode; + blockConnection?: boolean; +}) { + const [snapshot, setSnapshot] = useState({ + generation: 0, + readyState: ConnectionState.Disconnected, + identified: false, + }); + const [freshContacts, setFreshContacts] = useState([]); + const [freshCommunities, setFreshCommunities] = useState([]); + const [freshCalls, setFreshCalls] = useState([]); + const generationRef = useRef(0); + const { addInterceptor, attachSubscriptions, interceptorsRef, subscribe } = + useMessageHandlers(); + const [{ bridge, connection }] = useState(() => { + const bridge = createTauriAdapter(); + return { + bridge, + connection: new MTPProxyConnection(bridge.adapter, { + schemas: mtpSchemas, + throwProtocolErrors: true, + onValidationError: (error) => { + log(1, "mtp", "red", "Failed to validate native MTP message", error); + }, + }), + }; + }); + + const applySnapshot = useCallback(async (next: NativeSnapshot) => { + if (next.generation < generationRef.current) return; + generationRef.current = next.generation; + if (next.error) + log(0, "android", "orange", "MTP connection failed", next.error); + if (!next.identified) { + setSnapshot(next); + return; + } + if (next.state === undefined) { + setSnapshot({ + ...next, + identified: false, + error: "Native MTP connection omitted initial state", + }); + return; + } + try { + const state = await mtpSchemas.ClientStateSync.response.parseAsync( + next.state, + ); + setFreshContacts(state.Contacts); + setFreshCommunities(state.Communities); + setFreshCalls(state.Calls); + setSnapshot(next); + } catch (error) { + log(0, "mtp", "red", "Invalid native MTP state", error); + setSnapshot({ + ...next, + identified: false, + error: "Invalid ClientStateSync payload", + }); + } + }, []); + + const dispatchMessage = useCallback( + (message: MTPFrame) => { + bridge.dispatch(message); + }, + [bridge], + ); + + useEffect(() => { + return attachSubscriptions(connection); + }, [attachSubscriptions, connection]); + + useEffect( + () => + subscribe("GetStates", (message) => { + setFreshContacts((contacts) => + removeMissingContacts(contacts, message), + ); + }), + [subscribe], + ); + + useEffect(() => { + if (props.blockConnection) return; + let disposed = false; + let unlisten: UnlistenFn | undefined; + void (async () => { + try { + const nextUnlisten = await listen< + | { kind: "state"; snapshot: NativeSnapshot } + | { kind: "message"; generation: number; message: MTPFrame } + | { kind: "log"; level: number; message: string; details?: unknown } + >("mtp://event", ({ payload }) => { + if (disposed) return; + if (payload.kind === "state") { + void applySnapshot(payload.snapshot); + } else if (payload.kind === "message") { + if (payload.generation === generationRef.current) { + dispatchMessage(payload.message); + } + } else { + log( + payload.level, + "android", + "orange", + payload.message, + payload.details, + ); + } + }); + if (disposed) nextUnlisten(); + else unlisten = nextUnlisten; + } catch (error) { + log(0, "mtp", "red", "Failed to subscribe to native MTP events", error); + } + try { + const current = await invoke("mtp_status"); + if (!disposed) await applySnapshot(current); + } catch (error) { + log(0, "mtp", "red", "Failed to load native MTP status", error); + } + })(); + return () => { + disposed = true; + unlisten?.(); + }; + }, [applySnapshot, dispatchMessage, props.blockConnection]); + + useEffect(() => { + if (props.blockConnection) return; + const updateVisibility = () => { + void invoke("mtp_set_ui_visible", { + visible: document.visibilityState === "visible" && document.hasFocus(), + }); + }; + updateVisibility(); + document.addEventListener("visibilitychange", updateVisibility); + window.addEventListener("focus", updateVisibility); + window.addEventListener("blur", updateVisibility); + return () => { + document.removeEventListener("visibilitychange", updateVisibility); + window.removeEventListener("focus", updateVisibility); + window.removeEventListener("blur", updateVisibility); + void invoke("mtp_set_ui_visible", { visible: false }); + }; + }, [props.blockConnection]); + + const send = useCallback( + async (type, data, options) => { + const response = await connection.request(type, data, options); + if (response.type === "GetStates") { + setFreshContacts((contacts) => + removeMissingContacts( + contacts, + response as ProtocolMessage<"GetStates">, + ), + ); + } + for (const interceptor of interceptorsRef.current) { + void Promise.resolve(interceptor({ type, data, response })).catch( + (error) => { + log(1, "mtp", "yellow", "MTP interceptor failed", error, { type }); + }, + ); + } + return response; + }, + [connection, interceptorsRef], + ); + const connected = snapshot.readyState === ConnectionState.Connected; + + return ( + + {props.children} + + ); +} diff --git a/packages/mtp/src/values.ts b/packages/mtp/src/values.ts index d9f6cd4..c5ba321 100644 --- a/packages/mtp/src/values.ts +++ b/packages/mtp/src/values.ts @@ -1,3 +1,8 @@ export const RETRY_INTERVAL = 3_000; export const RECONNECT_TRIES = 3; export const RECONNECT_RESET = 6; +export const RECONNECT_LONG_INTERVAL = 60_000; +export const RECONNECT_JITTER = 0.2; +export const DISCOVERY_TIMEOUT = 20_000; +export const INITIAL_SYNC_TIMEOUT = 120_000; +export const STATE_ACK_TIMEOUT = 30_000; diff --git a/packages/notifications/src/context.tsx b/packages/notifications/src/context.tsx index b5ddf0d..e0da277 100644 --- a/packages/notifications/src/context.tsx +++ b/packages/notifications/src/context.tsx @@ -16,7 +16,6 @@ import { useLocation, useNavigate } from "@tanstack/react-router"; import { decryptChatText } from "@tensamin/crypto/chatSecret"; import { log } from "@tensamin/shared/log"; import { playSound } from "@tensamin/shared/sounds"; -import { type RawMessage } from "@tensamin/chat/values"; export const context = createContext(undefined); @@ -30,7 +29,7 @@ async function requestNotificationPermission() { } export default function Provider(props: { children: React.ReactNode }) { - const { subscribePush, send } = useMTP(); + const { subscribe, send } = useMTP(); const { load } = useStorage(); const { get } = useUser(); const { addLiveMessage, chatSecret, getChatSecret, userId } = useChat(); @@ -39,144 +38,140 @@ export default function Provider(props: { children: React.ReactNode }) { const location = useLocation(); useEffect(() => { - return subscribePush(async (message) => { - if (message.type === "MessageLive") { - const data = message.data as { - Message?: RawMessage; - SenderId?: number; - }; + return subscribe("MessageLive", async ({ data }) => { + if (!data.SenderId) return; - if (!data.SenderId) return; + const isCurrentChat = + location.pathname === "/chat" && userId === data.SenderId; + const appFocused = + document.hasFocus() && document.visibilityState === "visible"; + const shouldAlert = !isCurrentChat || !appFocused; - const isCurrentChat = - location.pathname === "/chat" && userId === data.SenderId; - const appFocused = - document.hasFocus() && document.visibilityState === "visible"; - const shouldAlert = !isCurrentChat || !appFocused; + const messageSecret = + isCurrentChat && chatSecret + ? chatSecret + : await getChatSecret(data.SenderId); - const messageSecret = - isCurrentChat && chatSecret - ? chatSecret - : await getChatSecret(data.SenderId); + if (!data.Message || !messageSecret) return; + if (shouldAlert) playSound("message"); - if (!data.Message || !messageSecret) return; - if (shouldAlert) playSound("message"); + void decryptChatText(messageSecret, data.Message.Content) + .catch((err) => { + log(1, "chat", "red", "Failed to decrypt live message", err, { + SendTime: data.Message?.SendTime, + }); + return null; + }) + .then(async (content) => { + if (!data.Message || !content || !data.SenderId) return; - void decryptChatText(messageSecret, data.Message.Content) - .catch((err) => { - log(1, "chat", "red", "Failed to decrypt live message", err, { - SendTime: data.Message?.SendTime, + if (isCurrentChat) { + addLiveMessage({ + ...data.Message, + Content: content ?? "Failed to decrypt message", + decryptionFailed: content === null, }); - return null; - }) - .then(async (content) => { - if (!data.Message || !content || !data.SenderId) return; + } - if (isCurrentChat) { - addLiveMessage({ - ...data.Message, - Content: content ?? "Failed to decrypt message", - decryptionFailed: content === null, + if (!shouldAlert) return; + + if (!isCurrentChat) { + // todo: add notification symbol to conversation cards (incl. message start) + moveUserIdToTop(data.SenderId); + + if (await load("settings.receive_confirmations")) { + void send("MessageState", { + MessageState: "received", }); } + } - if (!shouldAlert) return; + const user = await get(data.SenderId, [ + "UserId", + "Display", + "Avatar", + ]); - if (!isCurrentChat) { - // todo: add notification symbol to conversation cards (incl. message start) - moveUserIdToTop(data.SenderId); + if (isTauri()) { + if (!appFocused) return; + const permissionGranted = + (await isTauriNotificationPermissionGranted()) || + (await requestTauriNotificationPermission()) === "granted"; - if (await load("settings.receive_confirmations")) { - void send( - "MessageState", + if (permissionGranted) { + let handledNatively = false; + try { + handledNatively = await invoke( + "mtp_post_message_notification", { - MessageState: "received", - }, - { - id: data.Message.SendTime, + senderId: user.UserId, + sender: user.Display, + body: content, + avatar: user.Avatar, }, ); + } catch (error) { + log( + 1, + "notifications", + "red", + "Failed to create native message notification", + error, + ); + } + + if (!handledNatively) { + sendTauriNotification({ title: user.Display, body: content }); } } + } else { + const hasPermissions = await requestNotificationPermission(); - const user = await get(data.SenderId, [ - "UserId", - "Display", - "Avatar", - ]); - - if (isTauri()) { - if (!appFocused) return; - const permissionGranted = - (await isTauriNotificationPermissionGranted()) || - (await requestTauriNotificationPermission()) === "granted"; - - if (permissionGranted) { - let handledNatively = false; - try { - handledNatively = await invoke( - "mtp_post_message_notification", - { - senderId: user.UserId, - sender: user.Display, - body: content, - avatar: user.Avatar, - }, - ); - } catch (error) { - log( - 1, - "notifications", - "red", - "Failed to create native message notification", - error, - ); - } - - if (!handledNatively) { - sendTauriNotification({ title: user.Display, body: content }); - } - } - } else { - const hasPermissions = await requestNotificationPermission(); - - if (hasPermissions) { - const notification = new Notification(user.Display, { - body: content, - icon: user.Avatar || user.Display.slice(0, 2).toUpperCase(), - badge: user.Avatar || user.Display.slice(0, 2).toUpperCase(), - tag: `message-${user.UserId}`, - silent: true, - }); - - notification.onclick = () => { - window.focus(); - navigate({ - to: `/chat?id=${user.UserId}`, + if (hasPermissions) { + const options: NotificationOptions = { + body: content, + icon: user.Avatar || "/icons/icon-192.png", + badge: "/icons/notification-badge.png", + tag: `message-${user.UserId}`, + silent: true, + }; + if ("serviceWorker" in navigator) { + const registration = + await navigator.serviceWorker.getRegistration(); + if (registration) { + await registration.showNotification(user.Display, { + ...options, + data: { url: `/chat?id=${user.UserId}` }, }); - - notification.close(); - }; - } else { - sonnerToast(user.Display, { - classNames: { - content: "pl-4", - }, - description: content, - icon: ( - - - - {user.Display.slice(0, 2).toUpperCase()} - - - ), - }); + return; + } } + const notification = new Notification(user.Display, options); + notification.onclick = () => { + window.focus(); + navigate({ + to: `/chat?id=${user.UserId}`, + }); + notification.close(); + }; + } else { + sonnerToast(user.Display, { + classNames: { + content: "pl-4", + }, + description: content, + icon: ( + + + + {user.Display.slice(0, 2).toUpperCase()} + + + ), + }); } - }); - return; - } + } + }); }); }, [ addLiveMessage, @@ -187,7 +182,7 @@ export default function Provider(props: { children: React.ReactNode }) { navigate, send, moveUserIdToTop, - subscribePush, + subscribe, getChatSecret, userId, ]); diff --git a/packages/onboarding/src/index.tsx b/packages/onboarding/src/index.tsx index 5efa9c9..0184776 100644 --- a/packages/onboarding/src/index.tsx +++ b/packages/onboarding/src/index.tsx @@ -21,17 +21,37 @@ export { type OnboardingStepControls, } from "@methanium/ui"; - - interface GateState { docs: z.infer; acceptedPP: boolean; acceptedTOS: boolean; + changedPP: boolean; + changedTOS: boolean; includeLegal: boolean; includeOnboarding: boolean; includeTauriPermissions: boolean; } +async function fetchLegalDocumentHash(document: string) { + const response = await fetch( + `https://legal.methanium.net/tensamin/${document}/raw`, + ); + if (!response.ok) { + throw new Error(`Legal document request failed: ${response.status}`); + } + if (!response.headers.get("content-type")?.startsWith("text/plain")) { + throw new Error("Legal document request returned an invalid content type"); + } + + const hash = await crypto.subtle.digest( + "SHA-256", + await response.arrayBuffer(), + ); + return Array.from(new Uint8Array(hash), (byte) => + byte.toString(16).padStart(2, "0"), + ).join(""); +} + export default function OnboardingGate({ children }: { children: ReactNode }) { const { load, save } = useStorage(); const [state, setState] = useState(); @@ -51,25 +71,9 @@ export default function OnboardingGate({ children }: { children: ReactNode }) { void (async () => { try { - const response = await fetch("https://legal.tensamin.net/api/current"); - if (!response.ok) { - throw new Error(`Legal documents request failed: ${response.status}`); - } - - const current: unknown = await response.json(); - if (!active) return; - - const parsed = legalDocsSchema.safeParse(current); - if (!parsed.success) { - setError("Failed to load legal documents"); - setErrorDescription( - "The legal documents data received from the server is invalid. Please try again later.", - ); - log(0, "Legal", "red", "Invalid legal documents data", parsed.error); - return; - } - const [ + ppHash, + tosHash, localDocs, acceptedPP, acceptedTOS, @@ -77,6 +81,8 @@ export default function OnboardingGate({ children }: { children: ReactNode }) { onboardingStarted, tauriPermissionsDone, ] = await Promise.all([ + fetchLegalDocumentHash("privacy-policy"), + fetchLegalDocumentHash("terms-of-service"), load("legal_docs"), load("accepted_privacy_policy"), load("accepted_terms_of_service"), @@ -87,10 +93,16 @@ export default function OnboardingGate({ children }: { children: ReactNode }) { if (!active) return; + const docs = legalDocsSchema.parse({ + pp: { hash: ppHash }, + tos: { hash: tosHash }, + }); + const changedPP = acceptedPP && localDocs.pp.hash !== docs.pp.hash; + const changedTOS = acceptedTOS && localDocs.tos.hash !== docs.tos.hash; const currentAcceptedPP = - acceptedPP && localDocs.pp.hash === parsed.data.pp.hash; + acceptedPP && localDocs.pp.hash === docs.pp.hash; const currentAcceptedTOS = - acceptedTOS && localDocs.tos.hash === parsed.data.tos.hash; + acceptedTOS && localDocs.tos.hash === docs.tos.hash; const existingUser = acceptedPP && acceptedTOS; const includeOnboarding = !onboardingDone && (!existingUser || onboardingStarted); @@ -104,9 +116,11 @@ export default function OnboardingGate({ children }: { children: ReactNode }) { } setState({ - docs: parsed.data, + docs, acceptedPP: currentAcceptedPP, acceptedTOS: currentAcceptedTOS, + changedPP, + changedTOS, includeLegal: !currentAcceptedPP || !currentAcceptedTOS, includeOnboarding, includeTauriPermissions: @@ -168,16 +182,28 @@ export default function OnboardingGate({ children }: { children: ReactNode }) { const steps: OnboardingStep[] = []; if (state.includeLegal) { + const changedDocuments = [ + state.changedPP && "Privacy Policy", + state.changedTOS && "Terms of Service", + ].filter(Boolean); + steps.push({ id: "legal", - title: "Privacy Policy & ToS", - description: `${state.docs.pp.version} / ${state.docs.tos.version}`, + title: + changedDocuments.length > 0 + ? "Legal documents changed" + : "Privacy Policy & ToS", + description: + changedDocuments.length > 0 + ? changedDocuments.join(" & ") + : "Review and accept our legal documents", defaultCanContinue: false, content: ( ), diff --git a/packages/onboarding/src/pages/legal.tsx b/packages/onboarding/src/pages/legal.tsx index 86c8133..6619ae8 100644 --- a/packages/onboarding/src/pages/legal.tsx +++ b/packages/onboarding/src/pages/legal.tsx @@ -1,21 +1,17 @@ import { useCallback, useState } from "react"; -import { Checkbox, Label, Link } from "@methanium/ui"; -import { legalDocsSchema } from "@tensamin/shared/features/legal/schema"; -import type { z } from "zod"; - -import { useOnboardingStep } from "@methanium/ui"; - - +import { Checkbox, Label, Link, useOnboardingStep } from "@methanium/ui"; export default function LegalPage({ - docs, initiallyAcceptedPP, initiallyAcceptedTOS, + changedPP, + changedTOS, onAccept, }: { - docs: z.infer; initiallyAcceptedPP: boolean; initiallyAcceptedTOS: boolean; + changedPP: boolean; + changedTOS: boolean; onAccept: () => Promise; }) { const [acceptedPP, setAcceptedPP] = useState(initiallyAcceptedPP); @@ -34,34 +30,61 @@ export default function LegalPage({ return (
    -
    - - -
    - - +
    + {!initiallyAcceptedPP && ( + + )} + {!initiallyAcceptedTOS && ( + + )}
    ); } +function LegalDocumentAcceptance({ + id, + name, + link, + changed, + checked, + onChange, +}: { + id: string; + name: string; + link: string; + changed: boolean; + checked: boolean; + onChange: (checked: boolean) => void; +}) { + return ( +
    + + +
    + ); +} + function BigCheckbox({ id, label, diff --git a/packages/settings/src/layout.tsx b/packages/settings/src/layout.tsx index 53759cd..5d49f0b 100644 --- a/packages/settings/src/layout.tsx +++ b/packages/settings/src/layout.tsx @@ -1,8 +1,8 @@ -import { Outlet, useLocation, useNavigate } from "@tanstack/react-router"; -import { Button, ClearStorageButton, cn, useIsMobile } from "@methanium/ui"; +import { Outlet, useLocation } from "@tanstack/react-router"; +import { Button, useIsMobile } from "@methanium/ui"; import { ArrowLeft } from "lucide-react"; -import { settingsNavigation } from "./manifest"; +import { SettingsSidebar } from "./sidebar"; export default function SettingsLayout() { const isMobile = useIsMobile(); @@ -36,47 +36,3 @@ export default function SettingsLayout() {
    ); } - -export function SettingsSidebar({ - mobile = false, - className, -}: { - mobile?: boolean; - className?: string; -}) { - const navigate = useNavigate(); - const categories = [ - ...new Set(settingsNavigation.map((page) => page.category)), - ]; - - return ( -
    - {categories.map((category) => ( -
    -

    {category}

    - {settingsNavigation - .filter((page) => page.category === category) - .map((page) => ( - - ))} -
    - ))} -
    - -
    -
    - ); -} diff --git a/packages/settings/src/manifest.ts b/packages/settings/src/manifest.ts index 41fe14a..1afdd5a 100644 --- a/packages/settings/src/manifest.ts +++ b/packages/settings/src/manifest.ts @@ -8,46 +8,24 @@ import Profile from "./pages/profile"; import Security from "./pages/security"; import Theme from "./pages/theme"; import Hotkeys from "./pages/hotkeys"; +import { settingsNavigation } from "./navigation"; + +const pageComponents = { + profile: Profile, + security: Security, + chat: Chat, + call: Call, + cache: Cache, + theme: Theme, + accessibility: Accessibility, + hotkeys: Hotkeys, + licenses: Licenses, +} as const; export const settingsPages = [ { path: "/", component: Index }, - { - category: "account", - path: "profile", - label: "Profile", - component: Profile, - }, - { - category: "account", - path: "security", - label: "Security", - component: Security, - }, - { category: "general", path: "chat", label: "Chat", component: Chat }, - { category: "general", path: "call", label: "Call", component: Call }, - { category: "application", path: "cache", label: "Cache", component: Cache }, - { category: "application", path: "theme", label: "Theme", component: Theme }, - { - category: "application", - path: "accessibility", - label: "Accessibility", - component: Accessibility, - }, - { - category: "application", - path: "hotkeys", - label: "Hotkeys", - component: Hotkeys, - }, - { - category: "application", - path: "licenses", - label: "Licenses", - component: Licenses, - }, + ...settingsNavigation.map((page) => ({ + ...page, + component: pageComponents[page.path], + })), ] as const; - -export const settingsNavigation = settingsPages.filter( - (page): page is Exclude<(typeof settingsPages)[number], { path: "/" }> => - page.path !== "/", -); diff --git a/packages/settings/src/navigation.ts b/packages/settings/src/navigation.ts new file mode 100644 index 0000000..b47fae9 --- /dev/null +++ b/packages/settings/src/navigation.ts @@ -0,0 +1,31 @@ +export const settingsNavigation = [ + { + category: "account", + path: "profile", + label: "Profile", + }, + { + category: "account", + path: "security", + label: "Security", + }, + { category: "general", path: "chat", label: "Chat" }, + { category: "general", path: "call", label: "Call" }, + { category: "application", path: "cache", label: "Cache" }, + { category: "application", path: "theme", label: "Theme" }, + { + category: "application", + path: "accessibility", + label: "Accessibility", + }, + { + category: "application", + path: "hotkeys", + label: "Hotkeys", + }, + { + category: "application", + path: "licenses", + label: "Licenses", + }, +] as const; diff --git a/packages/settings/src/pages/index.tsx b/packages/settings/src/pages/index.tsx index 803a0c1..b5d04e3 100644 --- a/packages/settings/src/pages/index.tsx +++ b/packages/settings/src/pages/index.tsx @@ -1,4 +1,4 @@ -import { SettingsSidebar } from "../layout"; +import { SettingsSidebar } from "../sidebar"; export default function Page() { return ( diff --git a/packages/settings/src/sidebar.tsx b/packages/settings/src/sidebar.tsx new file mode 100644 index 0000000..3a2155e --- /dev/null +++ b/packages/settings/src/sidebar.tsx @@ -0,0 +1,48 @@ +import { Button, ClearStorageButton, cn } from "@methanium/ui"; +import { useNavigate } from "@tanstack/react-router"; + +import { settingsNavigation } from "./navigation"; + +export function SettingsSidebar({ + mobile = false, + className, +}: { + mobile?: boolean; + className?: string; +}) { + const navigate = useNavigate(); + const categories = [ + ...new Set(settingsNavigation.map((page) => page.category)), + ]; + + return ( +
    + {categories.map((category) => ( +
    +

    {category}

    + {settingsNavigation + .filter((page) => page.category === category) + .map((page) => ( + + ))} +
    + ))} +
    + +
    +
    + ); +} diff --git a/packages/shared/src/data.ts b/packages/shared/src/data.ts index 28c5966..eed6e7e 100644 --- a/packages/shared/src/data.ts +++ b/packages/shared/src/data.ts @@ -580,20 +580,11 @@ export const storageDefaults: Storage = { analytics_done: false, ...settingsStorageDefaults, legal_docs: { - eula: { - version: "0.0", - hash: "000000000000", - unix: 0, - }, tos: { - version: "0.0", - hash: "000000000000", - unix: 0, + hash: "0000000000000000000000000000000000000000000000000000000000000000", }, pp: { - version: "0.0", - hash: "000000000000", - unix: 0, + hash: "0000000000000000000000000000000000000000000000000000000000000000", }, }, cached_contacts: [], diff --git a/packages/shared/src/features/legal/schema.ts b/packages/shared/src/features/legal/schema.ts index cbdbf4e..26d35b1 100644 --- a/packages/shared/src/features/legal/schema.ts +++ b/packages/shared/src/features/legal/schema.ts @@ -1,13 +1,10 @@ import { z } from "zod"; const legalDocSchema = z.object({ - version: z.string().regex(/^\d+\.\d+$/), - hash: z.string().regex(/^[a-f0-9]{12}$/), - unix: z.number().int().positive(), + hash: z.string().regex(/^[a-f0-9]{64}$/), }); export const legalDocsSchema = z.object({ - eula: legalDocSchema, tos: legalDocSchema, pp: legalDocSchema, }); diff --git a/packages/storage/package.json b/packages/storage/package.json index 9e19759..dce1554 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -6,7 +6,9 @@ "exports": { "./session": "./src/session.tsx", "./context": "./src/context.tsx", - "./secure": "./src/secure.ts" + "./secure": "./src/secure.ts", + "./browserSecure": "./src/browserSecure.ts", + "./credentials": "./src/credentials.ts" }, "scripts": { "format": "pnpm exec prettier --write .", diff --git a/packages/storage/src/browserSecure.ts b/packages/storage/src/browserSecure.ts new file mode 100644 index 0000000..494b5f0 --- /dev/null +++ b/packages/storage/src/browserSecure.ts @@ -0,0 +1,40 @@ +import { getDatabaseEntry } from "@tensamin/shared/indexedDb"; + +type SecureEnvelope = { + __tensaminSecure: 1; + version: 1; + iv: string; + data: string; +}; + +function base64ToBytes(value: string) { + const binary = atob(value); + return Uint8Array.from(binary, (character) => character.charCodeAt(0)); +} + +function isSecureEnvelope(value: unknown): value is SecureEnvelope { + if (!value || typeof value !== "object") return false; + const envelope = value as Partial; + return ( + envelope.__tensaminSecure === 1 && + envelope.version === 1 && + typeof envelope.iv === "string" && + typeof envelope.data === "string" + ); +} + +export async function loadSecureBrowserValue(key: string) { + const stored = await getDatabaseEntry("storage", key); + if (stored === undefined || !isSecureEnvelope(stored)) { + return stored as T | undefined; + } + + const masterKey = await getDatabaseEntry("keys", "master-v1"); + if (!masterKey) throw new Error("Secure storage key is unavailable."); + const plaintext = await crypto.subtle.decrypt( + { name: "AES-GCM", iv: base64ToBytes(stored.iv) }, + masterKey, + base64ToBytes(stored.data), + ); + return JSON.parse(new TextDecoder().decode(plaintext)) as T; +} diff --git a/packages/storage/src/context.tsx b/packages/storage/src/context.tsx index 27d8759..407adfe 100644 --- a/packages/storage/src/context.tsx +++ b/packages/storage/src/context.tsx @@ -30,7 +30,7 @@ import { export type SaveOptions = { secure?: boolean }; -interface StorageContextValue { +export interface StorageContextValue { load(key: K): Promise; save( key: K, diff --git a/packages/storage/src/credentials.ts b/packages/storage/src/credentials.ts new file mode 100644 index 0000000..7562992 --- /dev/null +++ b/packages/storage/src/credentials.ts @@ -0,0 +1,68 @@ +import { invoke, isTauri } from "@tauri-apps/api/core"; + +import type { StorageContextValue } from "./context"; + +export function parseTuFileContent(rawFileContent: string): { + userId: number; + privateKey: string; + domain: string | null; +} { + const content = rawFileContent.trim(); + const separator = content.indexOf("::"); + if (separator <= 0 || separator !== content.lastIndexOf("::")) { + throw new Error("Invalid file"); + } + + const identity = content.slice(0, separator); + const privateKey = content.slice(separator + 2).trim(); + const [userIdValue, domain, ...extraDomainParts] = identity.split("@"); + const userId = Number(userIdValue); + if ( + !Number.isSafeInteger(userId) || + userId <= 0 || + !privateKey || + extraDomainParts.length > 0 || + (identity.includes("@") && !domain) + ) { + throw new Error("Invalid file"); + } + + return { userId, privateKey, domain: domain ?? null }; +} + +export async function persistMtpCredentials({ + storage, + userId, + keyring, + domain, +}: { + storage: Pick; + userId: number; + keyring: string; + domain?: string | null; +}) { + const omegaUrl = domain + ? `https://${domain}/` + : await storage.load("omega_url"); + if (domain) await storage.save("omega_url", omegaUrl); + + if (isTauri()) { + const [forcedOmikronUrl, forcedOmikronPublicKey] = await Promise.all([ + storage.load("forced_omikron_url"), + storage.load("forced_omikron_public_key"), + ]); + await invoke("mtp_store_credentials", { + config: { + userId, + keyring, + omegaUrl, + forcedOmikronUrl, + forcedOmikronPublicKey, + }, + }); + } + + await storage.save("mtp_keyring", keyring, { secure: true }); + await storage.save("session_id", Date.now()); + await storage.save("user_id", userId); +} diff --git a/packages/storage/src/secure.ts b/packages/storage/src/secure.ts index 283d5c0..367c494 100644 --- a/packages/storage/src/secure.ts +++ b/packages/storage/src/secure.ts @@ -4,10 +4,7 @@ import { getDatabaseEntry, setDatabaseEntry } from "@tensamin/shared/indexedDb"; export type SecureStorageStatus = { backend: - | "electron-keyring" - | "application-storage" - | "webcrypto" - | "indexeddb"; + "electron-keyring" | "application-storage" | "webcrypto" | "indexeddb"; secure: boolean; reason?: string; }; @@ -71,7 +68,12 @@ async function getKey() { if (!globalThis.crypto?.subtle || typeof indexedDB === "undefined") { return null; } - if (window.tensaminDesktop?.secureStorage) return loadElectronKey(); + if ( + typeof window !== "undefined" && + window.tensaminDesktop?.secureStorage + ) { + return loadElectronKey(); + } return loadBrowserKey(); })().catch(() => null); return keyPromise; @@ -120,7 +122,10 @@ export async function decodeSecureValue(value: unknown): Promise { } export async function getSecureStorageStatus(): Promise { - const desktop = window.tensaminDesktop?.secureStorage; + const desktop = + typeof window === "undefined" + ? undefined + : window.tensaminDesktop?.secureStorage; if (desktop?.getStatus) { const status = await desktop.getStatus(); if (status.available) return { backend: "electron-keyring", secure: true }; diff --git a/packages/user/src/context.tsx b/packages/user/src/context.tsx index 423661c..8c41aa4 100644 --- a/packages/user/src/context.tsx +++ b/packages/user/src/context.tsx @@ -9,13 +9,12 @@ import { useState, useSyncExternalStore, } from "react"; -import { useMTP, type ProtocolMessage } from "@tensamin/mtp"; +import { useMTP } from "@tensamin/mtp"; import { clientUserStateSchema, mtp as schemas, publicUserStateSchema, - userStateEntrySchema, } from "@tensamin/shared/data"; import type z from "zod"; import { createCache } from "@tensamin/cache"; @@ -89,7 +88,7 @@ export default function UserProvider(props: { children: ReactNode }) { ); const revisionsRef = useRef(new Map>()); - const { send, subscribePush } = useMTP(); + const { send, subscribe: subscribeMTP } = useMTP(); const { load } = useStorage(); const { contacts } = useSession(); const [accountId, setAccountId] = useState(null); @@ -174,39 +173,6 @@ export default function UserProvider(props: { children: ReactNode }) { [publishUser], ); - const handleStatePush = useCallback( - async (message: ProtocolMessage) => { - if (!accountId) return; - const data = message.data as Record; - if (message.type === "GetStates") { - if (Array.isArray(data.MissingUserIds)) { - for (const userId of data.MissingUserIds) { - if (typeof userId === "number") removePresence(userId); - } - } - if (!Array.isArray(data.UserStates)) return; - for (const entry of data.UserStates) { - const parsed = userStateEntrySchema.safeParse(entry); - if (!parsed.success) continue; - if (parsed.data.UserId === accountId) continue; - initialStatesRef.current.set( - parsed.data.UserId, - parsed.data.UserState, - ); - if (applyUserState(parsed.data.UserId, parsed.data.UserState)) { - initialStatesRef.current.delete(parsed.data.UserId); - } - } - return; - } - if (message.type !== "ClientChanged") return; - const parsed = schemas.ClientChanged.response.safeParse(data); - if (!parsed.success) return; - applyUserState(parsed.data.UserId, parsed.data.UserState, true); - }, - [accountId, applyUserState, removePresence], - ); - useEffect(() => { void load("user_id").then((accountId) => { accountIdRef.current = accountId; @@ -223,8 +189,24 @@ export default function UserProvider(props: { children: ReactNode }) { useEffect(() => { if (!accountId) return; - return subscribePush(handleStatePush); - }, [accountId, handleStatePush, subscribePush]); + const unsubscribeStates = subscribeMTP("GetStates", ({ data }) => { + for (const userId of data.MissingUserIds ?? []) removePresence(userId); + for (const entry of data.UserStates) { + if (!entry || entry.UserId === accountId) continue; + initialStatesRef.current.set(entry.UserId, entry.UserState); + if (applyUserState(entry.UserId, entry.UserState)) { + initialStatesRef.current.delete(entry.UserId); + } + } + }); + const unsubscribeChanged = subscribeMTP("ClientChanged", ({ data }) => { + applyUserState(data.UserId, data.UserState, true); + }); + return () => { + unsubscribeStates(); + unsubscribeChanged(); + }; + }, [accountId, applyUserState, removePresence, subscribeMTP]); const loadUser = useCallback( async (userId: number): Promise => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 38033b2..83d88ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,28 +5,28 @@ settings: excludeLinksFromLockfile: false overrides: - '@methanium/ui': https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz - mtp: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz + '@methanium/ui': https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz + mtp: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz importers: .: dependencies: '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz - version: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) + specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz + version: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) mtp: - specifier: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz - version: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz + specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz + version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz sonner: - specifier: ^2.0.7 - version: 2.0.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: ^2.0.8 + version: 2.0.8(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) devDependencies: '@eslint/js': specifier: ^10.0.1 version: 10.0.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)) '@types/node': - specifier: ^26.1.2 + specifier: ^26.2.0 version: 26.2.0 '@types/react': specifier: ^19.2.18 @@ -35,20 +35,20 @@ importers: specifier: ^19.2.4 version: 19.2.4(@types/react@19.2.18) '@typescript-eslint/parser': - specifier: ^8.66.0 - version: 8.66.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + specifier: ^8.67.0 + version: 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) eslint: - specifier: ^10.8.0 + specifier: ^10.8.1 version: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) eslint-plugin-react-hooks: specifier: ^7.1.1 version: 7.1.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) fallow: - specifier: ^3.14.0 - version: 3.14.0 + specifier: ^3.17.0 + version: 3.17.0 globals: - specifier: ^17.9.0 - version: 17.9.0 + specifier: ^17.11.0 + version: 17.11.0 prettier: specifier: ^3.9.6 version: 3.9.6 @@ -56,11 +56,11 @@ importers: specifier: ^6.0.3 version: 6.0.3 typescript-eslint: - specifier: ^8.66.0 - version: 8.66.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + specifier: ^8.67.0 + version: 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) vitest: - specifier: ^4.1.10 - version: 4.1.10(@types/node@26.2.0)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + specifier: ^4.1.11 + version: 4.1.11(@types/node@26.2.0)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) apps/electron: devDependencies: @@ -80,17 +80,69 @@ importers: specifier: ~6.0.3 version: 6.0.3 - apps/tauri: + apps/pwa: dependencies: '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz - version: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) + specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz + version: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) + '@tauri-apps/api': + specifier: ^2.11.1 + version: 2.11.1 + '@tensamin/crypto': + specifier: workspace:* + version: link:../../packages/crypto + '@tensamin/shared': + specifier: workspace:* + version: link:../../packages/shared + '@tensamin/storage': + specifier: workspace:* + version: link:../../packages/storage + mtp: + specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz + version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz + react: + specifier: ^19.2.8 + version: 19.2.8 + sonner: + specifier: ^2.0.7 + version: 2.0.8(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + vite-plugin-pwa: + specifier: ^1.1.0 + version: 1.3.0(supports-color@7.2.0)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(workbox-build@7.4.1(supports-color@7.2.0))(workbox-window@7.4.1) + workbox-core: + specifier: ^7.3.0 + version: 7.4.1 + workbox-precaching: + specifier: ^7.3.0 + version: 7.4.1 + workbox-routing: + specifier: ^7.3.0 + version: 7.4.1 + workbox-strategies: + specifier: ^7.3.0 + version: 7.4.1 + devDependencies: + '@types/node': + specifier: ^26.1.2 + version: 26.2.0 + '@types/react': + specifier: ^19.2.18 + version: 19.2.18 + typescript: + specifier: ~6.0.3 + version: 6.0.3 + vite: + specifier: ^8.2.1 + version: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0) + + apps/tauri: + dependencies: + '@methanium/ui': + specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz + version: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) '@tauri-apps/api': specifier: ^2.11.1 version: 2.11.1 - '@tauri-apps/plugin-barcode-scanner': - specifier: ~2.4.5 - version: 2.4.5 '@tauri-apps/plugin-deep-link': specifier: ~2.4.9 version: 2.4.9 @@ -117,11 +169,11 @@ importers: specifier: ^5.3.0 version: 5.3.0 '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz - version: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) + specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz + version: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) '@tailwindcss/vite': specifier: ^4.3.3 - version: 4.3.3(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.3.3(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) '@tanstack/react-router': specifier: ^1.170.21 version: 1.170.23(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -155,6 +207,9 @@ importers: '@tensamin/onboarding': specifier: workspace:* version: link:../../packages/onboarding + '@tensamin/pwa': + specifier: workspace:* + version: link:../pwa '@tensamin/settings': specifier: workspace:* version: link:../../packages/settings @@ -224,7 +279,7 @@ importers: version: 19.2.4(@types/react@19.2.18) '@vitejs/plugin-react': specifier: ^6.0.5 - version: 6.0.5(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 6.0.5(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) esbuild: specifier: ^0.28.1 version: 0.28.1 @@ -234,6 +289,9 @@ importers: globals: specifier: ^17.9.0 version: 17.9.0 + mtp: + specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz + version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz typescript: specifier: ~6.0.3 version: 6.0.3 @@ -242,7 +300,7 @@ importers: version: 8.66.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) vite: specifier: ^8.2.1 - version: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) + version: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0) packages/cache: dependencies: @@ -268,8 +326,8 @@ importers: specifier: ^2.9.23 version: 2.9.23(livekit-client@2.21.0(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tslib@2.8.1) '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz - version: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) + specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz + version: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) '@tanstack/react-router': specifier: ^1.170.21 version: 1.170.23(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -297,6 +355,9 @@ importers: lucide-react: specifier: ^1.29.0 version: 1.30.0(react@19.2.8) + mtp: + specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz + version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz react: specifier: ^19.2.8 version: 19.2.8 @@ -316,8 +377,8 @@ importers: packages/chat: dependencies: '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz - version: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) + specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz + version: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) '@tanstack/pacer': specifier: ^0.21.1 version: 0.21.1 @@ -369,6 +430,9 @@ importers: packages/crypto: dependencies: + mtp: + specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz + version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz react: specifier: ^19.2.8 version: 19.2.8 @@ -393,19 +457,55 @@ importers: devDependencies: vite: specifier: ^8.2.1 - version: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) + version: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0) + + packages/markdown: + dependencies: + '@codemirror/autocomplete': + specifier: ^6.20.3 + version: 6.20.3 + '@codemirror/commands': + specifier: ^6.10.4 + version: 6.11.0 + '@codemirror/lang-markdown': + specifier: ^6.5.2 + version: 6.5.2 + '@codemirror/language': + specifier: ^6.12.4 + version: 6.12.4 + '@codemirror/state': + specifier: ^6.7.1 + version: 6.7.1 + '@codemirror/view': + specifier: ^6.43.8 + version: 6.43.9 + '@methanium/ui': + specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz + version: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) + '@twemoji/api': + specifier: ^17.0.3 + version: 17.0.3 + emojibase-data: + specifier: ^17.0.0 + version: 17.0.0(emojibase@17.0.0) + lucide-react: + specifier: ^1.30.0 + version: 1.32.0(react@19.2.8) + react: + specifier: ^19.2.8 + version: 19.2.8 + react-dom: + specifier: ^19.2.8 + version: 19.2.8(react@19.2.8) packages/mtp: dependencies: '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz - version: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) + specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz + version: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) '@tauri-apps/api': specifier: ^2.11.1 version: 2.11.1 - '@tensamin/crypto': - specifier: workspace:* - version: link:../crypto '@tensamin/shared': specifier: workspace:* version: link:../shared @@ -413,17 +513,11 @@ importers: specifier: workspace:* version: link:../storage mtp: - specifier: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz - version: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz + specifier: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz + version: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz react: specifier: ^19.2.8 version: 19.2.8 - react-dom: - specifier: ^19.2.8 - version: 19.2.8(react@19.2.8) - zod: - specifier: ^4.4.3 - version: 4.4.3 devDependencies: eslint: specifier: ^10.8.0 @@ -432,8 +526,8 @@ importers: packages/notifications: dependencies: '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz - version: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) + specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz + version: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) '@tanstack/react-router': specifier: ^1.170.21 version: 1.170.23(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -474,8 +568,8 @@ importers: packages/onboarding: dependencies: '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz - version: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) + specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz + version: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) '@tauri-apps/api': specifier: ^2.11.1 version: 2.11.1 @@ -504,8 +598,8 @@ importers: packages/settings: dependencies: '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz - version: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) + specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz + version: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) '@tanstack/react-router': specifier: ^1.170.21 version: 1.170.23(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -542,13 +636,13 @@ importers: devDependencies: vite: specifier: ^8.2.1 - version: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) + version: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0) packages/shared: dependencies: '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz - version: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) + specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz + version: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) lucide-react: specifier: ^1.29.0 version: 1.30.0(react@19.2.8) @@ -565,8 +659,8 @@ importers: packages/storage: dependencies: '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz - version: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) + specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz + version: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) '@tauri-apps/api': specifier: ^2.11.1 version: 2.11.1 @@ -589,8 +683,8 @@ importers: packages/tauth: dependencies: '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz - version: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) + specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz + version: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3) '@tanstack/react-router': specifier: ^1.170.21 version: 1.170.23(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -645,6 +739,12 @@ importers: packages: + '@apideck/better-ajv-errors@0.3.7': + resolution: {integrity: sha512-TajUJwGWbDwkCx/CZi7tRE8PVB7simCvKJfHUsSdvps+aTM/PDPP4gkLmKnc+x3CE//y9i/nj74GqdL/hwk7Iw==} + engines: {node: '>=10'} + peerDependencies: + ajv: '>=8' + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -675,6 +775,17 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-create-regexp-features-plugin@7.29.7': + resolution: {integrity: sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.8': + resolution: {integrity: sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + '@babel/helper-globals@7.29.7': resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} engines: {node: '>=6.9.0'} @@ -701,6 +812,12 @@ packages: resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} engines: {node: '>=6.9.0'} + '@babel/helper-remap-async-to-generator@7.29.7': + resolution: {integrity: sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-replace-supers@7.29.7': resolution: {integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==} engines: {node: '>=6.9.0'} @@ -723,6 +840,10 @@ packages: resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} engines: {node: '>=6.9.0'} + '@babel/helper-wrap-function@7.29.7': + resolution: {integrity: sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==} + engines: {node: '>=6.9.0'} + '@babel/helpers@7.29.7': resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} engines: {node: '>=6.9.0'} @@ -732,6 +853,60 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.29.7': + resolution: {integrity: sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.29.7': + resolution: {integrity: sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.29.7': + resolution: {integrity: sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.7': + resolution: {integrity: sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.29.7': + resolution: {integrity: sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.29.7': + resolution: {integrity: sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-assertions@7.29.7': + resolution: {integrity: sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.29.7': + resolution: {integrity: sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-jsx@7.29.7': resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} engines: {node: '>=6.9.0'} @@ -744,18 +919,335 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-arrow-functions@7.29.7': + resolution: {integrity: sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.29.7': + resolution: {integrity: sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.29.7': + resolution: {integrity: sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoped-functions@7.29.7': + resolution: {integrity: sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.29.7': + resolution: {integrity: sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.29.7': + resolution: {integrity: sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-static-block@7.29.7': + resolution: {integrity: sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + + '@babel/plugin-transform-classes@7.29.7': + resolution: {integrity: sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-computed-properties@7.29.7': + resolution: {integrity: sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.29.7': + resolution: {integrity: sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-dotall-regex@7.29.7': + resolution: {integrity: sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-keys@7.29.7': + resolution: {integrity: sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.7': + resolution: {integrity: sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-dynamic-import@7.29.7': + resolution: {integrity: sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-explicit-resource-management@7.29.7': + resolution: {integrity: sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-exponentiation-operator@7.29.7': + resolution: {integrity: sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-export-namespace-from@7.29.7': + resolution: {integrity: sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.29.7': + resolution: {integrity: sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-function-name@7.29.7': + resolution: {integrity: sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-json-strings@7.29.7': + resolution: {integrity: sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-literals@7.29.7': + resolution: {integrity: sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-logical-assignment-operators@7.29.7': + resolution: {integrity: sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-member-expression-literals@7.29.7': + resolution: {integrity: sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-amd@7.29.7': + resolution: {integrity: sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-modules-commonjs@7.29.7': resolution: {integrity: sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-modules-systemjs@7.29.8': + resolution: {integrity: sha512-6iSnEK0zlkLKU4heofK/AdmRD4e2SHVpJMtrwnTCzhnaM98ria4rTrOXBBi45BTTYnJtO8txnPsX4fChYXkmeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-umd@7.29.7': + resolution: {integrity: sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.29.7': + resolution: {integrity: sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-new-target@7.29.7': + resolution: {integrity: sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.29.7': + resolution: {integrity: sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-numeric-separator@7.29.7': + resolution: {integrity: sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.29.7': + resolution: {integrity: sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-super@7.29.7': + resolution: {integrity: sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.29.7': + resolution: {integrity: sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.29.7': + resolution: {integrity: sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.29.7': + resolution: {integrity: sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.29.7': + resolution: {integrity: sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.29.7': + resolution: {integrity: sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.29.7': + resolution: {integrity: sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.29.8': + resolution: {integrity: sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regexp-modifiers@7.29.7': + resolution: {integrity: sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-reserved-words@7.29.7': + resolution: {integrity: sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.29.7': + resolution: {integrity: sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.29.8': + resolution: {integrity: sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.29.7': + resolution: {integrity: sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.29.7': + resolution: {integrity: sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.29.7': + resolution: {integrity: sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-typescript@7.29.7': resolution: {integrity: sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-unicode-escapes@7.29.7': + resolution: {integrity: sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.29.7': + resolution: {integrity: sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.29.7': + resolution: {integrity: sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.29.7': + resolution: {integrity: sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.29.7': + resolution: {integrity: sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + '@babel/preset-typescript@7.29.7': resolution: {integrity: sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==} engines: {node: '>=6.9.0'} @@ -811,8 +1303,8 @@ packages: '@codemirror/autocomplete@6.20.3': resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} - '@codemirror/commands@6.10.4': - resolution: {integrity: sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==} + '@codemirror/commands@6.11.0': + resolution: {integrity: sha512-/K4Rl5BN0OtTiPWmJCdqODu38XnDMsDxKY5rgrPnCkutPTJf2wVbkoixLfealF5Kwse/s8P8M5jAiURiwSwnFA==} '@codemirror/lang-css@6.3.1': resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} @@ -835,8 +1327,8 @@ packages: '@codemirror/state@6.7.1': resolution: {integrity: sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==} - '@codemirror/view@6.43.8': - resolution: {integrity: sha512-qtItTDssZ/5GFfi94hrILu9j/VUeFPDPkhovEfmWFj2ipTxnzPB8DdHgfbb8HYTzLTYhrndKmyQxXUz/PDLenw==} + '@codemirror/view@6.43.9': + resolution: {integrity: sha512-sTuUzTpPMFebRhg6dawChoKKgndIwfjmJgKVxBefPElcU2NwQ6AFroupk0SFqEerQyZOGRfDNnSN8Dw/lMAsXw==} '@dotenvx/dotenvx@1.75.1': resolution: {integrity: sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==} @@ -1084,43 +1576,43 @@ packages: resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@fallow-cli/darwin-arm64@3.14.0': - resolution: {integrity: sha512-seaix3OqADcq90PkPOOPrN8Jl7QZDGjFziHA85Ikp+lGSHVjJ2IMyGh/QKLhvi9Q6Ha3Y//F0+PFFGNd0Gi4mw==} + '@fallow-cli/darwin-arm64@3.17.0': + resolution: {integrity: sha512-u9O4aQaTSdaZaa4MAVEZEiwOx6QQzChymyQPrxV6KvDcVbcl0oYSekJKdvN4gEqUY8ClMze9gWS1tbn/W3N4dQ==} cpu: [arm64] os: [darwin] - '@fallow-cli/darwin-x64@3.14.0': - resolution: {integrity: sha512-qgYZHHTFobuJCtON7aVe/tXCUjiF/9nKT4tNLVfUPH4Qe9KwieO4rAuknDOnxkjzsWPO4MfHyCbcVdslf4JMnw==} + '@fallow-cli/darwin-x64@3.17.0': + resolution: {integrity: sha512-l3tZJovnCa8j+im4qTl4GVSfrx4gisJYeHDwb5TbcdTGaWYz42gHriAaz5MhOR8S24q3xRegX8QrI8PA4XXU4g==} cpu: [x64] os: [darwin] - '@fallow-cli/linux-arm64-gnu@3.14.0': - resolution: {integrity: sha512-FwmIG391lMViBXh1cAvOcwQxbI2sLjEYkugFauVbmntjuJt6xVxQXAxjybdFXSL/jjfFnbAzPKy9oHlhq3sA6A==} + '@fallow-cli/linux-arm64-gnu@3.17.0': + resolution: {integrity: sha512-IAjS6qR4tHcRo1y5lzYT6mktN/ATPyPNrJ6yjwTORwwQV1YfMO58A5zGBfJlPBY+lUmaMZ9w37ftvH9a2AS4dQ==} cpu: [arm64] os: [linux] - '@fallow-cli/linux-arm64-musl@3.14.0': - resolution: {integrity: sha512-x4R9aNT2kIoSHRIZ5s2MZ+swyb3uutV6SSYCwWhPSE5UI/oCEYWNWiYS7uKMLM0cTuMPVcsm2qMTd4iYA3ACLw==} + '@fallow-cli/linux-arm64-musl@3.17.0': + resolution: {integrity: sha512-Oe1dvk5Wt5Bmnxfc6HufJTaFY9SEwDVrZpnSdVQlv+nvz7DzrTK1r91cR3Ip477ANSLa9XD/MHPMpfqqo0dlZg==} cpu: [arm64] os: [linux] - '@fallow-cli/linux-x64-gnu@3.14.0': - resolution: {integrity: sha512-8kBxy9FkwKjiH1C19T0nSkmbtldwVyWLQWajIqvyHE0x4rurHhve7RQrwEDPzNMFu7OgYCMU1nXr/AMIJ6Kg9w==} + '@fallow-cli/linux-x64-gnu@3.17.0': + resolution: {integrity: sha512-XgLU2a51zuky7gifjVDHuiJGToAReDsVCDcyDF84TQz/h3bqO0iRkpDL8dh7+HODku84+EffpK5ZHY19n6Ij0w==} cpu: [x64] os: [linux] - '@fallow-cli/linux-x64-musl@3.14.0': - resolution: {integrity: sha512-9TAdXSpYvk9WkKy2GR8T/PHbq7QCTO/SYUmiZ+EAJUMzXBvSfnpj8cCGQpeAD000Nb+o7rL94lIAPJMAqMKO3Q==} + '@fallow-cli/linux-x64-musl@3.17.0': + resolution: {integrity: sha512-GnDZx2kbLKKdYAp05NSKv6CZHgzEKvhRd0btCKFMMpUEhdzmr4uh2Qb4UzKDBg/q2r1EhkzN4b/qC+tbFCmtwA==} cpu: [x64] os: [linux] - '@fallow-cli/win32-arm64-msvc@3.14.0': - resolution: {integrity: sha512-xc495cracyJL+0UKwcqNhofIz2K5XiJbb+YFhxrbSrrbm+EVDnQKpCOzzOJYbm7RPxF6XOxJpATn7ciBy7Rqrw==} + '@fallow-cli/win32-arm64-msvc@3.17.0': + resolution: {integrity: sha512-u9n5WzPuKuC1PKegKQknWtngAUJ2szQaRXsbY54s7UMqeFNyC+W04LV5D+sEZywU7L0kzL+Kck9LxFLXBQcgvA==} cpu: [arm64] os: [win32] - '@fallow-cli/win32-x64-msvc@3.14.0': - resolution: {integrity: sha512-ihyCZ5GMoYGnzlPP6b5pZBBcGZhDHDW2u1YEGkZEuf3pbW6E45U7Hhf213cE2CCheScXOirK6AKWnLM/Qj4c2A==} + '@fallow-cli/win32-x64-msvc@3.17.0': + resolution: {integrity: sha512-/OcyxHuCKabzQ9hKIjM/CFJ9fr30n0Mw5MK6nzi5rn26mbYUgh073UZq4FONOpPJ/FqHtC726rxpgBtR4qS+gg==} cpu: [x64] os: [win32] @@ -1145,8 +1637,8 @@ packages: '@fontsource-variable/public-sans@5.3.0': resolution: {integrity: sha512-AVfkmAt50BMXWpOO21FAntiJFKGX6xTc2dSL8dxtDteONe9IuRXJWGbs0EbG955vAMCq23ENeuopuW87cGWDSQ==} - '@hono/node-server@2.1.0': - resolution: {integrity: sha512-XovyyCCnBzW+zKu+z/zq8hwNs4KOR5rEMAOxo2f40Q5xoOI37IMm6MIg2COOUtUApo0i6850MTBKH2u4QLGIqg==} + '@hono/node-server@2.1.1': + resolution: {integrity: sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==} engines: {node: '>=20'} peerDependencies: hono: ^4 @@ -1171,6 +1663,10 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} + '@isaacs/fs-minipass@4.0.1': resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} @@ -1185,6 +1681,9 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -1249,9 +1748,9 @@ packages: '@marijn/find-cluster-break@1.0.3': resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==} - '@methanium/ui@https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz': - resolution: {integrity: sha512-I0uTA21w9iHM1UQAtRTVJCMB93QgwB//PKTrnvwe5WjgU/EZ4H3wsC+zIfiQn4lpNCfoPtHhMjISWjxYLIUgsA==, tarball: https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz} - version: 0.0.28 + '@methanium/ui@https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz': + resolution: {integrity: sha512-ij9zo/PdP5l/F/70t7a1o+OFZbMFRNNseNc1dlqddUFqIkAy+71VL6ptrXNpeVtzKFq8LuycwzXGsGihuViVig==, tarball: https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz} + version: 0.0.29 peerDependencies: react: ^19.2.7 react-dom: ^19.2.7 @@ -1266,6 +1765,13 @@ packages: '@cfworker/json-schema': optional: true + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + '@noble/hashes@1.4.0': resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} @@ -1569,35 +2075,222 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/plugin-babel@6.1.0': + resolution: {integrity: sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@types/babel__core': ^7.1.9 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + '@types/babel__core': + optional: true + rollup: + optional: true + + '@rollup/plugin-node-resolve@16.0.3': + resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-replace@6.0.3': + resolution: {integrity: sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-terser@1.0.0': + resolution: {integrity: sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + rollup: ^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.62.4': + resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.4': + resolution: {integrity: sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.4': + resolution: {integrity: sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.4': + resolution: {integrity: sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.4': + resolution: {integrity: sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.4': + resolution: {integrity: sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.4': + resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.4': + resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.4': + resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.4': + resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.4': + resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.4': + resolution: {integrity: sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + resolution: {integrity: sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + resolution: {integrity: sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.4': + resolution: {integrity: sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.4': + resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==} + cpu: [x64] + os: [win32] + '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@shikijs/core@4.4.2': - resolution: {integrity: sha512-StyzbAyxg2/tBGf78gwbBkGyeQ73lf8UiJArFaQhTQIDqQOCKPCQFanvrs4/Yv3Yfyc+ONInJM6K+FMIf+P+kA==} + '@shikijs/core@4.4.3': + resolution: {integrity: sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==} engines: {node: '>=20'} - '@shikijs/engine-javascript@4.4.2': - resolution: {integrity: sha512-MnIkeqWdVPUWsxlx8gKLVCJFTsqrQJgpTPBPpQwaFeJ56lOnJxj5aN2LUFnfxUEcvOQuNocmbaMnVrCEln6rkw==} + '@shikijs/engine-javascript@4.4.3': + resolution: {integrity: sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==} engines: {node: '>=20'} - '@shikijs/engine-oniguruma@4.4.2': - resolution: {integrity: sha512-GLhowz1+jixjz+wiZ3wMnOn1jTxiFCGl2PkXufivbnwPHKuyw1AYqu5/hbWhZZ2oAb0NP05WUJhYeigY14drnw==} + '@shikijs/engine-oniguruma@4.4.3': + resolution: {integrity: sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==} engines: {node: '>=20'} - '@shikijs/langs@4.4.2': - resolution: {integrity: sha512-8DfeusD+Zdv/eYIDdXyJTUnSMHt+aAWjAOCXV20HNGAHRlInXpG8wh421v6B91WOm9TFwRLN+b/LG5F2NAIojg==} + '@shikijs/langs@4.4.3': + resolution: {integrity: sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==} engines: {node: '>=20'} - '@shikijs/primitive@4.4.2': - resolution: {integrity: sha512-l6fQQKsOMlz72n38fztmSgZ76MO6KSWuw8o+GJ+FhmqrpC9pIOJNQNXGgbb5yX2AwpzlEHwsaLPnk/8o4Fm+rA==} + '@shikijs/primitive@4.4.3': + resolution: {integrity: sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==} engines: {node: '>=20'} - '@shikijs/themes@4.4.2': - resolution: {integrity: sha512-H0CFoL07ddDC2Dd6EdrPYNkRhUR6YCkJlnuYFceYYUJJA5TIm2b5B33qqiDYryBExgbKMndFJPb2u1gTuqO37g==} + '@shikijs/themes@4.4.3': + resolution: {integrity: sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==} engines: {node: '>=20'} - '@shikijs/types@4.4.2': - resolution: {integrity: sha512-PFYitV4vpDr/iPCIhnHp+Q4ftic5N5VeNJ3KQ1O8gn3h2ar8qgwMAXF7tq4m1CWaMS60fV4VqF6vfnWH4F7vqQ==} + '@shikijs/types@4.4.3': + resolution: {integrity: sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==} engines: {node: '>=20'} '@shikijs/vscode-textmate@10.0.2': @@ -1864,18 +2557,25 @@ packages: engines: {node: '>= 10'} hasBin: true - '@tauri-apps/plugin-barcode-scanner@2.4.5': - resolution: {integrity: sha512-sIPRYEfxww8/y8skZ2LcAp/h5bwvlHkQiq+3w6QEl+2BHs13xnpn7hP+pv4fkBs8DyDfpUbOBIYS5YBwP7x1QQ==} - '@tauri-apps/plugin-deep-link@2.4.9': resolution: {integrity: sha512-u0SKOUHnJ1wqeqXsDFq2+kASCBj9xxbG0g9XZWPy9SOmU4wXtp6b/wiYpm6oH6/5fBTQsLqnLhIvqLBRpgHJlA==} '@tauri-apps/plugin-notification@2.3.3': resolution: {integrity: sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==} + '@trickfilm400/rollup-plugin-off-main-thread@3.0.0-pre1': + resolution: {integrity: sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q==} + engines: {node: '>=12'} + '@ts-morph/common@0.27.0': resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} + '@twemoji/api@17.0.3': + resolution: {integrity: sha512-iwERjxY0QgPGVwT6b1OKG0Oa9nIfHhJw+Ij1TapTBMKTvVCU6qdXPXX/XKwxKx5QZIJW5GwELUCtw8wlaIQ2ug==} + + '@twemoji/parser@17.0.2': + resolution: {integrity: sha512-X/P7pHsGOxnrupQYUVetIeuxBGgffFu8CLwoPMMjH9CWmQvlXiCpbTW/BXxMOCWXQojgHdmgdvm6IsCqAQ5nxA==} + '@types/cacheable-request@6.0.3': resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} @@ -1968,9 +2668,15 @@ packages: '@types/react@19.2.18': resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -1991,6 +2697,14 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/eslint-plugin@8.67.0': + resolution: {integrity: sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.67.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/parser@8.66.0': resolution: {integrity: sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1998,22 +2712,45 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/parser@8.67.0': + resolution: {integrity: sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/project-service@8.66.0': resolution: {integrity: sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/project-service@8.67.0': + resolution: {integrity: sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/scope-manager@8.66.0': resolution: {integrity: sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.67.0': + resolution: {integrity: sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.66.0': resolution: {integrity: sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/tsconfig-utils@8.67.0': + resolution: {integrity: sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/type-utils@8.66.0': resolution: {integrity: sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2021,16 +2758,33 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/type-utils@8.67.0': + resolution: {integrity: sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/types@8.66.0': resolution: {integrity: sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.67.0': + resolution: {integrity: sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.66.0': resolution: {integrity: sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/typescript-estree@8.67.0': + resolution: {integrity: sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@8.66.0': resolution: {integrity: sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2038,10 +2792,21 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@8.67.0': + resolution: {integrity: sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/visitor-keys@8.66.0': resolution: {integrity: sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.67.0': + resolution: {integrity: sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/typescript-aix-ppc64@7.0.2': resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} engines: {node: '>=16.20.0'} @@ -2178,11 +2943,11 @@ packages: babel-plugin-react-compiler: optional: true - '@vitest/expect@4.1.10': - resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + '@vitest/expect@4.1.11': + resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} - '@vitest/mocker@4.1.10': - resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + '@vitest/mocker@4.1.11': + resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -2192,24 +2957,25 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.10': - resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + '@vitest/pretty-format@4.1.11': + resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} - '@vitest/runner@4.1.10': - resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + '@vitest/runner@4.1.11': + resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} - '@vitest/snapshot@4.1.10': - resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + '@vitest/snapshot@4.1.11': + resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} - '@vitest/spy@4.1.10': - resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + '@vitest/spy@4.1.11': + resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} - '@vitest/utils@4.1.10': - resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + '@vitest/utils@4.1.11': + resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} '@xmldom/xmldom@0.8.13': resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} engines: {node: '>=10.0.0'} + deprecated: this version has critical issues, please update to the latest version abbrev@4.0.0: resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} @@ -2263,8 +3029,8 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + ansi-regex@6.3.0: + resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} engines: {node: '>=12'} ansi-styles@4.3.0: @@ -2285,6 +3051,14 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + asn1js@3.0.10: resolution: {integrity: sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==} engines: {node: '>=12.0.0'} @@ -2301,6 +3075,10 @@ packages: resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} engines: {node: '>=0.12.0'} + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -2315,9 +3093,28 @@ packages: resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==} engines: {node: '>=10.12.0'} + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + aws4@1.13.2: resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + babel-plugin-polyfill-corejs2@0.4.17: + resolution: {integrity: sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.14.2: + resolution: {integrity: sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.8: + resolution: {integrity: sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -2331,8 +3128,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.11.12: - resolution: {integrity: sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==} + baseline-browser-mapping@2.11.15: + resolution: {integrity: sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA==} engines: {node: '>=6.0.0'} hasBin: true @@ -2361,8 +3158,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.7: - resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} + browserslist@4.28.8: + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2401,6 +3198,10 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} @@ -2507,6 +3308,9 @@ packages: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + commander@5.1.0: resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} engines: {node: '>= 6'} @@ -2519,6 +3323,10 @@ packages: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} + common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + compare-version@0.1.2: resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} engines: {node: '>=0.10.0'} @@ -2538,8 +3346,8 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - content-type@2.0.0: - resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + content-type@2.1.0: + resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} engines: {node: '>=18'} convert-source-map@2.0.0: @@ -2556,6 +3364,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + core-js-compat@3.50.0: + resolution: {integrity: sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==} + engines: {node: '>=6.4.0'} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -2582,6 +3394,10 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypto-random-string@2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -2634,6 +3450,18 @@ packages: resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} engines: {node: '>=12'} + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + debounce-fn@4.0.0: resolution: {integrity: sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==} engines: {node: '>=10'} @@ -2682,8 +3510,8 @@ packages: resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} engines: {node: '>=18'} - default-browser@5.5.0: - resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + default-browser@5.5.1: + resolution: {integrity: sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==} engines: {node: '>=18'} defer-to-connect@2.0.1: @@ -2783,8 +3611,8 @@ packages: electron-publish@26.15.3: resolution: {integrity: sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==} - electron-to-chromium@1.5.402: - resolution: {integrity: sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA==} + electron-to-chromium@1.5.411: + resolution: {integrity: sha512-gglkxzokjHfawpGxq75XdBV2/l3BAPzrsMs70qgaZdTW5rpV1tC4MdgJVP9fN126bODA4ZJQkn1wryEzJyQXIg==} electron-winstaller@5.4.0: resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} @@ -2856,6 +3684,14 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + es-abstract-get@1.0.0: + resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} + engines: {node: '>= 0.4'} + + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -2864,8 +3700,8 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@2.3.1: - resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} @@ -2875,6 +3711,10 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + es-to-primitive@1.3.4: + resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} + engines: {node: '>= 0.4'} + es-toolkit@1.50.0: resolution: {integrity: sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==} @@ -2953,6 +3793,9 @@ packages: estree-util-is-identifier-name@3.0.0: resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -2960,6 +3803,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eta@4.6.0: + resolution: {integrity: sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA==} + engines: {node: '>=20'} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -2971,8 +3818,8 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - eventsource-parser@3.1.0: - resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} + eventsource-parser@3.1.1: + resolution: {integrity: sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==} engines: {node: '>=18.0.0'} eventsource@3.0.7: @@ -3007,13 +3854,13 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - fallow-type-aware@3.14.0: - resolution: {integrity: sha512-D8+bNELjmoVV2dKpmHplQ1YzuheHltmZKaOVJ//hx7h8vFNyp5H3hcJV8rR26ofBU51z9K5eyA7FpgP3hebjwg==} + fallow-type-aware@3.17.0: + resolution: {integrity: sha512-DFtZUK5oqP56tSsrBWPzjrTIwPPxwlBV8QoSaGghMru347Nqj8ZBY6u9HFa/TIPcxvFU/zIZMRF8xS/XAWQTzg==} engines: {node: '>=20'} hasBin: true - fallow@3.14.0: - resolution: {integrity: sha512-hse+sChmWkrffLiZDPGdNSyiLybVEhgoiYCIYgqLwdYhfTCO+f2PS1g+gMhGAJdLlYpZ75tFwRoMBFmZjN3Q6g==} + fallow@3.17.0: + resolution: {integrity: sha512-tbLuvsPq3I2CzpoF/9MgSQ7hWzOqlj3avFUAmCr1J9OERE6z714SlDmA38NVvNy/zuK/70utXB3/b33xC9z29g==} engines: {node: '>=22'} hasBin: true @@ -3079,6 +3926,14 @@ packages: flatted@3.4.4: resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + form-data@4.0.6: resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} @@ -3137,9 +3992,20 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + function.prototype.name@1.2.0: + resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + fuzzysort@3.1.0: resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==} + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -3164,6 +4030,9 @@ packages: resolution: {integrity: sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==} engines: {node: '>=14.16'} + get-own-enumerable-property-symbols@3.0.2: + resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -3180,6 +4049,10 @@ packages: resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} engines: {node: '>=18'} + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} @@ -3191,6 +4064,12 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} + engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -3199,6 +4078,10 @@ packages: resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} engines: {node: '>=10.0'} + globals@17.11.0: + resolution: {integrity: sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==} + engines: {node: '>=18'} + globals@17.9.0: resolution: {integrity: sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==} engines: {node: '>=18'} @@ -3218,6 +4101,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -3225,6 +4112,10 @@ packages: has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -3282,8 +4173,8 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} - hono@4.13.1: - resolution: {integrity: sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==} + hono@4.13.3: + resolution: {integrity: sha512-r8AO2mYHoLxSHkgafNeC/BXyb2vWRxD3jem4Ts+ptav8oTG5FIRifAjuJEmZI4bSvvc2ns0GxmIYiZnHqN3mMw==} engines: {node: '>=16.9.0'} hosted-git-info@4.1.0: @@ -3327,6 +4218,9 @@ packages: resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} engines: {node: '>=0.10.0'} + idb@7.1.1: + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -3356,18 +4250,22 @@ packages: inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} - input-otp@1.4.2: - resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==} + input-otp@1.5.0: + resolution: {integrity: sha512-3AcfdW1sNG0FmSA5hHMBXG7jNW5CdcdKs8ln8JOmn6S2SRkoXoJx+2UwC+ZvjflfnOdHJx0SfQ9/YjlxVYLtGQ==} peerDependencies: react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + internmap@2.0.3: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} - ip-address@10.4.0: - resolution: {integrity: sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==} + ip-address@10.5.0: + resolution: {integrity: sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==} engines: {node: '>= 12'} ipaddr.js@1.9.1: @@ -3380,9 +4278,41 @@ packages: is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} @@ -3396,14 +4326,26 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true + is-document.all@1.0.0: + resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -3424,10 +4366,29 @@ packages: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} engines: {node: '>=12'} + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-obj@1.0.1: + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} + engines: {node: '>=0.10.0'} + is-obj@2.0.0: resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} engines: {node: '>=8'} @@ -3443,10 +4404,26 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-regexp@1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} + engines: {node: '>=0.10.0'} + is-regexp@3.1.0: resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} engines: {node: '>=12'} + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -3455,6 +4432,18 @@ packages: resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} engines: {node: '>=18'} + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + is-unicode-supported@1.3.0: resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} engines: {node: '>=12'} @@ -3463,6 +4452,18 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -3474,6 +4475,9 @@ packages: isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isbinaryfile@4.0.10: resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} engines: {node: '>= 8.0.0'} @@ -3497,6 +4501,10 @@ packages: resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} engines: {node: '>=20'} + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + jake@10.9.4: resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} engines: {node: '>=10'} @@ -3509,6 +4517,9 @@ packages: jose@6.2.8: resolution: {integrity: sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==} + jose@6.2.9: + resolution: {integrity: sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3553,15 +4564,22 @@ packages: jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsonfile@5.0.0: + resolution: {integrity: sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==} + jsonfile@6.2.1: resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + katex@0.16.47: resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} hasBin: true - katex@0.18.2: - resolution: {integrity: sha512-3snve4y0SXTMequLim1FMiPvLGElySam0blN5xQZBqEY3lOJz1TnuaaiugnBZBqHzlSAGmFTYL7MCQkORuLKCA==} + katex@0.18.4: + resolution: {integrity: sha512-IMPntbRLOU+eu88XDiFKqQ8Akhr9Tv7jDMXqPhjG9SI1JMA4DIgXk4x9k4skJz2NZJXBRbC+2pYBLj9olqcZow==} hasBin: true keyv@4.5.4: @@ -3578,6 +4596,10 @@ packages: lazy-val@1.0.5: resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -3771,6 +4793,10 @@ packages: resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} engines: {node: '>=8'} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -3783,6 +4809,11 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lucide-react@1.32.0: + resolution: {integrity: sha512-txX56hMFnRxPi1f9/nH69YN8uvAO6a7Y1KSWKjCDAtdD9+soEgmWuCt6iRm1pkxUZo2+YntSdsE1L6bIuKoY8Q==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -4045,9 +5076,9 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - mtp@https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz: - resolution: {integrity: sha512-WcXHd53aDM0zQfO8eaUnK5tpnRP73GlWjwAMMcstcVAIWrvyFGuINkCRNEkb2RsNk9h57UMhxElCZeBkFrVmCg==, tarball: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz} - version: 0.2.0 + mtp@https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz: + resolution: {integrity: sha512-MzqeWSaS2lVoiK0coNfY8EPgGNk7QYFS3eRqVYeYBCT0NNm2lQ7T8lLjBuIuMZ6Zh2KAVWhwI2xQIkMdZ7QThA==, tarball: https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz} + version: 0.3.0 nanoid@3.3.18: resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} @@ -4119,6 +5150,10 @@ packages: resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} engines: {node: '>= 10'} + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + obug@2.1.4: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} @@ -4144,8 +5179,8 @@ packages: oniguruma-to-es@4.3.6: resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} - open@11.0.0: - resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + open@11.0.1: + resolution: {integrity: sha512-NzwMUB6C1D0+Kd+9iMS/H4k+Ck3cTX6Ckyfr/gAGlmvSE1LUQZnEZvWBi4PYmMwH/S5SMeTXnE+9uAz8uF+pWw==} engines: {node: '>=20'} open@8.4.2: @@ -4160,6 +5195,10 @@ packages: resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} engines: {node: '>=18'} + own-keys@1.0.2: + resolution: {integrity: sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==} + engines: {node: '>= 0.4'} + p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} @@ -4184,6 +5223,9 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -4229,6 +5271,13 @@ packages: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + path-to-regexp@8.4.2: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} @@ -4266,6 +5315,10 @@ packages: resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} engines: {node: '>=10.4.0'} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss-selector-parser@7.1.5: resolution: {integrity: sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==} engines: {node: '>=4'} @@ -4283,6 +5336,10 @@ packages: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} + powershell-utils@0.2.0: + resolution: {integrity: sha512-ZlsFlG7MtSFCoc5xreOvBAozCJ6Pf06opgJjh9ONEv418xpZSAzNjstD36C6+JwOnfSqOW/9uDkqKjezTdxZhw==} + engines: {node: '>=20'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -4292,6 +5349,14 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + + pretty-bytes@6.1.1: + resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} + engines: {node: ^14.13.1 || >=16.0.0} + pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} @@ -4404,8 +5469,8 @@ packages: '@types/react': optional: true - react-resizable-panels@4.12.2: - resolution: {integrity: sha512-NwY5LCo4WrxVvDh0xoMML6EMLPONP/8ckKcIdpnojxexoatZdjLiRqLJQjQK5CPkd4SYiB/2M5BVrjZBQtOO7Q==} + react-resizable-panels@4.12.3: + resolution: {integrity: sha512-GHMJWnDXui/3RX4bT+cgBP+N3N2nkwcoZATr/2xLFpqQQe7TlBrE0cGM0dUa9ceT7A90tUrSRsiqgRG+g0/2AA==} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 @@ -4431,8 +5496,8 @@ packages: readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - recast@0.23.19: - resolution: {integrity: sha512-T98lym7kH+pnZmRaD8yDRdaNqyUbwnbEBx0MuchrzMFOEMray4AO3ZJoTUZ5r78Ao78X/OhzW0DL8GB85w/I2w==} + recast@0.23.21: + resolution: {integrity: sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==} engines: {node: '>= 4'} recharts@3.10.1: @@ -4451,6 +5516,17 @@ packages: redux@5.0.1: resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regenerate-unicode-properties@10.2.2: + resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -4460,6 +5536,21 @@ packages: regex@6.1.0: resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + regexpu-core@6.4.0: + resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} + engines: {node: '>=4'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.13.2: + resolution: {integrity: sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==} + hasBin: true + rehype-autolink-headings@7.1.0: resolution: {integrity: sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==} @@ -4509,6 +5600,11 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + responselike@2.0.1: resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} @@ -4538,6 +5634,11 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rollup@4.62.4: + resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -4552,9 +5653,21 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} + engines: {node: '>=0.4'} + safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -4604,6 +5717,10 @@ packages: resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} engines: {node: '>=10'} + serialize-javascript@7.1.0: + resolution: {integrity: sha512-RNEqWOyhhUQYN9V1GfHwu9AR/g+NTciH6Z5u3/no6X3/w+04J2lVDL+svFQVXgXrEGBMG2puMVN3gq2SNGuTGw==} + engines: {node: '>=20.0.0'} + seroval-plugins@1.6.2: resolution: {integrity: sha512-TfxuUjlbBESzUOWdTkTKqvSmav0ABym+itetDXLK6mDz8SmrpdI30aF8RTXE8Bvq+tH/1yIDkvy3W0lfQb1ipQ==} engines: {node: '>=10'} @@ -4618,11 +5735,23 @@ packages: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - shadcn@4.16.2: - resolution: {integrity: sha512-M1AvZKFWcCzWRDoyApIqJMSLIpY8Ev4uBGuiPLSFmiTbixXhPmzotSTvLzFmBrfoIxG9aIg2dZOETblEaXGUnQ==} + shadcn@4.18.0: + resolution: {integrity: sha512-tUFZgkYmfVNQVm3xX7lhSzOvDsp+O14ac5dwgXIr5mIsr79ISueb/Mu+ZtWMz0DH6v77u4eYyvbQ9TTMpSn3aw==} engines: {node: '>=20.18.1'} hasBin: true @@ -4634,8 +5763,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shiki@4.4.2: - resolution: {integrity: sha512-P8F/dFhRevaw2uSdeIYlq/5SXZNY85DPtmXQ947gD1Zj2JqO5AkNvVVBar0Me9JkFx3uzVud/qOtP5ek9NEQGA==} + shiki@4.4.3: + resolution: {integrity: sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==} engines: {node: '>=20'} side-channel-list@1.0.1: @@ -4671,12 +5800,34 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + smob@1.6.2: + resolution: {integrity: sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==} + engines: {node: '>=20.0.0'} + + socks@2.8.9: + resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + sonner@2.0.7: resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} peerDependencies: react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + sonner@2.0.8: + resolution: {integrity: sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==} + peerDependencies: + '@types/react': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -4688,6 +5839,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + source-map@0.8.0: + resolution: {integrity: sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==} + engines: {node: '>= 12'} + space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -4712,6 +5867,10 @@ packages: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -4720,12 +5879,32 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.trim@1.2.11: + resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.10: + resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + stringify-object@3.3.0: + resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} + engines: {node: '>=4'} + stringify-object@5.0.0: resolution: {integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==} engines: {node: '>=14.16'} @@ -4742,6 +5921,10 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-comments@2.0.1: + resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} + engines: {node: '>=10'} + strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} @@ -4767,6 +5950,10 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + systeminformation@5.33.1: resolution: {integrity: sha512-DEN6ICHk3Tk0Uf/hrAHh7xlt7iL5CJFBtPZinA0H62DrGG/KPKqq/Nzj6lCXPS4Ay/sf/14zNnk9LpqKzBIc+w==} engines: {node: '>=10.0.0'} @@ -4792,6 +5979,10 @@ packages: resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} engines: {node: '>=18'} + temp-dir@2.0.0: + resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} + engines: {node: '>=8'} + temp-file@3.4.0: resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} @@ -4799,6 +5990,15 @@ packages: resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} engines: {node: '>=6.0.0'} + tempy@0.6.0: + resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==} + engines: {node: '>=10'} + + terser@5.50.0: + resolution: {integrity: sha512-CN9BVxWhgS/hRxtUMjtC2uRWSTcSfQFHMDWma6sKKfIivCD91sM+FOPfvwoaRMqCSrUpe1nv3jDamd9eEQ4y+w==} + engines: {node: '>=10'} + hasBin: true + tiny-async-pool@1.3.0: resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==} @@ -4871,10 +6071,30 @@ packages: resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} engines: {node: '>=10'} + type-fest@0.16.0: + resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} + engines: {node: '>=10'} + type-is@2.1.0: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} + engines: {node: '>= 0.4'} + typed-emitter@2.1.0: resolution: {integrity: sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==} @@ -4885,6 +6105,13 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + typescript-eslint@8.67.0: + resolution: {integrity: sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} @@ -4895,6 +6122,10 @@ packages: engines: {node: '>=16.20.0'} hasBin: true + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} @@ -4909,6 +6140,22 @@ packages: resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} engines: {node: '>=20.18.1'} + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.1: + resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.2.0: + resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} + engines: {node: '>=4'} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -4916,6 +6163,10 @@ packages: unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + unique-string@2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} + unist-util-find-after@5.0.0: resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} @@ -4952,8 +6203,12 @@ packages: unzipper@0.12.5: resolution: {integrity: sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==} - update-browserslist-db@1.3.0: - resolution: {integrity: sha512-x/M6q3w4Ybp91CNaS4S69UnliqR3BzRpOT6LWbksjth0S/+jhfaPJsWjt/TewpT8j9eLIojUf5jr29WextHroA==} + upath@1.2.0: + resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} + engines: {node: '>=4'} + + update-browserslist-db@1.3.1: + resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -5024,6 +6279,18 @@ packages: victory-vendor@37.3.6: resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + vite-plugin-pwa@1.3.0: + resolution: {integrity: sha512-c5kMgN+ITrOtHXp8PAtk2uOIEea6XjP/unCGxOWWBzQ6qa65qj/awHg0wf+QF9E/2u9vh86LqxPwzEPNbM2r5A==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@vite-pwa/assets-generator': ^1.0.0 + vite: ^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + workbox-build: ^7.4.1 + workbox-window: ^7.4.1 + peerDependenciesMeta: + '@vite-pwa/assets-generator': + optional: true + vite@8.2.1: resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5067,20 +6334,20 @@ packages: yaml: optional: true - vitest@4.1.10: - resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + vitest@4.1.11: + resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.10 - '@vitest/browser-preview': 4.1.10 - '@vitest/browser-webdriverio': 4.1.10 - '@vitest/coverage-istanbul': 4.1.10 - '@vitest/coverage-v8': 4.1.10 - '@vitest/ui': 4.1.10 + '@vitest/browser-playwright': 4.1.11 + '@vitest/browser-preview': 4.1.11 + '@vitest/browser-webdriverio': 4.1.11 + '@vitest/coverage-istanbul': 4.1.11 + '@vitest/coverage-v8': 4.1.11 + '@vitest/ui': 4.1.11 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -5121,6 +6388,22 @@ packages: resolution: {integrity: sha512-CHbl2ZQbxx164IgWRgzJno4hWtM4tFbRam1QfI3Yxhs3w/DvqluVxVWeXs3oL5/fbGkSNLKo0Ty5MgUWceNhog==} engines: {node: '>=6.0.0', npm: '>=3.10.0'} + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -5150,6 +6433,55 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + workbox-background-sync@7.4.1: + resolution: {integrity: sha512-HhT7KE8tOWDm02wRNshXUnUPofMlhenF2DBdUnDPOubhizzPeItkYTmAB6td1Z2cjYPa98vzEiPLEuzn5hN66g==} + + workbox-broadcast-update@7.4.1: + resolution: {integrity: sha512-uAlgslKLvbQY+suirIdnBCSYrcgBhjp81Nj4l1lj/Jmj0MJO2CJERnCJjT0GFVwmReV0N+zs78K6gqd5gr9/+A==} + + workbox-build@7.4.1: + resolution: {integrity: sha512-SDhxIvEAde9Gy/5w4Yo1Jh/M49Z0qE3q0oteyE8zGq0DScxFqVBcCtIXFuLtmtxRQZCMbf0prco4VyEu3KBQuw==} + engines: {node: '>=20.0.0'} + + workbox-cacheable-response@7.4.1: + resolution: {integrity: sha512-8xaFoJdDc2OjrlbbL3gEeBO1WKcMwRqwLRupgqahYXu75yXajPLuwrbXMrIGZuWYXrQwk0xDjOxZ/ujCy/oJYw==} + + workbox-core@7.4.1: + resolution: {integrity: sha512-DT+vu46eh/2vRsSHTY4Xmc32Z1rr9PRlQUXr1Dx30ZuXRWwOsvZgGgcwxcasubQLQmbTNYZjv44LkBAQ4tT5tQ==} + + workbox-expiration@7.4.1: + resolution: {integrity: sha512-lRKUF7b+OGbeXkQk1s6MHXOa3d7Xxf7Of31W6c6hCfipfIyrtdWZ89stq21AHZMaoG7VNFoHply4Ox+rU31TWg==} + + workbox-google-analytics@7.4.1: + resolution: {integrity: sha512-Mks1JwLEt++ZAkF6sS1OpSh9RtAMIsiDgRpK+codiHGIPXeaUOgi4cPc3GFadUl8V5QPeypEk8Oxgl3HlwVzHw==} + + workbox-navigation-preload@7.4.1: + resolution: {integrity: sha512-C4KVsjPcYKJOhr631AxR9XoG2rLF3QiTk5aMv36MXOjtWvm8axwNFAtKUPGsWUwLXXAMgYM1En7fsvndaXeXRQ==} + + workbox-precaching@7.4.1: + resolution: {integrity: sha512-cdr/9qByww7yzEp7zg/qI4ukUrrNjQLgN+ONQRpjy/VqGQXwkgHwr00KksGJK8v0VifwDXBb8a4cWNZH71jn3Q==} + + workbox-range-requests@7.4.1: + resolution: {integrity: sha512-7i2oxAUE82gHdAJBCAQ04JzNOdRPqzuOzGfoUyJpFSmeqBNYGPrAH8GPoPjUQTfp+NycwrD2H68VtuF8qxv0vQ==} + + workbox-recipes@7.4.1: + resolution: {integrity: sha512-gnbVfmV4/TtmQaM4x9AtuXhcdstJsep3XMVeztOrQVPT+R6+6DeBjGTCQ7fFCXm+4GEHUA5VEBTyi5+4gWGeog==} + + workbox-routing@7.4.1: + resolution: {integrity: sha512-yubJGErZOusuidAenaL5ypfhQOa7urxP/f8E0ws7FPb4039RiWXUWBAyUkmUoOL/BcQGen3h0J8872d51IYxtA==} + + workbox-strategies@7.4.1: + resolution: {integrity: sha512-GZxpaw9NbmOelj7667uZ2kpk5BFpOGbO4X0qjwh5ls8XQ8C+Lha5LQchTiUzsTFSS+NlUpftYAyOVXvQUrcqOQ==} + + workbox-streams@7.4.1: + resolution: {integrity: sha512-HWWtraKUbJknd9kgqGcpQ3G114HOPYvqs8HaJMDs2ebLNAimDkVDaWfAXE6Ybl+m8U6KsCE6pWyLYuigWmnAXw==} + + workbox-sw@7.4.1: + resolution: {integrity: sha512-fez5f2DUlDJWTFYkCWQpY10N8gtztd849NswCbVFk0QlcSM4HT5A8x4g4ii650yem4I8tHY0R7JZahwp3ltIPw==} + + workbox-window@7.4.1: + resolution: {integrity: sha512-notZDH2u8VXaqyuD7xaqIfEFi6SRM4SUSd7ewe9PDsVqADuepxX2ZMY3uvuZGxzY5ZOsGC/vD3A/3smFtJt4/A==} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -5157,8 +6489,8 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - wsl-utils@0.3.1: - resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + wsl-utils@1.0.0: + resolution: {integrity: sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA==} engines: {node: '>=20'} xmlbuilder@15.1.1: @@ -5244,6 +6576,12 @@ packages: snapshots: + '@apideck/better-ajv-errors@0.3.7(ajv@8.20.0)': + dependencies: + ajv: 8.20.0 + jsonpointer: 5.0.1 + leven: 3.1.0 + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -5288,7 +6626,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.7 + browserslist: 4.28.8 lru-cache: 5.1.1 semver: 6.3.1 @@ -5305,6 +6643,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-create-regexp-features-plugin@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-annotate-as-pure': 7.29.7 + regexpu-core: 6.4.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + debug: 4.4.3(supports-color@7.2.0) + lodash.debounce: 4.0.8 + resolve: 1.22.12 + transitivePeerDependencies: + - supports-color + '@babel/helper-globals@7.29.7': {} '@babel/helper-member-expression-to-functions@7.29.7(supports-color@7.2.0)': @@ -5336,6 +6692,15 @@ snapshots: '@babel/helper-plugin-utils@7.29.7': {} + '@babel/helper-remap-async-to-generator@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-wrap-function': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.8(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) @@ -5358,6 +6723,14 @@ snapshots: '@babel/helper-validator-option@7.29.7': {} + '@babel/helper-wrap-function@7.29.7(supports-color@7.2.0)': + dependencies: + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@7.2.0) + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + '@babel/helpers@7.29.7': dependencies: '@babel/template': 7.29.7 @@ -5367,6 +6740,63 @@ snapshots: dependencies: '@babel/types': 7.29.8 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@7.2.0) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + + '@babel/plugin-syntax-import-assertions@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) @@ -5377,6 +6807,172 @@ snapshots: '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-arrow-functions@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-async-generator-functions@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/traverse': 7.29.8(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-module-imports': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-block-scoping@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-class-properties@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/traverse': 7.29.8(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/template': 7.29.7 + + '@babel/plugin-transform-destructuring@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-dotall-regex@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-duplicate-keys@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-dynamic-import@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-explicit-resource-management@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-exponentiation-operator@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-export-namespace-from@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-for-of@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-literals@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-logical-assignment-operators@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-member-expression-literals@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-modules-amd@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) @@ -5385,6 +6981,148 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-modules-systemjs@7.29.8(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-new-target@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-nullish-coalescing-operator@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-numeric-separator@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-object-rest-spread@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/traverse': 7.29.8(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-object-super@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-optional-chaining@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-private-methods@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-regenerator@7.29.8(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-regexp-modifiers@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-reserved-words@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-shorthand-properties@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-spread@7.29.8(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-template-literals@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-typeof-symbol@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) @@ -5396,6 +7134,113 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-unicode-escapes@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-unicode-property-regex@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-unicode-regex@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-unicode-sets-regex@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-syntax-import-assertions': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-async-generator-functions': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-async-to-generator': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-block-scoped-functions': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-block-scoping': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-class-static-block': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-computed-properties': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-dotall-regex': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-duplicate-keys': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-dynamic-import': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-explicit-resource-management': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-exponentiation-operator': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-export-namespace-from': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-for-of': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-function-name': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-json-strings': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-literals': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-logical-assignment-operators': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-member-expression-literals': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-modules-amd': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-modules-systemjs': 7.29.8(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-modules-umd': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-new-target': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-numeric-separator': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-object-rest-spread': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-object-super': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-optional-catch-binding': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-private-property-in-object': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-property-literals': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-regenerator': 7.29.8(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-regexp-modifiers': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-reserved-words': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-shorthand-properties': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-spread': 7.29.8(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-sticky-regex': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-template-literals': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-typeof-symbol': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-unicode-escapes': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-unicode-property-regex': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-unicode-sets-regex': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.7(supports-color@7.2.0)) + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + core-js-compat: 3.50.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.7(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/types': 7.29.8 + esutils: 2.0.3 + '@babel/preset-typescript@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) @@ -5461,14 +7306,14 @@ snapshots: dependencies: '@codemirror/language': 6.12.4 '@codemirror/state': 6.7.1 - '@codemirror/view': 6.43.8 + '@codemirror/view': 6.43.9 '@lezer/common': 1.5.2 - '@codemirror/commands@6.10.4': + '@codemirror/commands@6.11.0': dependencies: '@codemirror/language': 6.12.4 '@codemirror/state': 6.7.1 - '@codemirror/view': 6.43.8 + '@codemirror/view': 6.43.9 '@lezer/common': 1.5.2 '@codemirror/lang-css@6.3.1': @@ -5486,7 +7331,7 @@ snapshots: '@codemirror/lang-javascript': 6.2.5 '@codemirror/language': 6.12.4 '@codemirror/state': 6.7.1 - '@codemirror/view': 6.43.8 + '@codemirror/view': 6.43.9 '@lezer/common': 1.5.2 '@lezer/css': 1.3.6 '@lezer/html': 1.3.13 @@ -5497,7 +7342,7 @@ snapshots: '@codemirror/language': 6.12.4 '@codemirror/lint': 6.9.7 '@codemirror/state': 6.7.1 - '@codemirror/view': 6.43.8 + '@codemirror/view': 6.43.9 '@lezer/common': 1.5.2 '@lezer/javascript': 1.5.4 @@ -5507,14 +7352,14 @@ snapshots: '@codemirror/lang-html': 6.4.12 '@codemirror/language': 6.12.4 '@codemirror/state': 6.7.1 - '@codemirror/view': 6.43.8 + '@codemirror/view': 6.43.9 '@lezer/common': 1.5.2 '@lezer/markdown': 1.7.2 '@codemirror/language@6.12.4': dependencies: '@codemirror/state': 6.7.1 - '@codemirror/view': 6.43.8 + '@codemirror/view': 6.43.9 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.10 @@ -5523,14 +7368,14 @@ snapshots: '@codemirror/lint@6.9.7': dependencies: '@codemirror/state': 6.7.1 - '@codemirror/view': 6.43.8 + '@codemirror/view': 6.43.9 crelt: 1.0.7 '@codemirror/state@6.7.1': dependencies: '@marijn/find-cluster-break': 1.0.3 - '@codemirror/view@6.43.8': + '@codemirror/view@6.43.9': dependencies: '@codemirror/state': 6.7.1 crelt: 1.0.7 @@ -5764,28 +7609,28 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 - '@fallow-cli/darwin-arm64@3.14.0': + '@fallow-cli/darwin-arm64@3.17.0': optional: true - '@fallow-cli/darwin-x64@3.14.0': + '@fallow-cli/darwin-x64@3.17.0': optional: true - '@fallow-cli/linux-arm64-gnu@3.14.0': + '@fallow-cli/linux-arm64-gnu@3.17.0': optional: true - '@fallow-cli/linux-arm64-musl@3.14.0': + '@fallow-cli/linux-arm64-musl@3.17.0': optional: true - '@fallow-cli/linux-x64-gnu@3.14.0': + '@fallow-cli/linux-x64-gnu@3.17.0': optional: true - '@fallow-cli/linux-x64-musl@3.14.0': + '@fallow-cli/linux-x64-musl@3.17.0': optional: true - '@fallow-cli/win32-arm64-msvc@3.14.0': + '@fallow-cli/win32-arm64-msvc@3.17.0': optional: true - '@fallow-cli/win32-x64-msvc@3.14.0': + '@fallow-cli/win32-x64-msvc@3.17.0': optional: true '@floating-ui/core@1.8.0': @@ -5812,9 +7657,9 @@ snapshots: '@fontsource-variable/public-sans@5.3.0': {} - '@hono/node-server@2.1.0(hono@4.13.1)': + '@hono/node-server@2.1.1(hono@4.13.3)': dependencies: - hono: 4.13.1 + hono: 4.13.3 '@humanfs/core@0.19.2': dependencies: @@ -5832,6 +7677,8 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@isaacs/cliui@9.0.0': {} + '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.3 @@ -5848,6 +7695,11 @@ snapshots: '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.31': @@ -5929,16 +7781,16 @@ snapshots: '@marijn/find-cluster-break@1.0.3': {} - '@methanium/ui@https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3)': + '@methanium/ui@https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(emojibase@17.0.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@base-ui/react': 1.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@codemirror/autocomplete': 6.20.3 - '@codemirror/commands': 6.10.4 + '@codemirror/commands': 6.11.0 '@codemirror/lang-markdown': 6.5.2 '@codemirror/language': 6.12.4 '@codemirror/state': 6.7.1 - '@codemirror/view': 6.43.8 - '@shikijs/langs': 4.4.2 + '@codemirror/view': 6.43.9 + '@shikijs/langs': 4.4.3 '@tauri-apps/api': 2.11.1 class-variance-authority: 0.7.1 clsx: 2.1.1 @@ -5946,14 +7798,14 @@ snapshots: embla-carousel-react: 8.6.0(react@19.2.8) emojibase-data: 17.0.0(emojibase@17.0.0) hast-util-to-jsx-runtime: 2.3.6(supports-color@7.2.0) - input-otp: 1.4.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - katex: 0.18.2 - lucide-react: 1.30.0(react@19.2.8) + input-otp: 1.5.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + katex: 0.18.4 + lucide-react: 1.32.0(react@19.2.8) next-themes: 0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) react-markdown: 10.1.0(@types/react@19.2.18)(react@19.2.8)(supports-color@7.2.0) - react-resizable-panels: 4.12.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react-resizable-panels: 4.12.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) recharts: 3.10.1(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1) rehype-autolink-headings: 7.1.0 rehype-katex: 7.0.1 @@ -5961,9 +7813,9 @@ snapshots: remark-breaks: 4.0.0 remark-gfm: 4.0.1(supports-color@7.2.0) remark-math: 6.0.0(supports-color@7.2.0) - shadcn: 4.16.2(supports-color@7.2.0)(typescript@6.0.3) - shiki: 4.4.2 - sonner: 2.0.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + shadcn: 4.18.0(supports-color@7.2.0)(typescript@6.0.3) + shiki: 4.4.3 + sonner: 2.0.8(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) tailwind-merge: 3.6.0 tw-animate-css: 1.4.0 unist-util-visit: 5.1.0 @@ -5983,18 +7835,18 @@ snapshots: '@modelcontextprotocol/sdk@1.30.0(supports-color@7.2.0)(zod@3.25.76)': dependencies: - '@hono/node-server': 2.1.0(hono@4.13.1) + '@hono/node-server': 2.1.1(hono@4.13.3) ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) content-type: 1.0.5 cors: 2.8.6 cross-spawn: 7.0.6 eventsource: 3.0.7 - eventsource-parser: 3.1.0 + eventsource-parser: 3.1.1 express: 5.2.1(supports-color@7.2.0) express-rate-limit: 8.6.2(express@5.2.1(supports-color@7.2.0))(supports-color@7.2.0) - hono: 4.13.1 - jose: 6.2.8 + hono: 4.13.3 + jose: 6.2.9 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 raw-body: 3.0.2 @@ -6003,6 +7855,9 @@ snapshots: transitivePeerDependencies: - supports-color + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + '@noble/hashes@1.4.0': {} '@noble/hashes@2.3.0': {} @@ -6236,42 +8091,160 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@rollup/plugin-babel@6.1.0(@babel/core@7.29.7(supports-color@7.2.0))(rollup@4.62.4)(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-module-imports': 7.29.7(supports-color@7.2.0) + '@rollup/pluginutils': 5.4.0(rollup@4.62.4) + optionalDependencies: + rollup: 4.62.4 + transitivePeerDependencies: + - supports-color + + '@rollup/plugin-node-resolve@16.0.3(rollup@4.62.4)': + dependencies: + '@rollup/pluginutils': 5.4.0(rollup@4.62.4) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.12 + optionalDependencies: + rollup: 4.62.4 + + '@rollup/plugin-replace@6.0.3(rollup@4.62.4)': + dependencies: + '@rollup/pluginutils': 5.4.0(rollup@4.62.4) + magic-string: 0.30.21 + optionalDependencies: + rollup: 4.62.4 + + '@rollup/plugin-terser@1.0.0(rollup@4.62.4)': + dependencies: + serialize-javascript: 7.1.0 + smob: 1.6.2 + terser: 5.50.0 + optionalDependencies: + rollup: 4.62.4 + + '@rollup/pluginutils@5.4.0(rollup@4.62.4)': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.5 + optionalDependencies: + rollup: 4.62.4 + + '@rollup/rollup-android-arm-eabi@4.62.4': + optional: true + + '@rollup/rollup-android-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-x64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.4': + optional: true + '@sec-ant/readable-stream@0.4.1': {} - '@shikijs/core@4.4.2': + '@shikijs/core@4.4.3': dependencies: - '@shikijs/primitive': 4.4.2 - '@shikijs/types': 4.4.2 + '@shikijs/primitive': 4.4.3 + '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@4.4.2': + '@shikijs/engine-javascript@4.4.3': dependencies: - '@shikijs/types': 4.4.2 + '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.6 - '@shikijs/engine-oniguruma@4.4.2': + '@shikijs/engine-oniguruma@4.4.3': dependencies: - '@shikijs/types': 4.4.2 + '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@4.4.2': + '@shikijs/langs@4.4.3': dependencies: - '@shikijs/types': 4.4.2 + '@shikijs/types': 4.4.3 - '@shikijs/primitive@4.4.2': + '@shikijs/primitive@4.4.3': dependencies: - '@shikijs/types': 4.4.2 + '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 - '@shikijs/themes@4.4.2': + '@shikijs/themes@4.4.3': dependencies: - '@shikijs/types': 4.4.2 + '@shikijs/types': 4.4.3 - '@shikijs/types@4.4.2': + '@shikijs/types@4.4.3': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 @@ -6351,12 +8324,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/vite@4.3.3(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.3(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 tailwindcss: 4.3.3 - vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0) '@tanstack/devtools-event-client@0.4.4': {} @@ -6476,10 +8449,6 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.11.4 '@tauri-apps/cli-win32-x64-msvc': 2.11.4 - '@tauri-apps/plugin-barcode-scanner@2.4.5': - dependencies: - '@tauri-apps/api': 2.11.1 - '@tauri-apps/plugin-deep-link@2.4.9': dependencies: '@tauri-apps/api': 2.11.1 @@ -6488,12 +8457,28 @@ snapshots: dependencies: '@tauri-apps/api': 2.11.1 + '@trickfilm400/rollup-plugin-off-main-thread@3.0.0-pre1': + dependencies: + ejs: 3.1.10 + json5: 2.2.3 + magic-string: 0.30.21 + string.prototype.matchall: 4.0.12 + '@ts-morph/common@0.27.0': dependencies: fast-glob: 3.3.3 minimatch: 10.2.6 path-browserify: 1.0.1 + '@twemoji/api@17.0.3': + dependencies: + '@twemoji/parser': 17.0.2 + fs-extra: 8.1.0 + jsonfile: 5.0.0 + universalify: 0.1.2 + + '@twemoji/parser@17.0.2': {} + '@types/cacheable-request@6.0.3': dependencies: '@types/http-cache-semantics': 4.2.0 @@ -6590,10 +8575,14 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/resolve@1.20.2': {} + '@types/responselike@1.0.3': dependencies: '@types/node': 26.2.0 + '@types/trusted-types@2.0.7': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -6618,6 +8607,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/eslint-plugin@8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.67.0 + '@typescript-eslint/type-utils': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.67.0 + eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + ignore: 7.0.6 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@8.66.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.66.0 @@ -6630,6 +8635,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.67.0 + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.67.0 + debug: 4.4.3(supports-color@7.2.0) + eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/project-service@8.66.0(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3) @@ -6639,15 +8656,33 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.67.0(supports-color@7.2.0)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@6.0.3) + '@typescript-eslint/types': 8.67.0 + debug: 4.4.3(supports-color@7.2.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@8.66.0': dependencies: '@typescript-eslint/types': 8.66.0 '@typescript-eslint/visitor-keys': 8.66.0 + '@typescript-eslint/scope-manager@8.67.0': + dependencies: + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/visitor-keys': 8.67.0 + '@typescript-eslint/tsconfig-utils@8.66.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 + '@typescript-eslint/tsconfig-utils@8.67.0(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + '@typescript-eslint/type-utils@8.66.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.66.0 @@ -6660,8 +8695,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/type-utils@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + debug: 4.4.3(supports-color@7.2.0) + eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/types@8.66.0': {} + '@typescript-eslint/types@8.67.0': {} + '@typescript-eslint/typescript-estree@8.66.0(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@typescript-eslint/project-service': 8.66.0(supports-color@7.2.0)(typescript@6.0.3) @@ -6677,6 +8726,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.67.0(supports-color@7.2.0)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.67.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@6.0.3) + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/visitor-keys': 8.67.0 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@8.66.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)) @@ -6688,11 +8752,27 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)) + '@typescript-eslint/scope-manager': 8.67.0 + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@6.0.3) + eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@8.66.0': dependencies: '@typescript-eslint/types': 8.66.0 eslint-visitor-keys: 5.0.1 + '@typescript-eslint/visitor-keys@8.67.0': + dependencies: + '@typescript-eslint/types': 8.67.0 + eslint-visitor-keys: 5.0.1 + '@typescript/typescript-aix-ppc64@7.0.2': optional: true @@ -6755,49 +8835,49 @@ snapshots: '@ungap/structured-clone@1.3.3': {} - '@vitejs/plugin-react@6.0.5(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0))': + '@vitejs/plugin-react@6.0.5(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0) - '@vitest/expect@4.1.10': + '@vitest/expect@4.1.11': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 chai: 6.2.2 tinyrainbow: 3.1.1 - '@vitest/mocker@4.1.10(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0))': + '@vitest/mocker@4.1.11(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))': dependencies: - '@vitest/spy': 4.1.10 + '@vitest/spy': 4.1.11 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0) - '@vitest/pretty-format@4.1.10': + '@vitest/pretty-format@4.1.11': dependencies: tinyrainbow: 3.1.1 - '@vitest/runner@4.1.10': + '@vitest/runner@4.1.11': dependencies: - '@vitest/utils': 4.1.10 + '@vitest/utils': 4.1.11 pathe: 2.0.3 - '@vitest/snapshot@4.1.10': + '@vitest/snapshot@4.1.11': dependencies: - '@vitest/pretty-format': 4.1.10 - '@vitest/utils': 4.1.10 + '@vitest/pretty-format': 4.1.11 + '@vitest/utils': 4.1.11 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.10': {} + '@vitest/spy@4.1.11': {} - '@vitest/utils@4.1.10': + '@vitest/utils@4.1.11': dependencies: - '@vitest/pretty-format': 4.1.10 + '@vitest/pretty-format': 4.1.11 convert-source-map: 2.0.0 tinyrainbow: 3.1.1 @@ -6844,7 +8924,7 @@ snapshots: ansi-regex@5.0.1: {} - ansi-regex@6.2.2: {} + ansi-regex@6.3.0: {} ansi-styles@4.3.0: dependencies: @@ -6904,6 +8984,21 @@ snapshots: dependencies: tslib: 2.8.1 + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + asn1js@3.0.10: dependencies: pvtsutils: 1.3.6 @@ -6918,6 +9013,8 @@ snapshots: async-exit-hook@2.0.1: {} + async-function@1.0.0: {} + async@3.2.6: {} asynckit@0.4.0: {} @@ -6926,8 +9023,36 @@ snapshots: atomically@1.7.0: {} + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + aws4@1.13.2: {} + babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0): + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.14.2(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0): + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + core-js-compat: 3.50.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0): + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + bail@2.0.2: {} balanced-match@1.0.2: {} @@ -6936,14 +9061,14 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.11.12: {} + baseline-browser-mapping@2.11.15: {} bluebird@3.7.2: {} body-parser@2.3.0(supports-color@7.2.0): dependencies: bytes: 3.1.2 - content-type: 2.0.0 + content-type: 2.1.0 debug: 4.4.3(supports-color@7.2.0) http-errors: 2.0.1 iconv-lite: 0.7.3 @@ -6974,13 +9099,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.7: + browserslist@4.28.8: dependencies: - baseline-browser-mapping: 2.11.12 + baseline-browser-mapping: 2.11.15 caniuse-lite: 1.0.30001809 - electron-to-chromium: 1.5.402 + electron-to-chromium: 1.5.411 node-releases: 2.0.53 - update-browserslist-db: 1.3.0(browserslist@4.28.7) + update-browserslist-db: 1.3.1(browserslist@4.28.8) buffer-from@1.1.2: {} @@ -7035,6 +9160,13 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 @@ -7123,6 +9255,8 @@ snapshots: commander@14.0.3: {} + commander@2.20.3: {} + commander@5.1.0: {} commander@8.3.0: {} @@ -7130,6 +9264,8 @@ snapshots: commander@9.5.0: optional: true + common-tags@1.8.2: {} + compare-version@0.1.2: {} concat-map@0.0.1: {} @@ -7151,7 +9287,7 @@ snapshots: content-type@1.0.5: {} - content-type@2.0.0: {} + content-type@2.1.0: {} convert-source-map@2.0.0: {} @@ -7161,6 +9297,10 @@ snapshots: cookie@0.7.2: {} + core-js-compat@3.50.0: + dependencies: + browserslist: 4.28.8 + core-util-is@1.0.3: {} cors@2.8.6: @@ -7188,6 +9328,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-random-string@2.0.0: {} + cssesc@3.0.0: {} csstype@3.2.3: {} @@ -7230,6 +9372,24 @@ snapshots: d3-timer@3.0.1: {} + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + debounce-fn@4.0.0: dependencies: mimic-fn: 3.1.0 @@ -7262,7 +9422,7 @@ snapshots: default-browser-id@5.0.1: {} - default-browser@5.5.0: + default-browser@5.5.1: dependencies: bundle-name: 4.1.0 default-browser-id: 5.0.1 @@ -7274,7 +9434,6 @@ snapshots: es-define-property: 1.0.1 es-errors: 1.3.0 gopd: 1.2.0 - optional: true define-lazy-prop@2.0.0: {} @@ -7285,7 +9444,6 @@ snapshots: define-data-property: 1.1.4 has-property-descriptors: 1.0.2 object-keys: 1.1.1 - optional: true delayed-stream@1.0.0: {} @@ -7388,7 +9546,7 @@ snapshots: transitivePeerDependencies: - supports-color - electron-to-chromium@1.5.402: {} + electron-to-chromium@1.5.411: {} electron-winstaller@5.4.0(supports-color@7.2.0): dependencies: @@ -7460,11 +9618,75 @@ snapshots: dependencies: is-arrayish: 0.2.1 + es-abstract-get@1.0.0: + dependencies: + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + is-callable: 1.2.7 + object-inspect: 1.13.4 + + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.4 + function.prototype.name: 1.2.0 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.2 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.4 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.11 + string.prototype.trimend: 1.0.10 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.8 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.22 + es-define-property@1.0.1: {} es-errors@1.3.0: {} - es-module-lexer@2.3.1: {} + es-module-lexer@2.3.2: {} es-object-atoms@1.1.2: dependencies: @@ -7477,6 +9699,15 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.4 + es-to-primitive@1.3.4: + dependencies: + es-abstract-get: 1.0.0 + es-define-property: 1.0.1 + es-errors: 1.3.0 + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + es-toolkit@1.50.0: {} es6-error@4.1.1: @@ -7598,23 +9829,27 @@ snapshots: estree-util-is-identifier-name@3.0.0: {} + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 esutils@2.0.3: {} + eta@4.6.0: {} + etag@1.8.1: {} eventemitter3@5.0.4: {} events@3.3.0: {} - eventsource-parser@3.1.0: {} + eventsource-parser@3.1.1: {} eventsource@3.0.7: dependencies: - eventsource-parser: 3.1.0 + eventsource-parser: 3.1.1 execa@5.1.1: dependencies: @@ -7651,7 +9886,7 @@ snapshots: dependencies: debug: 4.4.3(supports-color@7.2.0) express: 5.2.1(supports-color@7.2.0) - ip-address: 10.4.0 + ip-address: 10.5.0 transitivePeerDependencies: - supports-color @@ -7690,24 +9925,24 @@ snapshots: extend@3.0.2: {} - fallow-type-aware@3.14.0: + fallow-type-aware@3.17.0: dependencies: typescript: 7.0.2 optional: true - fallow@3.14.0: + fallow@3.17.0: dependencies: detect-libc: 2.1.2 optionalDependencies: - '@fallow-cli/darwin-arm64': 3.14.0 - '@fallow-cli/darwin-x64': 3.14.0 - '@fallow-cli/linux-arm64-gnu': 3.14.0 - '@fallow-cli/linux-arm64-musl': 3.14.0 - '@fallow-cli/linux-x64-gnu': 3.14.0 - '@fallow-cli/linux-x64-musl': 3.14.0 - '@fallow-cli/win32-arm64-msvc': 3.14.0 - '@fallow-cli/win32-x64-msvc': 3.14.0 - fallow-type-aware: 3.14.0 + '@fallow-cli/darwin-arm64': 3.17.0 + '@fallow-cli/darwin-x64': 3.17.0 + '@fallow-cli/linux-arm64-gnu': 3.17.0 + '@fallow-cli/linux-arm64-musl': 3.17.0 + '@fallow-cli/linux-x64-gnu': 3.17.0 + '@fallow-cli/linux-x64-musl': 3.17.0 + '@fallow-cli/win32-arm64-msvc': 3.17.0 + '@fallow-cli/win32-x64-msvc': 3.17.0 + fallow-type-aware: 3.17.0 fast-deep-equal@3.1.3: {} @@ -7776,6 +10011,15 @@ snapshots: flatted@3.4.4: {} + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + form-data@4.0.6: dependencies: asynckit: 0.4.0 @@ -7841,8 +10085,24 @@ snapshots: function-bind@1.1.2: {} + function.prototype.name@1.2.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + es-define-property: 1.0.1 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + hasown: 2.0.4 + is-callable: 1.2.7 + is-document.all: 1.0.0 + + functions-have-names@1.2.3: {} + fuzzysort@3.1.0: {} + generator-function@2.0.1: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -7866,6 +10126,8 @@ snapshots: get-own-enumerable-keys@1.0.0: {} + get-own-enumerable-property-symbols@3.0.2: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -7882,6 +10144,12 @@ snapshots: '@sec-ant/readable-stream': 0.4.1 is-stream: 4.0.1 + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + github-slugger@2.0.0: {} glob-parent@5.1.2: @@ -7892,6 +10160,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@11.1.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.2.3 + minimatch: 10.2.6 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.2 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -7911,13 +10188,14 @@ snapshots: serialize-error: 7.0.1 optional: true + globals@17.11.0: {} + globals@17.9.0: {} globalthis@1.0.4: dependencies: define-properties: 1.2.1 gopd: 1.2.0 - optional: true gopd@1.2.0: {} @@ -7937,12 +10215,17 @@ snapshots: graceful-fs@4.2.11: {} + has-bigints@1.1.0: {} + has-flag@4.0.0: {} has-property-descriptors@1.0.2: dependencies: es-define-property: 1.0.1 - optional: true + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 has-symbols@1.1.0: {} @@ -8062,7 +10345,7 @@ snapshots: dependencies: hermes-estree: 0.25.1 - hono@4.13.1: {} + hono@4.13.3: {} hosted-git-info@4.1.0: dependencies: @@ -8109,6 +10392,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + idb@7.1.1: {} + ignore@5.3.2: {} ignore@7.0.6: {} @@ -8131,14 +10416,20 @@ snapshots: inline-style-parser@0.2.7: {} - input-otp@1.4.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + input-otp@1.5.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.4 + side-channel: 1.1.1 + internmap@2.0.3: {} - ip-address@10.4.0: {} + ip-address@10.5.0: {} ipaddr.js@1.9.1: {} @@ -8149,18 +10440,74 @@ snapshots: is-alphabetical: 2.0.1 is-decimal: 2.0.1 + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-arrayish@0.2.1: {} + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-decimal@2.0.1: {} is-docker@2.2.1: {} is-docker@3.0.0: {} + is-document.all@1.0.0: + dependencies: + call-bound: 1.0.4 + is-extglob@2.1.1: {} + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + is-fullwidth-code-point@3.0.0: {} + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -8175,8 +10522,21 @@ snapshots: is-interactive@2.0.0: {} + is-map@2.0.3: {} + + is-module@1.0.0: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-number@7.0.0: {} + is-obj@1.0.1: {} + is-obj@2.0.0: {} is-obj@3.0.0: {} @@ -8185,16 +10545,57 @@ snapshots: is-promise@4.0.0: {} + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + is-regexp@1.0.0: {} + is-regexp@3.1.0: {} + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + is-stream@2.0.1: {} is-stream@4.0.1: {} + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.22 + is-unicode-supported@1.3.0: {} is-unicode-supported@2.1.0: {} + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-wsl@2.2.0: dependencies: is-docker: 2.2.1 @@ -8205,6 +10606,8 @@ snapshots: isarray@1.0.0: {} + isarray@2.0.5: {} + isbinaryfile@4.0.10: {} isbinaryfile@5.0.7: {} @@ -8217,6 +10620,10 @@ snapshots: isexe@4.0.0: {} + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + jake@10.9.4: dependencies: async: 3.2.6 @@ -8227,6 +10634,8 @@ snapshots: jose@6.2.8: {} + jose@6.2.9: {} + js-tokens@4.0.0: {} js-yaml@4.3.1: @@ -8258,17 +10667,25 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonfile@5.0.0: + dependencies: + universalify: 0.1.2 + optionalDependencies: + graceful-fs: 4.2.11 + jsonfile@6.2.1: dependencies: universalify: 2.0.1 optionalDependencies: graceful-fs: 4.2.11 + jsonpointer@5.0.1: {} + katex@0.16.47: dependencies: commander: 8.3.0 - katex@0.18.2: + katex@0.18.4: dependencies: commander: 8.3.0 @@ -8282,6 +10699,8 @@ snapshots: lazy-val@1.0.5: {} + leven@3.1.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -8426,6 +10845,8 @@ snapshots: lowercase-keys@2.0.0: {} + lru-cache@11.5.2: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -8438,6 +10859,10 @@ snapshots: dependencies: react: 19.2.8 + lucide-react@1.32.0(react@19.2.8): + dependencies: + react: 19.2.8 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -8903,7 +11328,7 @@ snapshots: ms@2.1.3: {} - mtp@https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz: + mtp@https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz: dependencies: yaml: 2.9.0 @@ -8962,11 +11387,19 @@ snapshots: object-inspect@1.13.4: {} - object-keys@1.1.1: - optional: true + object-keys@1.1.1: {} object-treeify@1.1.33: {} + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + obug@2.1.4: {} on-finished@2.4.1: @@ -8993,14 +11426,14 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 - open@11.0.0: + open@11.0.1: dependencies: - default-browser: 5.5.0 + default-browser: 5.5.1 define-lazy-prop: 3.0.0 is-in-ssh: 1.0.0 is-inside-container: 1.0.0 - powershell-utils: 0.1.0 - wsl-utils: 0.3.1 + powershell-utils: 0.2.0 + wsl-utils: 1.0.0 open@8.4.2: dependencies: @@ -9029,6 +11462,13 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.2.0 + own-keys@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + p-cancelable@2.1.1: {} p-limit@2.3.0: @@ -9049,6 +11489,8 @@ snapshots: p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -9090,6 +11532,13 @@ snapshots: path-key@4.0.0: {} + path-parse@1.0.7: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.2 + minipass: 7.1.3 + path-to-regexp@8.4.2: {} pathe@2.0.3: {} @@ -9123,6 +11572,8 @@ snapshots: base64-js: 1.5.1 xmlbuilder: 15.1.1 + possible-typed-array-names@1.1.0: {} + postcss-selector-parser@7.1.5: dependencies: cssesc: 3.0.0 @@ -9141,10 +11592,16 @@ snapshots: powershell-utils@0.1.0: {} + powershell-utils@0.2.0: {} + prelude-ls@1.2.1: {} prettier@3.9.6: {} + pretty-bytes@5.6.0: {} + + pretty-bytes@6.1.1: {} + pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 @@ -9262,7 +11719,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.18 - react-resizable-panels@4.12.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + react-resizable-panels@4.12.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -9293,7 +11750,7 @@ snapshots: string_decoder: 1.1.1 util-deprecate: 1.0.2 - recast@0.23.19: + recast@0.23.21: dependencies: ast-types: 0.16.1 esprima: 4.0.1 @@ -9327,6 +11784,23 @@ snapshots: redux@5.0.1: {} + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regenerate-unicode-properties@10.2.2: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 @@ -9337,6 +11811,30 @@ snapshots: dependencies: regex-utilities: 2.3.0 + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + regexpu-core@6.4.0: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.2 + regjsgen: 0.8.0 + regjsparser: 0.13.2 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.1 + + regjsgen@0.8.0: {} + + regjsparser@0.13.2: + dependencies: + jsesc: 3.1.0 + rehype-autolink-headings@7.1.0: dependencies: '@types/hast': 3.0.5 @@ -9427,6 +11925,13 @@ snapshots: resolve-from@4.0.0: {} + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + responselike@2.0.1: dependencies: lowercase-keys: 2.0.0 @@ -9474,6 +11979,38 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.3 '@rolldown/binding-win32-x64-msvc': 1.2.3 + rollup@4.62.4: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.62.4 + '@rollup/rollup-android-arm64': 4.62.4 + '@rollup/rollup-darwin-arm64': 4.62.4 + '@rollup/rollup-darwin-x64': 4.62.4 + '@rollup/rollup-freebsd-arm64': 4.62.4 + '@rollup/rollup-freebsd-x64': 4.62.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.4 + '@rollup/rollup-linux-arm-musleabihf': 4.62.4 + '@rollup/rollup-linux-arm64-gnu': 4.62.4 + '@rollup/rollup-linux-arm64-musl': 4.62.4 + '@rollup/rollup-linux-loong64-gnu': 4.62.4 + '@rollup/rollup-linux-loong64-musl': 4.62.4 + '@rollup/rollup-linux-ppc64-gnu': 4.62.4 + '@rollup/rollup-linux-ppc64-musl': 4.62.4 + '@rollup/rollup-linux-riscv64-gnu': 4.62.4 + '@rollup/rollup-linux-riscv64-musl': 4.62.4 + '@rollup/rollup-linux-s390x-gnu': 4.62.4 + '@rollup/rollup-linux-x64-gnu': 4.62.4 + '@rollup/rollup-linux-x64-musl': 4.62.4 + '@rollup/rollup-openbsd-x64': 4.62.4 + '@rollup/rollup-openharmony-arm64': 4.62.4 + '@rollup/rollup-win32-arm64-msvc': 4.62.4 + '@rollup/rollup-win32-ia32-msvc': 4.62.4 + '@rollup/rollup-win32-x64-gnu': 4.62.4 + '@rollup/rollup-win32-x64-msvc': 4.62.4 + fsevents: 2.3.3 + router@2.2.0(supports-color@7.2.0): dependencies: debug: 4.4.3(supports-color@7.2.0) @@ -9494,8 +12031,27 @@ snapshots: dependencies: tslib: 2.8.1 + safe-array-concat@1.1.4: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + safe-buffer@5.1.2: {} + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + safer-buffer@2.1.2: {} sanitize-filename@1.6.4: @@ -9542,6 +12098,8 @@ snapshots: type-fest: 0.13.1 optional: true + serialize-javascript@7.1.0: {} + seroval-plugins@1.6.2(seroval@1.6.2): dependencies: seroval: 1.6.2 @@ -9557,9 +12115,31 @@ snapshots: transitivePeerDependencies: - supports-color + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + setprototypeof@1.2.0: {} - shadcn@4.16.2(supports-color@7.2.0)(typescript@6.0.3): + shadcn@4.18.0(supports-color@7.2.0)(typescript@6.0.3): dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/parser': 7.29.8 @@ -9568,7 +12148,7 @@ snapshots: '@dotenvx/dotenvx': 1.75.1 '@modelcontextprotocol/sdk': 1.30.0(supports-color@7.2.0)(zod@3.25.76) '@types/validate-npm-package-name': 4.0.2 - browserslist: 4.28.7 + browserslist: 4.28.8 commander: 14.0.3 cosmiconfig: 9.0.2(typescript@6.0.3) dedent: 1.7.2 @@ -9579,12 +12159,13 @@ snapshots: fs-extra: 11.4.0 fuzzysort: 3.1.0 kleur: 4.1.5 - open: 11.0.0 + open: 11.0.1 ora: 8.2.0 postcss: 8.5.26 postcss-selector-parser: 7.1.5 prompts: 2.4.2 - recast: 0.23.19 + recast: 0.23.21 + socks: 2.8.9 stringify-object: 5.0.0 tailwind-merge: 3.6.0 ts-morph: 26.0.0 @@ -9605,14 +12186,14 @@ snapshots: shebang-regex@3.0.0: {} - shiki@4.4.2: + shiki@4.4.3: dependencies: - '@shikijs/core': 4.4.2 - '@shikijs/engine-javascript': 4.4.2 - '@shikijs/engine-oniguruma': 4.4.2 - '@shikijs/langs': 4.4.2 - '@shikijs/themes': 4.4.2 - '@shikijs/types': 4.4.2 + '@shikijs/core': 4.4.3 + '@shikijs/engine-javascript': 4.4.3 + '@shikijs/engine-oniguruma': 4.4.3 + '@shikijs/langs': 4.4.3 + '@shikijs/themes': 4.4.3 + '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 @@ -9656,11 +12237,27 @@ snapshots: sisteransi@1.0.5: {} + smart-buffer@4.2.0: {} + + smob@1.6.2: {} + + socks@2.8.9: + dependencies: + ip-address: 10.5.0 + smart-buffer: 4.2.0 + sonner@2.0.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) + sonner@2.0.8(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -9670,6 +12267,8 @@ snapshots: source-map@0.6.1: {} + source-map@0.8.0: {} + space-separated-tokens@2.0.2: {} sprintf-js@1.1.3: @@ -9685,6 +12284,11 @@ snapshots: stdin-discarder@0.2.2: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -9697,6 +12301,46 @@ snapshots: get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.1 + + string.prototype.trim@1.2.11: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + has-property-descriptors: 1.0.2 + safe-regex-test: 1.1.0 + + string.prototype.trimend@1.0.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 @@ -9706,6 +12350,12 @@ snapshots: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 + stringify-object@3.3.0: + dependencies: + get-own-enumerable-property-symbols: 3.0.2 + is-obj: 1.0.1 + is-regexp: 1.0.0 + stringify-object@5.0.0: dependencies: get-own-enumerable-keys: 1.0.0 @@ -9718,10 +12368,12 @@ snapshots: strip-ansi@7.2.0: dependencies: - ansi-regex: 6.2.2 + ansi-regex: 6.3.0 strip-bom@3.0.0: {} + strip-comments@2.0.1: {} + strip-final-newline@2.0.0: {} strip-final-newline@4.0.0: {} @@ -9746,6 +12398,8 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} + systeminformation@5.33.1: {} tailwind-merge@3.6.0: {} @@ -9766,6 +12420,8 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 + temp-dir@2.0.0: {} + temp-file@3.4.0: dependencies: async-exit-hook: 2.0.1 @@ -9776,6 +12432,20 @@ snapshots: mkdirp: 0.5.6 rimraf: 2.6.3 + tempy@0.6.0: + dependencies: + is-stream: 2.0.1 + temp-dir: 2.0.0 + type-fest: 0.16.0 + unique-string: 2.0.0 + + terser@5.50.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.18.0 + commander: 2.20.3 + source-map-support: 0.5.21 + tiny-async-pool@1.3.0: dependencies: semver: 5.7.2 @@ -9839,12 +12509,47 @@ snapshots: type-fest@0.13.1: optional: true + type-fest@0.16.0: {} + type-is@2.1.0: dependencies: - content-type: 2.0.0 + content-type: 2.1.0 media-typer: 1.1.1 mime-types: 3.0.2 + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.8: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + typed-emitter@2.1.0: optionalDependencies: rxjs: 7.8.2 @@ -9860,6 +12565,17 @@ snapshots: transitivePeerDependencies: - supports-color + typescript-eslint@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + typescript@6.0.3: {} typescript@7.0.2: @@ -9886,6 +12602,13 @@ snapshots: '@typescript/typescript-win32-x64': 7.0.2 optional: true + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + undici-types@7.18.2: {} undici-types@8.3.0: {} @@ -9894,6 +12617,17 @@ snapshots: undici@7.29.0: {} + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.2.0 + + unicode-match-property-value-ecmascript@2.2.1: {} + + unicode-property-aliases-ecmascript@2.2.0: {} + unicorn-magic@0.3.0: {} unified@11.0.5: @@ -9906,6 +12640,10 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 + unique-string@2.0.0: + dependencies: + crypto-random-string: 2.0.0 + unist-util-find-after@5.0.0: dependencies: '@types/unist': 3.0.3 @@ -9953,9 +12691,11 @@ snapshots: graceful-fs: 4.2.11 node-int64: 0.4.0 - update-browserslist-db@1.3.0(browserslist@4.28.7): + upath@1.2.0: {} + + update-browserslist-db@1.3.1(browserslist@4.28.8): dependencies: - browserslist: 4.28.7 + browserslist: 4.28.8 escalade: 3.2.0 picocolors: 1.1.1 @@ -10036,7 +12776,18 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0): + vite-plugin-pwa@1.3.0(supports-color@7.2.0)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))(workbox-build@7.4.1(supports-color@7.2.0))(workbox-window@7.4.1): + dependencies: + debug: 4.4.3(supports-color@7.2.0) + pretty-bytes: 6.1.1 + tinyglobby: 0.2.17 + vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0) + workbox-build: 7.4.1(supports-color@7.2.0) + workbox-window: 7.4.1 + transitivePeerDependencies: + - supports-color + + vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 @@ -10048,18 +12799,19 @@ snapshots: esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.7.0 + terser: 5.50.0 yaml: 2.9.0 - vitest@4.1.10(@types/node@26.2.0)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)): + vitest@4.1.11(@types/node@26.2.0)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)): dependencies: - '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.10 - '@vitest/runner': 4.1.10 - '@vitest/snapshot': 4.1.10 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - es-module-lexer: 2.3.1 + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + es-module-lexer: 2.3.2 expect-type: 1.4.0 magic-string: 0.30.21 obug: 2.1.4 @@ -10070,7 +12822,7 @@ snapshots: tinyexec: 1.3.0 tinyglobby: 0.2.17 tinyrainbow: 3.1.1 - vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 26.2.0 @@ -10093,6 +12845,47 @@ snapshots: dependencies: sdp: 3.2.2 + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.2.0 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.22 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.22: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -10116,6 +12909,119 @@ snapshots: word-wrap@1.2.5: {} + workbox-background-sync@7.4.1: + dependencies: + idb: 7.1.1 + workbox-core: 7.4.1 + + workbox-broadcast-update@7.4.1: + dependencies: + workbox-core: 7.4.1 + + workbox-build@7.4.1(supports-color@7.2.0): + dependencies: + '@apideck/better-ajv-errors': 0.3.7(ajv@8.20.0) + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/preset-env': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/runtime': 7.29.7 + '@rollup/plugin-babel': 6.1.0(@babel/core@7.29.7(supports-color@7.2.0))(rollup@4.62.4)(supports-color@7.2.0) + '@rollup/plugin-node-resolve': 16.0.3(rollup@4.62.4) + '@rollup/plugin-replace': 6.0.3(rollup@4.62.4) + '@rollup/plugin-terser': 1.0.0(rollup@4.62.4) + '@trickfilm400/rollup-plugin-off-main-thread': 3.0.0-pre1 + ajv: 8.20.0 + common-tags: 1.8.2 + eta: 4.6.0 + fast-json-stable-stringify: 2.1.0 + fs-extra: 9.1.0 + glob: 11.1.0 + pretty-bytes: 5.6.0 + rollup: 4.62.4 + source-map: 0.8.0 + stringify-object: 3.3.0 + strip-comments: 2.0.1 + tempy: 0.6.0 + upath: 1.2.0 + workbox-background-sync: 7.4.1 + workbox-broadcast-update: 7.4.1 + workbox-cacheable-response: 7.4.1 + workbox-core: 7.4.1 + workbox-expiration: 7.4.1 + workbox-google-analytics: 7.4.1 + workbox-navigation-preload: 7.4.1 + workbox-precaching: 7.4.1 + workbox-range-requests: 7.4.1 + workbox-recipes: 7.4.1 + workbox-routing: 7.4.1 + workbox-strategies: 7.4.1 + workbox-streams: 7.4.1 + workbox-sw: 7.4.1 + workbox-window: 7.4.1 + transitivePeerDependencies: + - '@types/babel__core' + - supports-color + + workbox-cacheable-response@7.4.1: + dependencies: + workbox-core: 7.4.1 + + workbox-core@7.4.1: {} + + workbox-expiration@7.4.1: + dependencies: + idb: 7.1.1 + workbox-core: 7.4.1 + + workbox-google-analytics@7.4.1: + dependencies: + workbox-background-sync: 7.4.1 + workbox-core: 7.4.1 + workbox-routing: 7.4.1 + workbox-strategies: 7.4.1 + + workbox-navigation-preload@7.4.1: + dependencies: + workbox-core: 7.4.1 + + workbox-precaching@7.4.1: + dependencies: + workbox-core: 7.4.1 + workbox-routing: 7.4.1 + workbox-strategies: 7.4.1 + + workbox-range-requests@7.4.1: + dependencies: + workbox-core: 7.4.1 + + workbox-recipes@7.4.1: + dependencies: + workbox-cacheable-response: 7.4.1 + workbox-core: 7.4.1 + workbox-expiration: 7.4.1 + workbox-precaching: 7.4.1 + workbox-routing: 7.4.1 + workbox-strategies: 7.4.1 + + workbox-routing@7.4.1: + dependencies: + workbox-core: 7.4.1 + + workbox-strategies@7.4.1: + dependencies: + workbox-core: 7.4.1 + + workbox-streams@7.4.1: + dependencies: + workbox-core: 7.4.1 + workbox-routing: 7.4.1 + + workbox-sw@7.4.1: {} + + workbox-window@7.4.1: + dependencies: + '@types/trusted-types': 2.0.7 + workbox-core: 7.4.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -10124,7 +13030,7 @@ snapshots: wrappy@1.0.2: {} - wsl-utils@0.3.1: + wsl-utils@1.0.0: dependencies: is-wsl: 3.1.1 powershell-utils: 0.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2bcade5..6ec3531 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,5 +6,5 @@ allowBuilds: electron-winstaller: true esbuild: true overrides: - "@methanium/ui": "https://git.methanium.net/methanium/ui/releases/download/0.0.28/methanium-ui.tgz" - mtp: "https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz" + "@methanium/ui": "https://git.methanium.net/methanium/ui/releases/download/0.0.29/methanium-ui.tgz" + mtp: "https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz" diff --git a/utils/scripts/build-packages.ts b/utils/scripts/build-packages.ts index 9518c49..1f1f4fd 100644 --- a/utils/scripts/build-packages.ts +++ b/utils/scripts/build-packages.ts @@ -15,7 +15,14 @@ function getPackageDirs(dir: string): string[] { for (const entry of entries) { const fullPath = join(dir, entry); - if (!statSync(fullPath).isDirectory()) continue; + if (entry === "node_modules" || entry.startsWith(".")) continue; + let stats; + try { + stats = statSync(fullPath); + } catch { + continue; + } + if (!stats.isDirectory()) continue; if (existsSync(join(fullPath, "package.json"))) { dirs.push(fullPath); diff --git a/utils/scripts/lint-packages.ts b/utils/scripts/lint-packages.ts index 0aea8c3..c84e0e0 100644 --- a/utils/scripts/lint-packages.ts +++ b/utils/scripts/lint-packages.ts @@ -16,7 +16,14 @@ function getPackageDirs(dir: string): string[] { for (const entry of entries) { const fullPath = join(dir, entry); - if (!statSync(fullPath).isDirectory()) continue; + if (entry === "node_modules" || entry.startsWith(".")) continue; + let stats; + try { + stats = statSync(fullPath); + } catch { + continue; + } + if (!stats.isDirectory()) continue; if (existsSync(join(fullPath, "package.json"))) { dirs.push(fullPath); diff --git a/utils/scripts/update-packages.ts b/utils/scripts/update-packages.ts index 22b258f..90b3b0c 100644 --- a/utils/scripts/update-packages.ts +++ b/utils/scripts/update-packages.ts @@ -15,7 +15,14 @@ function getPackageDirs(dir: string): string[] { for (const entry of entries) { const fullPath = join(dir, entry); - if (!statSync(fullPath).isDirectory()) continue; + if (entry === "node_modules" || entry.startsWith(".")) continue; + let stats; + try { + stats = statSync(fullPath); + } catch { + continue; + } + if (!stats.isDirectory()) continue; if (existsSync(join(fullPath, "package.json"))) { dirs.push(fullPath);