diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48a4879..216433c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,11 +17,28 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 22 + # The Vault crypto core is Rust -> WASM. tsc and `next build` import the + # generated bindings (src/lib/vault/pkg, gitignored), so CI must build + # them before typechecking — this also verifies the crypto FFI contract. + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + with: + workspaces: crypto-core + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh # Installs @mind-studio/* from GitHub Packages via the committed .npmrc. - run: npm ci env: NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN || secrets.GITHUB_TOKEN }} + - name: Build crypto-core WASM bindings + run: npm run wasm + # Lint + format gate (Biome). Fails on errors; warnings are the + # documented legacy baseline tracked in biome.json. + - run: npx biome ci . - run: npx tsc --noEmit + - run: npm run test - run: npm run build env: NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..2312dc5 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 0000000..81b473f --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,15 @@ +# Fast confidence gate before code leaves this machine — mirrors CI's +# correctness checks. The slow `next build` is left to CI. +# Bypass in a pinch with `git push --no-verify`. +npm run lint || exit 1 + +# Typecheck needs the generated WASM bindings (src/lib/vault/pkg, built via +# `npm run wasm`). Skip when absent so contributors without the Rust toolchain +# aren't blocked — CI always builds WASM and typechecks for real. +if [ -f src/lib/vault/pkg/crypto_core.d.ts ]; then + npm run typecheck || exit 1 +else + echo "⏭ Skipping typecheck — WASM bindings not built (run 'npm run wasm'). CI typechecks for real." +fi + +npm run test || exit 1 diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..b84f9a2 --- /dev/null +++ b/biome.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.0/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": true, + "includes": [ + "**", + "!**/src/lib/vault/pkg", + "!**/crypto-core", + "!**/src-tauri", + "!**/public", + "!**/scripts", + "!**/next-env.d.ts", + "!**/package-lock.json", + "!**/*.css" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "linter": { + "enabled": true, + "domains": { + "next": "recommended", + "react": "recommended" + }, + "rules": { + "recommended": true, + "correctness": { + "useExhaustiveDependencies": "warn" + }, + "suspicious": { + "noImplicitAnyLet": "warn", + "useIterableCallbackReturn": "warn", + "noArrayIndexKey": "warn" + }, + "a11y": { + "useButtonType": "warn", + "noLabelWithoutControl": "warn", + "noStaticElementInteractions": "warn", + "noSvgWithoutTitle": "warn", + "noAutofocus": "warn" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/package-lock.json b/package-lock.json index 2513820..429b3b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,14 +20,18 @@ "react-dom": "19.2.4" }, "devDependencies": { + "@biomejs/biome": "^2.5.0", "@tailwindcss/postcss": "^4", "@tauri-apps/cli": "^2.11.0", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "husky": "^9.1.7", + "lint-staged": "^17.0.7", "tailwindcss": "^4", "tsx": "^4.22.3", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.1.9" } }, "node_modules/@alloc/quick-lru": { @@ -124,6 +128,169 @@ "buffer": "^6.0.3" } }, + "node_modules/@biomejs/biome": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.0.tgz", + "integrity": "sha512-4kURkd9hAPrdDM3C9n82ycYgx8hvQcW6MjKTEejruj8rK0N8P3OPpdy8BvI8kt3KWY4ycF5XtDOrktetEfhfuw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.5.0", + "@biomejs/cli-darwin-x64": "2.5.0", + "@biomejs/cli-linux-arm64": "2.5.0", + "@biomejs/cli-linux-arm64-musl": "2.5.0", + "@biomejs/cli-linux-x64": "2.5.0", + "@biomejs/cli-linux-x64-musl": "2.5.0", + "@biomejs/cli-win32-arm64": "2.5.0", + "@biomejs/cli-win32-x64": "2.5.0" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-Mn3Fwi3SA5fgmfCPqmzpWF2DLZnms3BVAhM088nTnGrTZmHS3wwIjcoZPqpXeNgd3DrrLH6xp8vTLIBuJoZiXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.0.tgz", + "integrity": "sha512-rg3VPL5P8mYro6pqlXYXuJWph21slVp3SZtAqWSrkZs40d2gTzYmHF8E/X1iTID25btmNKltNDJ926sqVBp7DQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.0.tgz", + "integrity": "sha512-tl+LW8fdD96/xdeWtWwc82LIOc5CoY7N2AsogLTp5R4ECErYt+8Jl/N68ezN9vzSiqPTxw6vjcihoLPYKZHrlw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-vQdM4oSGaf7ZNeGO9w5+Y8SBtyser9M6znxYbm7Ec8wInxJu1WiKxFYZW5Auj2d80bcVvefuGGRxoFOE0eee8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.0.tgz", + "integrity": "sha512-zpEGf4RQbFEh8Vt7OmavLyyOzRbtcE9osCqrS1kfvt8jDvxwhKXLSf7n0ebr/ov0RJ9ssP+lhs6C8a9WwFvrQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.0.tgz", + "integrity": "sha512-+9hIcMngJ+yGUahXqZuZ8CoWKJE9SAZsFsM3QDvXpNsLbXZ9lqVzgBhOk/jTSYkOA0GLP9eu3teukqpLUojHMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.0.tgz", + "integrity": "sha512-jB0wAvTLI4itx5VidqVUejPQFhRUxiZ9l9FvZ26D5fl6t3qme+ZB4PD3bTSeL1vZ8NI2Rx/zj6H9zcESuGHKGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.0.tgz", + "integrity": "sha512-VT/lF+GId+67j8aDfLkxdxNoVApsPSTbyAtB3jJq0IWTrY77WXfbPfpngxq0bA6JCEv/7k8C9qWjDRKRznDlyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@date-fns/tz": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.5.0.tgz", @@ -197,6 +364,18 @@ "react": ">=16.8.0" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", @@ -207,6 +386,17 @@ "tslib": "^2.4.0" } }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", @@ -1373,6 +1563,25 @@ "tailwindcss": "^4.0.0" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@next/env": { "version": "16.2.6", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", @@ -1507,6 +1716,16 @@ "node": ">= 10" } }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.2.tgz", @@ -3069,106 +3288,10 @@ "url": "https://opencollective.com/immer" } }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" - }, - "node_modules/@standard-schema/utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", - "license": "MIT" - }, - "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@tabby_ai/hijri-converter": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@tabby_ai/hijri-converter/-/hijri-converter-1.0.5.tgz", - "integrity": "sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@tabler/icons": { - "version": "3.44.0", - "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.44.0.tgz", - "integrity": "sha512-Wn0AOZG9sg0L+bjfMqq4eNhC6pQjIrk94LvvWYNYkY8KH8wC3YILRzQlrnVJc4FUeMxH/AK97QsYCX35H3LndA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/codecalm" - } - }, - "node_modules/@tabler/icons-react": { - "version": "3.44.0", - "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.44.0.tgz", - "integrity": "sha512-8+rvzBbVm/1Z3sG3x7GUNAaxIKxwgz8xaMhRs23nrCnMTKRFAhEC+82zAIFeAA0seXdrAGX5HFCkaLpGK2rVHg==", - "license": "MIT", - "dependencies": { - "@tabler/icons": "3.44.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/codecalm" - }, - "peerDependencies": { - "react": ">= 16" - } - }, - "node_modules/@tailwindcss/node": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", - "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.21.0", - "jiti": "^2.6.1", - "lightningcss": "1.32.0", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.3.0" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", - "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.0", - "@tailwindcss/oxide-darwin-arm64": "4.3.0", - "@tailwindcss/oxide-darwin-x64": "4.3.0", - "@tailwindcss/oxide-freebsd-x64": "4.3.0", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", - "@tailwindcss/oxide-linux-x64-musl": "4.3.0", - "@tailwindcss/oxide-wasm32-wasi": "4.3.0", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", - "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", "cpu": [ "arm64" ], @@ -3179,13 +3302,13 @@ "android" ], "engines": { - "node": ">= 20" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", - "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -3196,13 +3319,13 @@ "darwin" ], "engines": { - "node": ">= 20" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", - "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -3213,13 +3336,13 @@ "darwin" ], "engines": { - "node": ">= 20" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", - "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", "cpu": [ "x64" ], @@ -3230,13 +3353,13 @@ "freebsd" ], "engines": { - "node": ">= 20" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", - "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", "cpu": [ "arm" ], @@ -3247,11 +3370,371 @@ "linux" ], "engines": { - "node": ">= 20" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.0", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tabby_ai/hijri-converter": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@tabby_ai/hijri-converter/-/hijri-converter-1.0.5.tgz", + "integrity": "sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@tabler/icons": { + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.44.0.tgz", + "integrity": "sha512-Wn0AOZG9sg0L+bjfMqq4eNhC6pQjIrk94LvvWYNYkY8KH8wC3YILRzQlrnVJc4FUeMxH/AK97QsYCX35H3LndA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + } + }, + "node_modules/@tabler/icons-react": { + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.44.0.tgz", + "integrity": "sha512-8+rvzBbVm/1Z3sG3x7GUNAaxIKxwgz8xaMhRs23nrCnMTKRFAhEC+82zAIFeAA0seXdrAGX5HFCkaLpGK2rVHg==", + "license": "MIT", + "dependencies": { + "@tabler/icons": "3.44.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + }, + "peerDependencies": { + "react": ">= 16" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", "cpu": [ @@ -3722,6 +4205,28 @@ "node": ">= 10" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -3785,59 +4290,186 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, - "node_modules/@types/http-link-header": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/http-link-header/-/http-link-header-1.0.7.tgz", - "integrity": "sha512-snm5oLckop0K3cTDAiBnZDy6ncx9DJ3mCRDvs42C884MbVYPP74Tiq2hFsSDRTyjK6RyDYDIulPiW23ge+g5Lw==", + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-link-header": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/http-link-header/-/http-link-header-1.0.7.tgz", + "integrity": "sha512-snm5oLckop0K3cTDAiBnZDy6ncx9DJ3mCRDvs42C884MbVYPP74Tiq2hFsSDRTyjK6RyDYDIulPiW23ge+g5Lw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", + "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz", + "integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@types/node": { - "version": "20.19.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", - "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@types/react": { - "version": "19.2.15", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", - "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", - "devOptional": true, + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, "license": "MIT", "dependencies": { - "csstype": "^3.2.2" + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "devOptional": true, + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@types/readable-stream": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz", - "integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==", + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", - "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", - "license": "MIT" - }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -3850,6 +4482,48 @@ "node": ">=6.5" } }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/aria-hidden": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", @@ -3862,6 +4536,16 @@ "node": ">=10" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -3944,6 +4628,16 @@ "integrity": "sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A==", "license": "Apache-2.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -3956,6 +4650,39 @@ "url": "https://polar.sh/cva" } }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -3987,6 +4714,13 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -4181,6 +4915,13 @@ "embla-carousel": "8.6.0" } }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.22.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz", @@ -4195,6 +4936,26 @@ "node": ">=10.13.0" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, "node_modules/es-toolkit": { "version": "1.47.1", "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.1.tgz", @@ -4247,6 +5008,16 @@ "@esbuild/win32-x64": "0.28.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -4271,6 +5042,34 @@ "node": ">=0.8.x" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4285,6 +5084,19 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -4310,6 +5122,22 @@ "node": ">=6.0.0" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -4359,6 +5187,22 @@ "node": ">=12" } }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -4703,6 +5547,121 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lint-staged": { + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.0.7.tgz", + "integrity": "sha512-JrSobt+tW3rH8IOMi8tDZd3foorM5yPEkLD/V2NxobgHrFfHWGee4MOLVuZeScgxftEwbHrPHIFA/ZL+nUJeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "listr2": "^10.2.1", + "picomatch": "^4.0.4", + "string-argv": "^0.3.2", + "tinyexec": "^1.2.4" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=22.22.1" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + }, + "optionalDependencies": { + "yaml": "^2.9.0" + } + }, + "node_modules/listr2": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.1.tgz", + "integrity": "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.2.0", + "eventemitter3": "^5.0.4", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^10.0.0" + }, + "engines": { + "node": ">=22.13.0" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -4730,8 +5689,21 @@ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/n3": { @@ -4865,6 +5837,20 @@ "node": ">= 6" } }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/oidc-client-ts": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.5.0.tgz", @@ -4886,6 +5872,22 @@ "node": "^10.13.0 || >=12.0.0" } }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/openid-client": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", @@ -4910,12 +5912,32 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", @@ -5295,6 +6317,64 @@ "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", "license": "MIT" }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -5379,6 +6459,43 @@ "@img/sharp-win32-x64": "0.34.5" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -5398,6 +6515,20 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -5407,6 +6538,49 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -5466,6 +6640,50 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -5611,12 +6829,232 @@ "d3-timer": "^3.0.1" } }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "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.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", + "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "string-width": "^8.2.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", diff --git a/package.json b/package.json index 05594e4..65cca27 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,11 @@ "build": "next build", "start": "next start -p 3100", "typecheck": "tsc --noEmit", - "lint": "next lint", + "lint": "biome check .", + "lint:fix": "biome check --write .", + "format": "biome format --write .", + "test": "vitest run --passWithNoTests", + "prepare": "husky", "seed:demo": "tsx scripts/seed-demo.ts", "seed:embed-demo": "tsx scripts/seed-embed-demo.ts", "seed:drive": "tsx scripts/seed-drive.ts", @@ -35,13 +39,20 @@ "react-dom": "19.2.4" }, "devDependencies": { + "@biomejs/biome": "^2.5.0", "@tailwindcss/postcss": "^4", "@tauri-apps/cli": "^2.11.0", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "husky": "^9.1.7", + "lint-staged": "^17.0.7", "tailwindcss": "^4", "tsx": "^4.22.3", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.1.9" + }, + "lint-staged": { + "*.{js,jsx,ts,tsx,json,jsonc}": "biome check --write --no-errors-on-unmatched" } } diff --git a/src/app/connect/page.tsx b/src/app/connect/page.tsx index ecbc70a..e9be222 100644 --- a/src/app/connect/page.tsx +++ b/src/app/connect/page.tsx @@ -8,8 +8,7 @@ const ISSUER = process.env.NEXT_PUBLIC_SOLID_ISSUER ?? process.env.NEXT_PUBLIC_POD_BASE_URL ?? "https://pods.mindpods.org/"; -const IS_LOCAL_ISSUER = - ISSUER.includes("localhost") || ISSUER.includes("127.0.0.1"); +const IS_LOCAL_ISSUER = ISSUER.includes("localhost") || ISSUER.includes("127.0.0.1"); export default function ConnectPage() { return ( @@ -21,8 +20,8 @@ export default function ConnectPage() { Sign in once. Everything lives in your pod.

- One surface wraps your identity: switch workspaces, projects and apps — - starting with Vault, a password manager whose secrets only you can read. + One surface wraps your identity: switch workspaces, projects and apps — starting with Vault, + a password manager whose secrets only you can read.

@@ -33,9 +32,7 @@ export default function ConnectPage() {

Dev shortcut

-

- The local CSS instance on port 3101 has two pre-seeded accounts: -

+

The local CSS instance on port 3101 has two pre-seeded accounts:

) : ( -

- {note || "Nothing here yet."} -

+

{note || "Nothing here yet."}

)} {scopeEnforced !== null && ( diff --git a/src/app/widget/up-next/page.tsx b/src/app/widget/up-next/page.tsx index 023c213..cf4a5b0 100644 --- a/src/app/widget/up-next/page.tsx +++ b/src/app/widget/up-next/page.tsx @@ -65,14 +65,14 @@ export default function UpNextWidget() { } catch { return { name: f.name, title: "(unreadable event)", start: null } as Event; } - }) + }), ); // Upcoming only (anything from the start of today), soonest first. const floor = new Date(); floor.setHours(0, 0, 0, 0); const upcoming = all .filter((e) => e.start && e.start.getTime() >= floor.getTime()) - .sort((a, b) => (a.start!.getTime() - b.start!.getTime())) + .sort((a, b) => a.start!.getTime() - b.start!.getTime()) .slice(0, 4); setEvents(upcoming); }, []); diff --git a/src/app/widget/vault-glance/page.tsx b/src/app/widget/vault-glance/page.tsx index a513184..94a3f55 100644 --- a/src/app/widget/vault-glance/page.tsx +++ b/src/app/widget/vault-glance/page.tsx @@ -80,7 +80,13 @@ export default function VaultGlanceWidget() { const hoverBg = dark ? "#141417" : "#fafafa"; const label = - count === null ? "…" : count === 0 ? "Vault is empty" : count === 1 ? "item secured" : "items secured"; + count === null + ? "…" + : count === 0 + ? "Vault is empty" + : count === 1 + ? "item secured" + : "items secured"; return (
a.key === ref.slice(0, i)); @@ -77,7 +74,7 @@ export default function HomeApp() { setItems(renumbered); if (workspacePod) void writeHomeLayout(workspacePod, renumbered).catch(() => {}); }, - [workspacePod] + [workspacePod], ); const onDrop = useCallback( @@ -90,7 +87,7 @@ export default function HomeApp() { next.splice(target, 0, moved); persist(next); }, - [items, persist] + [items, persist], ); const tiles = useMemo( @@ -98,7 +95,7 @@ export default function HomeApp() { (items ?? []) .map((it) => ({ it, r: resolveRef(it.ref, apps) })) .filter((t): t is { it: HomeLayoutItem; r: NonNullable } => !!t.r), - [items, apps] + [items, apps], ); if (items === null) { diff --git a/src/apps/identity/index.tsx b/src/apps/identity/index.tsx index b0484de..6edd3e5 100644 --- a/src/apps/identity/index.tsx +++ b/src/apps/identity/index.tsx @@ -18,45 +18,40 @@ * the prototype (the caveat is shown in the UI). */ -import { useCallback, useEffect, useState } from "react"; import { Button, - Input, - Label, Dialog, DialogContent, - DialogHeader, - DialogTitle, DialogDescription, DialogFooter, + DialogHeader, + DialogTitle, + Input, + Label, } from "@mind-studio/ui"; -import { useShell } from "@/lib/shell/context"; +import { useCallback, useEffect, useState } from "react"; +import { readAndVerify, writeBinding } from "@/lib/identity/binding"; +import { enterPassport, logoutPassport } from "@/lib/identity/passport-login"; +import { captureManualPassport, provisionPassport } from "@/lib/identity/provision"; +import { type Passport, walletBackupUrl } from "@/lib/identity/types"; import { + addPassport, createWallet, - unlockWallet, + exportBlob, lockWallet, - addPassport, + unlockWallet, updatePassport, sign as walletSign, - exportBlob, } from "@/lib/identity/wallet"; -import { provisionPassport, captureManualPassport } from "@/lib/identity/provision"; -import { enterPassport, logoutPassport } from "@/lib/identity/passport-login"; -import { - getActivePassportSession, - subscribePassportSession, -} from "@/lib/solid/passport-session"; -import { writeBinding, readAndVerify } from "@/lib/identity/binding"; +import { useShell } from "@/lib/shell/context"; import { loginWithDid, serverSupportsDid } from "@/lib/solid/did-account"; -import { walletBackupUrl, type Passport } from "@/lib/identity/types"; -import { writeFileText, ensureContainerChain } from "@/lib/solid/pod-fs"; +import { getActivePassportSession, subscribePassportSession } from "@/lib/solid/passport-session"; +import { ensureContainerChain, writeFileText } from "@/lib/solid/pod-fs"; import { useWallet } from "./useWallet"; /** Subscribe to the active passport-session id (null when on the main WebID). */ function useActivePassportId(): string | null { - const [id, setId] = useState( - () => getActivePassportSession()?.passportId ?? null - ); + const [id, setId] = useState(() => getActivePassportSession()?.passportId ?? null); useEffect(() => { const sync = () => setId(getActivePassportSession()?.passportId ?? null); sync(); @@ -77,8 +72,8 @@ export default function IdentityApp() {

Your wallet

- A portable master identity (a did:key) that controls your - per-server passports — without replacing any WebID or changing any server. + A portable master identity (a did:key) that controls your per-server + passports — without replacing any WebID or changing any server.

@@ -128,8 +123,8 @@ function CreateScreen() {

Create your master identity

- We generate an Ed25519 key inside the audited crypto core and wrap its seed - under this master password. The seed never leaves the core. + We generate an Ed25519 key inside the audited crypto core and wrap its seed under this + master password. The seed never leaves the core.

@@ -267,8 +262,7 @@ function Dashboard({ did, passports }: { did: string; passports: Passport[] }) { {activePassport.webId}

- No password typed — the shell signed in for you with this passport's - stored key. + No password typed — the shell signed in for you with this passport's stored key.

- Each passport is a fresh WebID + pod on a server, all controlled by your - one master DID. + Each passport is a fresh WebID + pod on a server, all controlled by your one master DID.

{passports.length === 0 && ( @@ -397,7 +387,7 @@ function SessionBindingCard({ setStatus( result.ok ? "Verified: this WebID is controlled by your DID — checked locally, no server." - : `Not verified: ${result.reason ?? "unknown"}.` + : `Not verified: ${result.reason ?? "unknown"}.`, ); } catch (e) { setVerdict("fail"); @@ -412,8 +402,8 @@ function SessionBindingCard({

This session

{webId}

- Prove your master DID controls the WebID you're signed in as, by writing a - signed binding into this pod. + Prove your master DID controls the WebID you're signed in as, by writing a signed binding + into this pod.

) : (

- This passport was captured manually (no stored key). Sign in as it the - usual way to write its binding or open its pod. + This passport was captured manually (no stored key). Sign in as it the usual way to + write its binding or open its pod.

{didSupported === false ? (

- This server is stock CSS — it doesn't accept DID login. The shell - signs you in here with this passport's stored key instead. + This server is stock CSS — it doesn't accept DID login. The shell signs you in here + with this passport's stored key instead.

) : ( <>

- Prove control of your master DID by signing a one-time challenge and - get a fresh account session — no password, no redirect. + Prove control of your master DID by signing a one-time challenge and get a fresh + account session — no password, no redirect.

@@ -864,10 +854,9 @@ function AddPassportDialog({ did, onClose }: { did: string; onClose: () => void

The shell creates a brand-new account on the server with a fresh WebID, - generates a strong password for you (you never type or - see one), and stores a key so it can sign you in later with no password - and no redirect. The key is sealed in your encrypted wallet — never - written to a pod. + generates a strong password for you (you never type or see one), and + stores a key so it can sign you in later with no password and no redirect. The key is + sealed in your encrypted wallet — never written to a pod.

) : ( @@ -925,9 +914,8 @@ function Field({ function XssCaveat() { return (

- Prototype note: in the browser, your unlocked seed lives in memory for the - session (a documented XSS tradeoff). The hardened native build custodies it - in the OS keychain. + Prototype note: in the browser, your unlocked seed lives in memory for the session (a + documented XSS tradeoff). The hardened native build custodies it in the OS keychain.

); } diff --git a/src/apps/identity/useWallet.ts b/src/apps/identity/useWallet.ts index 5054b60..692a233 100644 --- a/src/apps/identity/useWallet.ts +++ b/src/apps/identity/useWallet.ts @@ -1,8 +1,8 @@ "use client"; import { useEffect, useState } from "react"; -import { getView, subscribe } from "@/lib/identity/wallet"; import type { WalletView } from "@/lib/identity/types"; +import { getView, subscribe } from "@/lib/identity/wallet"; /** * Subscribe a component to the process-wide wallet singleton. Re-renders on every diff --git a/src/apps/registry.tsx b/src/apps/registry.tsx index 15e56fc..0faf681 100644 --- a/src/apps/registry.tsx +++ b/src/apps/registry.tsx @@ -1,6 +1,6 @@ "use client"; -import { lazy, type ComponentType } from "react"; +import { type ComponentType, lazy } from "react"; /** * Registry of in-process apps the shell can render in the app body (PRD §3: diff --git a/src/apps/vault/ItemDetail.tsx b/src/apps/vault/ItemDetail.tsx index c31375f..bf26030 100644 --- a/src/apps/vault/ItemDetail.tsx +++ b/src/apps/vault/ItemDetail.tsx @@ -1,10 +1,10 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; import { Button, Separator } from "@mind-studio/ui"; +import { useCallback, useEffect, useState } from "react"; import type { AsyncCryptoCore } from "@/lib/platform"; -import type { VaultItemMeta, VaultItemSecret } from "@/lib/vault/model"; import { copyWithAutoClear } from "@/lib/vault/clipboard"; +import type { VaultItemMeta, VaultItemSecret } from "@/lib/vault/model"; const TOTP_PERIOD = 30; const TOTP_DIGITS = 6; diff --git a/src/apps/vault/ItemEditor.tsx b/src/apps/vault/ItemEditor.tsx index c7d95a1..e9176f3 100644 --- a/src/apps/vault/ItemEditor.tsx +++ b/src/apps/vault/ItemEditor.tsx @@ -1,22 +1,22 @@ "use client"; -import { useEffect, useState } from "react"; import { Button, - Input, - Label, Dialog, DialogContent, - DialogHeader, - DialogTitle, DialogDescription, DialogFooter, + DialogHeader, + DialogTitle, + Input, + Label, Select, - SelectTrigger, - SelectValue, SelectContent, SelectItem, + SelectTrigger, + SelectValue, } from "@mind-studio/ui"; +import { useEffect, useState } from "react"; import type { AsyncCryptoCore } from "@/lib/platform"; import type { VaultItemKind, VaultItemMeta, VaultItemSecret } from "@/lib/vault/model"; import { PasswordGenerator } from "./PasswordGenerator"; @@ -215,7 +215,7 @@ export function ItemEditor({ digits: true, symbols: true, avoidAmbiguous: true, - }) + }), ); } catch { // Panel still opens; its own Generate surfaces errors. diff --git a/src/apps/vault/PasswordGenerator.tsx b/src/apps/vault/PasswordGenerator.tsx index 3824b06..951fb67 100644 --- a/src/apps/vault/PasswordGenerator.tsx +++ b/src/apps/vault/PasswordGenerator.tsx @@ -1,7 +1,16 @@ "use client"; +import { + Button, + Checkbox, + Input, + Label, + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@mind-studio/ui"; import { useCallback, useState } from "react"; -import { Button, Input, Label, Checkbox, Tabs, TabsList, TabsTrigger, TabsContent } from "@mind-studio/ui"; import type { AsyncCryptoCore } from "@/lib/platform"; import { copyWithAutoClear } from "@/lib/vault/clipboard"; @@ -34,7 +43,7 @@ export function PasswordGenerator({ setError(null); try { setValue( - await core.generatePassword({ length, upper, lower, digits, symbols, avoidAmbiguous }) + await core.generatePassword({ length, upper, lower, digits, symbols, avoidAmbiguous }), ); } catch (e) { setError(e instanceof Error ? e.message : "Could not generate"); diff --git a/src/apps/vault/ProviderAccounts.tsx b/src/apps/vault/ProviderAccounts.tsx index bcf36de..bbe301e 100644 --- a/src/apps/vault/ProviderAccounts.tsx +++ b/src/apps/vault/ProviderAccounts.tsx @@ -15,25 +15,22 @@ * carries the reusable-login warning (PRD-PROVIDER-ACCOUNTS §3 tradeoff). */ -import { useEffect, useState } from "react"; import { Button, Input, Separator } from "@mind-studio/ui"; +import { useEffect, useState } from "react"; import { - getView, - getPassports, - subscribe, - markEmailVerified, - addManualProviderAccount, -} from "@/lib/identity/wallet"; -import { - projectProviderAccounts, - type ProviderAccount, -} from "@/lib/identity/provider-accounts"; -import { - validateManualAccount, isManualAccountValid, type ManualAccountDraft, + validateManualAccount, } from "@/lib/identity/manual-account"; +import { type ProviderAccount, projectProviderAccounts } from "@/lib/identity/provider-accounts"; import { planProviderEntry } from "@/lib/identity/provider-entry"; +import { + addManualProviderAccount, + getPassports, + getView, + markEmailVerified, + subscribe, +} from "@/lib/identity/wallet"; import { brokeredHandoffAvailable } from "@/lib/shell/brokered-bridge"; import { useShell } from "@/lib/shell/context"; import { copyWithAutoClear } from "@/lib/vault/clipboard"; @@ -70,9 +67,7 @@ export function ProviderAccounts() { {accounts.length} - - logins for your other pods - + logins for your other pods {open && ( @@ -86,8 +81,8 @@ export function ProviderAccounts() { ))} {accounts.length === 0 && (

- No saved logins yet. Add one for a provider you registered yourself - (e.g. Inrupt PodSpaces) to keep its login here. + No saved logins yet. Add one for a provider you registered yourself (e.g. Inrupt + PodSpaces) to keep its login here.

)} @@ -147,14 +142,11 @@ function AddLoginForm() { }; return ( -
+

Add a provider login

- For a pod you registered yourself. The password is saved to your Vault — - the shell never signs you in automatically with it. + For a pod you registered yourself. The password is saved to your Vault — the shell never + signs you in automatically with it.

@@ -298,8 +290,8 @@ function AccountRow({ account }: { account: ProviderAccount }) { {account.pending && (
- Confirm this email at the provider, then mark it verified to re-enable - automatic sign-in. + Confirm this email at the provider, then mark it verified to re-enable automatic + sign-in.
)} - {verifyError && ( -

{verifyError}

- )} + {verifyError &&

{verifyError}

} diff --git a/src/apps/vault/index.tsx b/src/apps/vault/index.tsx index b658130..4d7898f 100644 --- a/src/apps/vault/index.tsx +++ b/src/apps/vault/index.tsx @@ -16,32 +16,42 @@ * and are never written to the pod, localStorage, or any log. */ +import { + Button, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + Input, + Label, +} from "@mind-studio/ui"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { Button, Input, Label, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@mind-studio/ui"; +import { PrototypeWarning } from "@/components/PrototypeWarning"; +import { type AsyncCryptoCore, getPlatform } from "@/lib/platform"; import { useShell } from "@/lib/shell/context"; import { appZone } from "@/lib/shell/types"; -import { getPlatform, type AsyncCryptoCore } from "@/lib/platform"; +import { useAutoLock } from "@/lib/vault/autolock"; +import { cancelAutoClear } from "@/lib/vault/clipboard"; import type { SessionHandle } from "@/lib/vault/crypto-contract"; import { - loadVault, + changeMasterPassword, createVaultOnPod, - saveItem, - loadItemSecret, deleteItem, - changeMasterPassword, - newItemId, type LoadedVault, + loadItemSecret, + loadVault, + newItemId, + saveItem, + type VaultItemKind, type VaultItemMeta, type VaultItemSecret, - type VaultItemKind, } from "@/lib/vault/model"; -import { useAutoLock } from "@/lib/vault/autolock"; -import { cancelAutoClear } from "@/lib/vault/clipboard"; -import { ItemEditor, type EditorDraft } from "./ItemEditor"; import { ItemDetail } from "./ItemDetail"; -import { ProviderAccounts } from "./ProviderAccounts"; +import { type EditorDraft, ItemEditor } from "./ItemEditor"; import { PasswordGenerator } from "./PasswordGenerator"; -import { PrototypeWarning } from "@/components/PrototypeWarning"; +import { ProviderAccounts } from "./ProviderAccounts"; type Phase = "loading" | "setup" | "locked" | "unlocked"; @@ -49,7 +59,7 @@ export default function VaultApp() { const { workspacePod, project, ready } = useShell(); const zone = useMemo( () => (workspacePod ? appZone(workspacePod, "vault", project) : null), - [workspacePod, project] + [workspacePod, project], ); const [core, setCore] = useState(null); @@ -113,7 +123,12 @@ export default function VaultApp() { const v = await createVaultOnPod(core, zone, workspacePod, pw); setVault(v); // Unlock immediately with the just-set password. - const h = await core.unlock(pw, v.bootstrap.salt_b64, v.bootstrap.kdf, v.bootstrap.wrapped_data_key_b64); + const h = await core.unlock( + pw, + v.bootstrap.salt_b64, + v.bootstrap.kdf, + v.bootstrap.wrapped_data_key_b64, + ); setHandle(h); setPhase("unlocked"); }} @@ -129,7 +144,7 @@ export default function VaultApp() { pw, vault.bootstrap.salt_b64, vault.bootstrap.kdf, - vault.bootstrap.wrapped_data_key_b64 + vault.bootstrap.wrapped_data_key_b64, ); setHandle(h); setPhase("unlocked"); @@ -261,8 +276,8 @@ function UnlockScreen({ onUnlock }: { onUnlock: (pw: string) => Promise })
🔒

Unlock your vault

- The vault has its own master password — set when it was created, separate from the - one that unlocks your identity on this device. + The vault has its own master password — set when it was created, separate from the one + that unlocks your identity on this device.

@@ -320,7 +335,7 @@ function UnlockedVault({ (m) => m.title.toLowerCase().includes(q) || (m.url ?? "").toLowerCase().includes(q) || - (m.username ?? "").toLowerCase().includes(q) + (m.username ?? "").toLowerCase().includes(q), ); }, [vault.index, query]); @@ -335,7 +350,9 @@ function UnlockedVault({ setSecretError(null); loadItemSecret(core, zone, handle, selected) .then((s) => !cancelled && setSecret(s)) - .catch((e) => !cancelled && setSecretError(e instanceof Error ? e.message : "decrypt failed")); + .catch( + (e) => !cancelled && setSecretError(e instanceof Error ? e.message : "decrypt failed"), + ); return () => { cancelled = true; }; @@ -437,9 +454,7 @@ function UnlockedVault({ {/* detail — hidden on mobile until an item is picked; always shown ≥md. */}
{selected && (
diff --git a/src/components/WalletOnboarding.tsx b/src/components/WalletOnboarding.tsx index 4c5b6b0..7976c52 100644 --- a/src/components/WalletOnboarding.tsx +++ b/src/components/WalletOnboarding.tsx @@ -27,25 +27,25 @@ * passport key is sealed in the encrypted registry (AGENTS.md rules #1, #5). */ -import { useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; import { Button, Input, Label } from "@mind-studio/ui"; -import { rememberIssuer, storedIssuer } from "@/lib/solid/session"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { enterPassport, loginWithDidSession } from "@/lib/identity/passport-login"; +import { provisionPassport } from "@/lib/identity/provision"; +import { LAST_ACTIVE_PASSPORT_KEY, type Passport } from "@/lib/identity/types"; import { - getView, + addPassport, + createWallet, getDid, getPassports, - createWallet, - unlockWallet, - addPassport, + getView, newPassportId, + unlockWallet, sign as walletSign, } from "@/lib/identity/wallet"; -import { provisionPassport } from "@/lib/identity/provision"; -import { writeProfileName } from "@/lib/solid/profile"; -import { enterPassport, loginWithDidSession } from "@/lib/identity/passport-login"; import { serverSupportsDid } from "@/lib/solid/did-account"; -import { LAST_ACTIVE_PASSPORT_KEY, type Passport } from "@/lib/identity/types"; +import { writeProfileName } from "@/lib/solid/profile"; +import { rememberIssuer, storedIssuer } from "@/lib/solid/session"; type WalletStatus = "unknown" | "none" | "locked" | "unlocked"; @@ -91,7 +91,7 @@ export default function WalletOnboarding() { await unlockWallet(pw); } catch { setError( - "This device already holds an identity wallet, and that master password doesn't match it. Unlock your existing identity instead, or sign in with a different account below." + "This device already holds an identity wallet, and that master password doesn't match it. Unlock your existing identity instead, or sign in with a different account below.", ); setBusy(""); return; @@ -171,9 +171,7 @@ export default function WalletOnboarding() { // its session: client-credentials (re-mint a token) or DID (re-sign a // challenge — solid-server-rs). Manual/password logins can't. const lastId = - typeof window !== "undefined" - ? localStorage.getItem(LAST_ACTIVE_PASSPORT_KEY) - : null; + typeof window !== "undefined" ? localStorage.getItem(LAST_ACTIVE_PASSPORT_KEY) : null; const canResume = (p: Passport) => p.creds?.kind === "client-credentials" || p.creds?.kind === "did"; const resumable = @@ -209,8 +207,8 @@ export default function WalletOnboarding() { Welcome back

Unlock your identity

- Your master key lives on this device. Enter your master password to - continue into your pod — no server password needed. + Your master key lives on this device. Enter your master password to continue into your pod + — no server password needed.

{/* A real so Enter submits and password managers can fill. */} New to Mind

Create a new identity

- We generate your keys on this device and create your first pod — with a - strong password the shell stores for you (you never type or see one). + We generate your keys on this device and create your first pod — with a strong password the + shell stores for you (you never type or see one).

{/* A real so Enter submits and password managers can fill. */} - This device already holds an identity wallet (one per device for - now). Your master password must match it — this form finishes its - setup or resumes it, rather than starting over. + This device already holds an identity wallet (one per device for now). Your master + password must match it — this form finishes its setup or resumes it, rather than + starting over.

)} {error &&

{error}

} @@ -313,9 +311,8 @@ export default function WalletOnboarding() { {busy === "create" ? "Creating your identity & pod…" : "Create my identity"}

- Your master password is the one thing to remember — it unlocks this - identity and every pod the shell creates for you. It never leaves your - device. + Your master password is the one thing to remember — it unlocks this identity and every pod + the shell creates for you. It never leaves your device.

{status === "locked" && (
diff --git a/src/lib/identity/binding.ts b/src/lib/identity/binding.ts index d5492c3..c297905 100644 --- a/src/lib/identity/binding.ts +++ b/src/lib/identity/binding.ts @@ -20,8 +20,8 @@ * guaranteed byte-identical. */ -import { ensureContainerChain, readFileText, writeFileText } from "@/lib/solid/pod-fs"; import { getPlatform } from "@/lib/platform"; +import { ensureContainerChain, readFileText, writeFileText } from "@/lib/solid/pod-fs"; import { sign as walletSign } from "./wallet"; const MIND_NS = "https://mind.dev/ns/v1#"; @@ -161,7 +161,7 @@ export async function readBinding(podRoot: string): Promise { */ export async function verifyBindingDoc( doc: BindingDoc, - opts?: { podRoot?: string; expectedController?: string } + opts?: { podRoot?: string; expectedController?: string }, ): Promise { const core = await (await getPlatform()).crypto.getCore(); let ok: boolean; @@ -191,7 +191,7 @@ export async function verifyBindingDoc( /** Convenience: read + verify in one call. */ export async function readAndVerify( podRoot: string, - expectedController?: string + expectedController?: string, ): Promise<{ doc: BindingDoc | null; result: VerifyResult }> { const doc = await readBinding(podRoot); if (!doc) return { doc: null, result: { ok: false, reason: "no binding found" } }; diff --git a/src/lib/identity/manual-account.ts b/src/lib/identity/manual-account.ts index ae100e0..9d304dc 100644 --- a/src/lib/identity/manual-account.ts +++ b/src/lib/identity/manual-account.ts @@ -119,7 +119,7 @@ export function isManualAccountValid(draft: ManualAccountDraft): boolean { */ export function buildManualPassport( draft: ManualAccountDraft, - ctx: { id: string; did: string; createdAt: string } + ctx: { id: string; did: string; createdAt: string }, ): Passport { const server = normalizeServer(draft.server); if (!server) throw new Error("Invalid provider address."); diff --git a/src/lib/identity/passport-login.ts b/src/lib/identity/passport-login.ts index 0c54078..7646c14 100644 --- a/src/lib/identity/passport-login.ts +++ b/src/lib/identity/passport-login.ts @@ -23,21 +23,18 @@ */ import { + buildAuthenticatedFetch, createDpopHeader, generateDpopKeyPair, - buildAuthenticatedFetch, type KeyPair, } from "@inrupt/solid-client-authn-core"; import { ensureSeeded } from "@mind-studio/core/apps"; import { getPlatform } from "@/lib/platform"; -import { - setActivePassportSession, - clearActivePassportSession, -} from "@/lib/solid/passport-session"; -import { loginWithDid } from "@/lib/solid/did-account"; import { podRootFromWebId } from "@/lib/solid/account-login"; -import { sign as walletSign, getDid } from "./wallet"; +import { loginWithDid } from "@/lib/solid/did-account"; +import { clearActivePassportSession, setActivePassportSession } from "@/lib/solid/passport-session"; import type { Passport } from "./types"; +import { getDid, sign as walletSign } from "./wallet"; function ensureSlash(u: string): string { return u.endsWith("/") ? u : u + "/"; @@ -63,11 +60,7 @@ interface MintedToken { } /** Run the client_credentials grant once and return a fresh DPoP-bound token. */ -async function mintToken( - tokenUrl: string, - id: string, - secret: string -): Promise { +async function mintToken(tokenUrl: string, id: string, secret: string): Promise { const dpopKey = await generateDpopKeyPair(); const basic = btoa(`${encodeURIComponent(id)}:${encodeURIComponent(secret)}`); const res = await fetch(tokenUrl, { @@ -97,7 +90,7 @@ async function mintToken( async function makePassportFetch( server: string, id: string, - secret: string + secret: string, ): Promise { const tokenUrl = await tokenEndpoint(server); let current = await mintToken(tokenUrl, id, secret); @@ -142,9 +135,7 @@ export async function loginWithClientCredentials(opts: { }): Promise { const platform = await getPlatform(); if (platform.kind !== "web") { - throw new Error( - "Headless passport sign-in is available on the web build for now." - ); + throw new Error("Headless passport sign-in is available on the web build for now."); } const fetchFn = await makePassportFetch(opts.server, opts.id, opts.secret); setActivePassportSession({ @@ -169,7 +160,7 @@ async function makeDidSessionFetch( server: string, did: string, sign: (payload: string) => Promise, - initialToken: string + initialToken: string, ): Promise { let token = initialToken; diff --git a/src/lib/identity/provider-accounts.ts b/src/lib/identity/provider-accounts.ts index d3d7004..9c7b1e8 100644 --- a/src/lib/identity/provider-accounts.ts +++ b/src/lib/identity/provider-accounts.ts @@ -20,8 +20,8 @@ * import — so it stays unit-testable under `tsx` and carries no runtime weight. */ +import { type VerificationState, verificationState } from "./email"; import type { Passport } from "./types"; -import { verificationState, type VerificationState } from "./email"; /** A viewable provider login, projected from a sealed `password`-kind passport. */ export interface ProviderAccount { diff --git a/src/lib/identity/provider-entry.ts b/src/lib/identity/provider-entry.ts index fc33ce2..13a6015 100644 --- a/src/lib/identity/provider-entry.ts +++ b/src/lib/identity/provider-entry.ts @@ -64,7 +64,7 @@ function hostOf(url: string): string { /** Find the stored login for a provider, matched by host (scheme/path-agnostic). */ export function matchAccountForServer( accounts: ProviderAccount[], - server: string + server: string, ): ProviderAccount | undefined { const h = hostOf(server); if (!h) return undefined; diff --git a/src/lib/identity/provision.ts b/src/lib/identity/provision.ts index 6f206e7..1f3bfea 100644 --- a/src/lib/identity/provision.ts +++ b/src/lib/identity/provision.ts @@ -25,10 +25,10 @@ * creds, into the sealed registry) leave this module. */ -import { createPassportAccount } from "@/lib/solid/account"; import { getPlatform } from "@/lib/platform"; -import { newPassportId, sign as walletSign } from "./wallet"; +import { createPassportAccount } from "@/lib/solid/account"; import type { Passport } from "./types"; +import { newPassportId, sign as walletSign } from "./wallet"; /** Origin of a server URL, trailing-slash-free (e.g. "http://localhost:3101"). */ function origin(server: string): string { diff --git a/src/lib/identity/types.ts b/src/lib/identity/types.ts index dd3e752..b93acae 100644 --- a/src/lib/identity/types.ts +++ b/src/lib/identity/types.ts @@ -14,11 +14,7 @@ * blob only — never written to a pod or localStorage in the clear). */ -import type { - IdentityKeystore, - VaultBootstrap, - SealedItem, -} from "@/lib/vault/crypto-contract"; +import type { IdentityKeystore, SealedItem, VaultBootstrap } from "@/lib/vault/crypto-contract"; /** The portable master identity (PRD-DID §2.4 — one master DID for v0). */ export interface MasterIdentity { diff --git a/src/lib/identity/wallet.ts b/src/lib/identity/wallet.ts index e84a26a..9a1d3b3 100644 --- a/src/lib/identity/wallet.ts +++ b/src/lib/identity/wallet.ts @@ -26,14 +26,14 @@ * (AGENTS.md rule #5). OK to log: the public did, server origin, event type. */ -import { getPlatform, type AsyncCryptoCore } from "@/lib/platform"; +import { type AsyncCryptoCore, getPlatform } from "@/lib/platform"; import type { KdfParams } from "@/lib/vault/crypto-contract"; import { buildManualPassport, type ManualAccountDraft } from "./manual-account"; import { - REGISTRY_ITEM_ID, - WALLET_STORAGE_KEY, type Passport, + REGISTRY_ITEM_ID, type StoredWallet, + WALLET_STORAGE_KEY, type WalletView, } from "./types"; @@ -140,7 +140,7 @@ export async function createWallet(masterPassword: string): Promise masterPassword, bootstrap.salt_b64, bootstrap.kdf, - bootstrap.wrapped_data_key_b64 + bootstrap.wrapped_data_key_b64, ); const w: StoredWallet = { @@ -173,13 +173,13 @@ export async function unlockWallet(masterPassword: string): Promise masterPassword, w.identity.salt_b64, w.identity.kdf, - w.identity.wrapped_seed_b64 + w.identity.wrapped_seed_b64, ); const vaultHandle = await c.unlock( masterPassword, w.registry.bootstrap.salt_b64, w.registry.bootstrap.kdf, - w.registry.bootstrap.wrapped_data_key_b64 + w.registry.bootstrap.wrapped_data_key_b64, ); let passports: Passport[] = []; if (w.registry.sealed) { @@ -187,7 +187,7 @@ export async function unlockWallet(masterPassword: string): Promise vaultHandle, REGISTRY_ITEM_ID, w.registry.version, - w.registry.sealed + w.registry.sealed, ); passports = JSON.parse(json) as Passport[]; } @@ -235,14 +235,9 @@ export async function addPassport(passport: Passport): Promise { } /** Patch a passport (by id) in the encrypted registry. */ -export async function updatePassport( - id: string, - patch: Partial -): Promise { +export async function updatePassport(id: string, patch: Partial): Promise { if (!unlocked) throw new Error("Wallet is locked."); - unlocked.passports = unlocked.passports.map((p) => - p.id === id ? { ...p, ...patch } : p - ); + unlocked.passports = unlocked.passports.map((p) => (p.id === id ? { ...p, ...patch } : p)); await saveRegistry(); emit(); return getView(); @@ -258,7 +253,7 @@ export async function updatePassport( export async function markEmailVerified(id: string): Promise { if (!unlocked) throw new Error("Wallet is locked."); unlocked.passports = unlocked.passports.map((p) => - p.id === id && p.creds ? { ...p, creds: { ...p.creds, emailVerified: true } } : p + p.id === id && p.creds ? { ...p, creds: { ...p.creds, emailVerified: true } } : p, ); await saveRegistry(); emit(); @@ -273,9 +268,7 @@ export async function markEmailVerified(id: string): Promise { * validation live in {@link buildManualPassport}; this only supplies the id, did, * and timestamp and persists. Throws if the draft is invalid. */ -export async function addManualProviderAccount( - draft: ManualAccountDraft -): Promise { +export async function addManualProviderAccount(draft: ManualAccountDraft): Promise { if (!unlocked) throw new Error("Wallet is locked."); const passport = buildManualPassport(draft, { id: newPassportId(), diff --git a/src/lib/platform/detect.ts b/src/lib/platform/detect.ts index 220df6c..8bcfd78 100644 --- a/src/lib/platform/detect.ts +++ b/src/lib/platform/detect.ts @@ -25,8 +25,7 @@ declare global { export function isNative(): boolean { if (typeof window === "undefined") return false; return ( - typeof window.__TAURI_INTERNALS__ !== "undefined" || - typeof window.__TAURI__ !== "undefined" + typeof window.__TAURI_INTERNALS__ !== "undefined" || typeof window.__TAURI__ !== "undefined" ); } diff --git a/src/lib/platform/index.ts b/src/lib/platform/index.ts index a3f0899..66f54c6 100644 --- a/src/lib/platform/index.ts +++ b/src/lib/platform/index.ts @@ -24,19 +24,19 @@ import { isNative, platformKind } from "./detect"; import type { Platform, PlatformKind } from "./types"; import { webPlatform } from "./web"; +export { isNative, platformKind } from "./detect"; export type { + AsyncCryptoCore, + AutofillIndexEntry, Platform, - PlatformKind, PlatformAuth, - PlatformCrypto, - PlatformBiometric, PlatformAutofill, - PlatformStorage, + PlatformBiometric, + PlatformCrypto, + PlatformKind, PlatformPod, - AutofillIndexEntry, - AsyncCryptoCore, + PlatformStorage, } from "./types"; -export { isNative, platformKind } from "./detect"; let platformPromise: Promise | null = null; diff --git a/src/lib/platform/native.ts b/src/lib/platform/native.ts index 30ef22f..b385534 100644 --- a/src/lib/platform/native.ts +++ b/src/lib/platform/native.ts @@ -18,26 +18,26 @@ import type { ISessionInfo } from "@inrupt/solid-client-authn-browser"; import type { + CreatedIdentity, + HibpPrefix, + IdentityHandle, KdfParams, - VaultBootstrap, - SealedItem, PwGenOptions, - HibpPrefix, + SealedItem, SessionHandle, - CreatedIdentity, UnlockedIdentity, - IdentityHandle, + VaultBootstrap, } from "../vault/crypto-contract"; import type { + AsyncCryptoCore, + AutofillIndexEntry, Platform, PlatformAuth, PlatformAutofill, PlatformBiometric, PlatformCrypto, - PlatformStorage, PlatformPod, - AutofillIndexEntry, - AsyncCryptoCore, + PlatformStorage, } from "./types"; // Tauri command + event names — the single source of truth for the JS↔Rust wire. @@ -219,16 +219,10 @@ const auth: PlatformAuth = { * — identical to the wasm FFI, just async because it's a process hop. */ const nativeCore: AsyncCryptoCore = { - calibrateKdf: (targetMs: number) => - invokeCmd(CMD.calibrateKdf, { targetMs }), + calibrateKdf: (targetMs: number) => invokeCmd(CMD.calibrateKdf, { targetMs }), createVault: (masterPassword: string, params: KdfParams) => invokeCmd(CMD.createVault, { masterPassword, params }), - unlock: ( - masterPassword: string, - saltB64: string, - params: KdfParams, - wrappedDataKeyB64: string - ) => + unlock: (masterPassword: string, saltB64: string, params: KdfParams, wrappedDataKeyB64: string) => invokeCmd(CMD.unlock, { masterPassword, saltB64, @@ -236,42 +230,27 @@ const nativeCore: AsyncCryptoCore = { wrappedDataKeyB64, }), lock: (handle: SessionHandle) => invokeCmd(CMD.lock, { handle }), - encryptItem: ( - handle: SessionHandle, - itemId: string, - version: number, - plaintextJson: string - ) => + encryptItem: (handle: SessionHandle, itemId: string, version: number, plaintextJson: string) => invokeCmd(CMD.encryptItem, { handle, itemId, version, plaintextJson, }), - decryptItem: ( - handle: SessionHandle, - itemId: string, - version: number, - sealed: SealedItem - ) => invokeCmd(CMD.decryptItem, { handle, itemId, version, sealed }), - changePassword: ( - handle: SessionHandle, - newPassword: string, - params: KdfParams - ) => + decryptItem: (handle: SessionHandle, itemId: string, version: number, sealed: SealedItem) => + invokeCmd(CMD.decryptItem, { handle, itemId, version, sealed }), + changePassword: (handle: SessionHandle, newPassword: string, params: KdfParams) => invokeCmd(CMD.changePassword, { handle, newPassword, params, }), - generatePassword: (opts: PwGenOptions) => - invokeCmd(CMD.generatePassword, { opts }), + generatePassword: (opts: PwGenOptions) => invokeCmd(CMD.generatePassword, { opts }), generatePassphrase: (words: number, separator: string) => invokeCmd(CMD.generatePassphrase, { words, separator }), totpAt: (secretB32: string, unixSeconds: number, period: number, digits: number) => invokeCmd(CMD.totpAt, { secretB32, unixSeconds, period, digits }), - hibpPrefix: (password: string) => - invokeCmd(CMD.hibpPrefix, { password }), + hibpPrefix: (password: string) => invokeCmd(CMD.hibpPrefix, { password }), // ---- identity / DID layer (PRD-DID §5.9) ---- identityCreate: (masterPassword: string, params: KdfParams) => @@ -280,7 +259,7 @@ const nativeCore: AsyncCryptoCore = { masterPassword: string, saltB64: string, params: KdfParams, - wrappedSeedB64: string + wrappedSeedB64: string, ) => invokeCmd(CMD.identityUnlock, { masterPassword, @@ -290,14 +269,12 @@ const nativeCore: AsyncCryptoCore = { }), identityFromSeed: (seedB64: string) => invokeCmd(CMD.identityFromSeed, { seedB64 }), - masterDid: (handle: IdentityHandle) => - invokeCmd(CMD.masterDid, { handle }), + masterDid: (handle: IdentityHandle) => invokeCmd(CMD.masterDid, { handle }), signDetached: (handle: IdentityHandle, payload: string) => invokeCmd(CMD.signDetached, { handle, payload }), verifyBinding: (did: string, payload: string, signatureB64: string) => invokeCmd(CMD.verifyBinding, { did, payload, signatureB64 }), - identityLock: (handle: IdentityHandle) => - invokeCmd(CMD.identityLock, { handle }), + identityLock: (handle: IdentityHandle) => invokeCmd(CMD.identityLock, { handle }), }; const crypto: PlatformCrypto = { @@ -420,14 +397,15 @@ async function encodeBodyB64(body: BodyInit | null | undefined): Promise { - const request = - input instanceof Request ? input : new Request(input as RequestInfo | URL, init); + const request = input instanceof Request ? input : new Request(input as RequestInfo | URL, init); const url = request.url; const method = (init?.method ?? request.method ?? "GET").toUpperCase(); // Headers: prefer the Request's merged headers; fall back to init. const headers = headerPairs( - input instanceof Request && !init?.headers ? request.headers : init?.headers ?? request.headers + input instanceof Request && !init?.headers + ? request.headers + : (init?.headers ?? request.headers), ); // Body (base64): only bodyless-exempt verbs carry one. init first, else Request. diff --git a/src/lib/platform/web.ts b/src/lib/platform/web.ts index a351b35..af86db2 100644 --- a/src/lib/platform/web.ts +++ b/src/lib/platform/web.ts @@ -12,31 +12,25 @@ * unsupported so the UI falls back to the master password / hides OS autofill. */ +import { login as browserLogin, type ISessionInfo } from "@inrupt/solid-client-authn-browser"; import { - login as browserLogin, - type ISessionInfo, -} from "@inrupt/solid-client-authn-browser"; -import { - ensureSession as solidEnsureSession, completeLoginRedirect, rememberReturnToDefault, + ensureSession as solidEnsureSession, } from "../solid/auth"; -import { session, rememberIssuer } from "../solid/session"; -import { - getActivePassportSession, - clearActivePassportSession, -} from "../solid/passport-session"; +import { clearActivePassportSession, getActivePassportSession } from "../solid/passport-session"; +import { rememberIssuer, session } from "../solid/session"; import { getCryptoCore } from "../vault/core"; import type { + AsyncCryptoCore, + AutofillIndexEntry, Platform, PlatformAuth, PlatformAutofill, PlatformBiometric, PlatformCrypto, - PlatformStorage, PlatformPod, - AutofillIndexEntry, - AsyncCryptoCore, + PlatformStorage, } from "./types"; const auth: PlatformAuth = { diff --git a/src/lib/shell/bridge-client.ts b/src/lib/shell/bridge-client.ts index e9f2467..0dd93bc 100644 --- a/src/lib/shell/bridge-client.ts +++ b/src/lib/shell/bridge-client.ts @@ -14,10 +14,10 @@ */ import { - PROTOCOL_VERSION, + type BridgeEntry, type BridgeIdentity, type BridgeTheme, - type BridgeEntry, + PROTOCOL_VERSION, } from "./bridge-protocol"; export interface WidgetSession { @@ -59,7 +59,7 @@ export interface WidgetClient { */ fetch( url: string, - init?: { method?: string; headers?: Record } + init?: { method?: string; headers?: Record }, ): Promise; /** * Write text to a resource (whole-file PUT), relative to the scope ceiling. @@ -161,8 +161,7 @@ export function createWidgetClient(): WidgetClient { readdir: (path) => request("mind:readdir", { path }), read: (path) => request("mind:read", { path }), fetch: (url, init) => request("mind:fetch", { url, init }), - write: (path, body, contentType) => - request("mind:write", { path, body, contentType }), + write: (path, body, contentType) => request("mind:write", { path, body, contentType }), resize(height) { post({ t: "mind:resize", v: PROTOCOL_VERSION, height: Math.round(height) }); }, diff --git a/src/lib/shell/bridge.ts b/src/lib/shell/bridge.ts index 1216717..5ba0b83 100644 --- a/src/lib/shell/bridge.ts +++ b/src/lib/shell/bridge.ts @@ -1,21 +1,13 @@ +import { getActivePassportSession, subscribePassportSession } from "@/lib/solid/passport-session"; +import { ensureContainerChain, readdir, readFileText, writeFileText } from "@/lib/solid/pod-fs"; import { - readdir, - readFileText, - writeFileText, - ensureContainerChain, -} from "@/lib/solid/pod-fs"; -import { - subscribePassportSession, - getActivePassportSession, -} from "@/lib/solid/passport-session"; -import { - PROTOCOL_VERSION, - isBridgeMessage, - type ChildMessage, - type ParentMessage, type BridgeIdentity, type BridgeTheme, + type ChildMessage, type FetchMsg, + isBridgeMessage, + type ParentMessage, + PROTOCOL_VERSION, } from "./bridge-protocol"; import type { HostedApp } from "./types"; @@ -252,7 +244,7 @@ export function createBridge(opts: BridgeOptions): Bridge { function logLine(t: string, ref: string, status: number | string, started: number) { // Never log bodies/secrets — only event type, resource, status, latency. console.debug( - `[bridge] ${t} ${ref} → ${status} (${Math.round(performance.now() - started)}ms) webId=${identity.webId}` + `[bridge] ${t} ${ref} → ${status} (${Math.round(performance.now() - started)}ms) webId=${identity.webId}`, ); } @@ -273,9 +265,7 @@ export function createBridge(opts: BridgeOptions): Bridge { // tripped on download) is base64-framed so the bytes survive postMessage. const buf = await res.arrayBuffer(); const textual = isTextualContentType(res.headers.get("content-type") ?? ""); - const body = textual - ? new TextDecoder().decode(buf) - : bytesToBase64(new Uint8Array(buf)); + const body = textual ? new TextDecoder().decode(buf) : bytesToBase64(new Uint8Array(buf)); const encoding = textual ? "utf8" : "base64"; logLine("fetch", url, res.status, started); post({ diff --git a/src/lib/shell/brokered-bridge.ts b/src/lib/shell/brokered-bridge.ts index a537b68..df617ee 100644 --- a/src/lib/shell/brokered-bridge.ts +++ b/src/lib/shell/brokered-bridge.ts @@ -20,18 +20,12 @@ */ import type { HostedApp } from "./types"; -export function brokeredHandoffAvailable( - server: string | undefined, - apps: HostedApp[] -): boolean { +export function brokeredHandoffAvailable(server: string | undefined, apps: HostedApp[]): boolean { const target = hostOf(server); if (!target) return false; return apps.some( (a) => - a.enabled && - a.embed === "iframe" && - a.trust === "first-party" && - hostOf(a.url) === target + a.enabled && a.embed === "iframe" && a.trust === "first-party" && hostOf(a.url) === target, ); } diff --git a/src/lib/shell/catalog.ts b/src/lib/shell/catalog.ts index 3d01e11..f2af96d 100644 --- a/src/lib/shell/catalog.ts +++ b/src/lib/shell/catalog.ts @@ -1,15 +1,15 @@ "use client"; +import type { SolidDataset } from "@inrupt/solid-client"; import { + getInteger, getSolidDataset, - getThingAll, getStringNoLocale, - getInteger, + getThingAll, getUrl, } from "@inrupt/solid-client"; import { resourceExistsByListing } from "@/lib/solid/pod-fs"; -import type { HostedApp, AppEmbed, AppTrust, WidgetDecl, WidgetSize } from "./types"; -import type { SolidDataset } from "@inrupt/solid-client"; +import type { AppEmbed, AppTrust, HostedApp, WidgetDecl, WidgetSize } from "./types"; /** * Read the pod-owned app catalog at `{podRoot}home/apps.ttl` (PRD-APPS §4) into @@ -53,9 +53,7 @@ function asEmbed(v: string | null): AppEmbed | undefined { } function asTrust(v: string | null): AppTrust | undefined { - return v === "first-party" || v === "community" || v === "untrusted" - ? v - : undefined; + return v === "first-party" || v === "community" || v === "untrusted" ? v : undefined; } function asSize(v: string | null): WidgetSize | undefined { @@ -74,8 +72,7 @@ function collectWidgets(ds: SolidDataset): Map { if (!types.includes(MIND_WIDGET)) continue; const appKey = getStringNoLocale(thing, MIND_APP_REF); const id = getStringNoLocale(thing, MIND_ID); - const url = - getStringNoLocale(thing, MIND_URL) ?? getUrl(thing, MIND_URL) ?? undefined; + const url = getStringNoLocale(thing, MIND_URL) ?? getUrl(thing, MIND_URL) ?? undefined; if (!appKey || !id || !url) continue; const decl: WidgetDecl = { id, @@ -99,10 +96,7 @@ function collectWidgets(ds: SolidDataset): Map { * be read. Gated on a container listing so a fresh pod doesn't 404 (and log). * Never throws — the shell falls back to its built-ins on any failure. */ -export async function readCatalog( - podRoot: string, - fetchFn: typeof fetch -): Promise { +export async function readCatalog(podRoot: string, fetchFn: typeof fetch): Promise { const base = ensureSlash(podRoot); const docUrl = `${base}home/apps.ttl`; try { @@ -117,11 +111,9 @@ export async function readCatalog( const types = thing.predicates[RDF_TYPE]?.namedNodes ?? []; if (!types.includes(MIND_APP)) continue; const hash = thing.url.indexOf("#"); - const key = - hash >= 0 ? thing.url.slice(hash + 1) : thing.url.slice(base.length); + const key = hash >= 0 ? thing.url.slice(hash + 1) : thing.url.slice(base.length); if (!key) continue; - const url = - getStringNoLocale(thing, MIND_URL) ?? getUrl(thing, MIND_URL) ?? undefined; + const url = getStringNoLocale(thing, MIND_URL) ?? getUrl(thing, MIND_URL) ?? undefined; rows.push({ app: { key, diff --git a/src/lib/shell/context.tsx b/src/lib/shell/context.tsx index a4eefa1..ae61232 100644 --- a/src/lib/shell/context.tsx +++ b/src/lib/shell/context.tsx @@ -1,47 +1,36 @@ "use client"; -import { - createContext, - useContext, - useEffect, - useState, - useCallback, - useRef, -} from "react"; import { useRouter } from "next/navigation"; +import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react"; +import { + addPassport, + getDid as getWalletDid, + getView as getWalletView, + newPassportId, + sign as walletSign, +} from "@/lib/identity/wallet"; import { getPlatform } from "@/lib/platform"; -import { readProfile, readPodRoot } from "@/lib/solid/profile"; -import { readdir, readFileText } from "@/lib/solid/pod-fs"; +import { provisionWorkspaceAccount } from "@/lib/solid/account"; +import { exists, readdir, readFileText } from "@/lib/solid/pod-fs"; +import { readPodRoot, readProfile } from "@/lib/solid/profile"; +import { trySilentResume } from "@/lib/solid/resume"; import { - listWorkspaceRefs, addWorkspaceRef, ensureHomeRef, ensureSlash, + listWorkspaceRefs, } from "@/lib/solid/workspaces"; -import { exists } from "@/lib/solid/pod-fs"; -import { provisionWorkspaceAccount } from "@/lib/solid/account"; -import { - getView as getWalletView, - getDid as getWalletDid, - sign as walletSign, - addPassport, - newPassportId, -} from "@/lib/identity/wallet"; -import { - listAccounts, - rememberAccount, -} from "./accounts"; +import { listAccounts, rememberAccount } from "./accounts"; import { readCatalog } from "./catalog"; -import { trySilentResume } from "@/lib/solid/resume"; import type { + AccountIdentity, + HostedApp, + Project, ShellContextValue, + WidgetDecl, Workspace, WorkspaceRef, WorkspaceRole, - Project, - HostedApp, - WidgetDecl, - AccountIdentity, } from "./types"; /** @@ -360,8 +349,7 @@ export function ShellProvider({ children }: { children: React.ReactNode }) { // authoritative when present; otherwise the cached ref name, then the slug. const readWorkspaceName = useCallback( async (podRoot: string, fallback?: string): Promise => { - const slug = - podRoot.replace(/\/$/, "").split("/").filter(Boolean).pop() ?? "Workspace"; + const slug = podRoot.replace(/\/$/, "").split("/").filter(Boolean).pop() ?? "Workspace"; try { const entries = await readdir(podRoot); if (entries.some((e) => e.name === "workspace.ttl")) { @@ -374,7 +362,7 @@ export function ShellProvider({ children }: { children: React.ReactNode }) { } return fallback ?? slug; }, - [] + [], ); // Resolve the rail: read the per-identity Workspace index off the home pod, @@ -412,70 +400,73 @@ export function ShellProvider({ children }: { children: React.ReactNode }) { podRoot: r.podRoot, name: await readWorkspaceName(r.podRoot, r.name), role: r.role, - })) + })), ); setWorkspaces(resolved); }, - [readWorkspaceName] + [readWorkspaceName], ); // Load the projects + apps for the currently active workspace pod. - const loadActiveWorkspace = useCallback(async (podRoot: string) => { - setWorkspacePod(podRoot); - - // Projects under {podRoot}projects/. Gate on the pod-root listing so a pod - // without a projects/ container doesn't 404 (and log) on every switch. - try { - const rootEntries = await readdir(podRoot); - const hasProjects = rootEntries.some( - (x) => x.kind === "container" && x.name === "projects" - ); - const found: Project[] = []; - if (hasProjects) { - const entries = await readdir(`${podRoot}projects/`); - for (const e of entries.filter((x) => x.kind === "container")) { - const id = e.name; - let pname = id; - try { - const ptl = await readFileText(`${e.url}project.ttl`); - const pm = ptl.match(/dct:title\s+"([^"]+)"/); - if (pm) pname = pm[1]; - } catch {} - found.push({ id, url: e.url, name: pname }); + const loadActiveWorkspace = useCallback( + async (podRoot: string) => { + setWorkspacePod(podRoot); + + // Projects under {podRoot}projects/. Gate on the pod-root listing so a pod + // without a projects/ container doesn't 404 (and log) on every switch. + try { + const rootEntries = await readdir(podRoot); + const hasProjects = rootEntries.some( + (x) => x.kind === "container" && x.name === "projects", + ); + const found: Project[] = []; + if (hasProjects) { + const entries = await readdir(`${podRoot}projects/`); + for (const e of entries.filter((x) => x.kind === "container")) { + const id = e.name; + let pname = id; + try { + const ptl = await readFileText(`${e.url}project.ttl`); + const pm = ptl.match(/dct:title\s+"([^"]+)"/); + if (pm) pname = pm[1]; + } catch {} + found.push({ id, url: e.url, name: pname }); + } } + setProjects(found); + } catch { + setProjects([]); } - setProjects(found); - } catch { - setProjects([]); - } - // Apps: built-ins always (incl. Drive); PLUS pod-owned `embed:"iframe"` apps - // from the catalog (PRD-APPS §4) so the shell can HOST them in the app body, - // not just link out. Pure-link apps stay out of `apps` — the waffle still - // lists them. Non-fatal: a read failure keeps just the built-ins. - const builtins = builtinApps(); - let merged = builtins; - try { - const catalog = await readCatalog(podRoot, authedFetch); - const hostable = catalog.filter( - (a) => a.embed === "iframe" && a.url && !builtins.some((b) => b.key === a.key) - ); - merged = [...builtins, ...hostable]; - } catch { - /* catalog unreadable — built-ins (incl. Drive) still stand */ - } - setApps(merged); - // First load only: open the preferred default app (Drive). It's a built-in - // now, so it's always present; the guard only matters if the default key is - // overridden to something absent — then we stay on the Vault fallback. - if (!defaultApplied.current) { - defaultApplied.current = true; - const target = merged.some((a) => a.key === DEFAULT_APP_KEY) - ? DEFAULT_APP_KEY - : FALLBACK_APP_KEY; - if (merged.some((a) => a.key === target)) setActiveAppKey(target); - } - }, [authedFetch]); + // Apps: built-ins always (incl. Drive); PLUS pod-owned `embed:"iframe"` apps + // from the catalog (PRD-APPS §4) so the shell can HOST them in the app body, + // not just link out. Pure-link apps stay out of `apps` — the waffle still + // lists them. Non-fatal: a read failure keeps just the built-ins. + const builtins = builtinApps(); + let merged = builtins; + try { + const catalog = await readCatalog(podRoot, authedFetch); + const hostable = catalog.filter( + (a) => a.embed === "iframe" && a.url && !builtins.some((b) => b.key === a.key), + ); + merged = [...builtins, ...hostable]; + } catch { + /* catalog unreadable — built-ins (incl. Drive) still stand */ + } + setApps(merged); + // First load only: open the preferred default app (Drive). It's a built-in + // now, so it's always present; the guard only matters if the default key is + // overridden to something absent — then we stay on the Vault fallback. + if (!defaultApplied.current) { + defaultApplied.current = true; + const target = merged.some((a) => a.key === DEFAULT_APP_KEY) + ? DEFAULT_APP_KEY + : FALLBACK_APP_KEY; + if (merged.some((a) => a.key === target)) setActiveAppKey(target); + } + }, + [authedFetch], + ); const refresh = useCallback(async () => { const platform = await getPlatform(); @@ -527,7 +518,7 @@ export function ShellProvider({ children }: { children: React.ReactNode }) { await loadActiveWorkspace(target); })(); }, - [resolveWorkspaces, loadActiveWorkspace] + [resolveWorkspaces, loadActiveWorkspace], ); // Join an existing pod as a Workspace (B3): probe access, persist a ref in the @@ -536,9 +527,7 @@ export function ShellProvider({ children }: { children: React.ReactNode }) { async (podRoot: string, opts?: { role?: WorkspaceRole; name?: string }) => { const home = homePod.current; if (!home) throw new Error("No home pod available to register against."); - const normalized = podRoot.trim().endsWith("/") - ? podRoot.trim() - : podRoot.trim() + "/"; + const normalized = podRoot.trim().endsWith("/") ? podRoot.trim() : podRoot.trim() + "/"; // Validate it's a real http(s) URL before touching the index. let url: URL; try { @@ -568,7 +557,7 @@ export function ShellProvider({ children }: { children: React.ReactNode }) { // switchWorkspace re-resolves the rail (now including the new ref) + activates it. switchWorkspace(normalized); }, - [switchWorkspace] + [switchWorkspace], ); // Provision a brand-new pod reusing this WebID (B4 / PRD-DID §5.7 hybrid), then @@ -579,7 +568,15 @@ export function ShellProvider({ children }: { children: React.ReactNode }) { // simply records didLinked:false. A master wallet is optional: with none, the // workspace still provisions (name-only), just without DID-link or sealed creds. const createWorkspace = useCallback( - async ({ name, server, email: byoEmail }: { name: string; server?: string; email?: string }) => { + async ({ + name, + server, + email: byoEmail, + }: { + name: string; + server?: string; + email?: string; + }) => { const home = homePod.current; if (!home || !webId) { throw new Error("You need to be signed in to create a workspace."); @@ -599,7 +596,10 @@ export function ShellProvider({ children }: { children: React.ReactNode }) { avoidAmbiguous: true, }); const slug = - title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "workspace"; + title + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") || "workspace"; const rand = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID().slice(0, 8) @@ -666,7 +666,7 @@ export function ShellProvider({ children }: { children: React.ReactNode }) { await addWorkspaceRef(home, { podRoot, role: "owner", name: title }); switchWorkspace(podRoot); }, - [webId, switchWorkspace] + [webId, switchWorkspace], ); const setProject = useCallback((p: Project | null) => setProjectState(p), []); diff --git a/src/lib/shell/home-layout.ts b/src/lib/shell/home-layout.ts index c67dfe6..852e133 100644 --- a/src/lib/shell/home-layout.ts +++ b/src/lib/shell/home-layout.ts @@ -16,13 +16,13 @@ */ import { - readFileText, - writeFileText, ensureContainerChain, + readFileText, resourceExistsByListing, + writeFileText, } from "@/lib/solid/pod-fs"; -import { shellZone } from "./types"; import type { HomeLayoutItem, WidgetSize } from "./types"; +import { shellZone } from "./types"; const MIND_NS = "https://mind.dev/ns/v1#"; @@ -92,9 +92,7 @@ function parseHomeTtl(ttl: string): HomeLayoutItem[] { * (so the caller can fall back to a default Home). Never throws — any read error * resolves to `null` and Home renders its default. */ -export async function readHomeLayout( - podRoot: string -): Promise { +export async function readHomeLayout(podRoot: string): Promise { try { const url = homeTtlUrl(podRoot); if (!(await resourceExistsByListing(url, podRoot))) return null; @@ -109,10 +107,7 @@ export async function readHomeLayout( * so a fresh pod stays console-clean. Re-numbers `order` from the array order so * the on-pod order is always canonical. */ -export async function writeHomeLayout( - podRoot: string, - items: HomeLayoutItem[] -): Promise { +export async function writeHomeLayout(podRoot: string, items: HomeLayoutItem[]): Promise { const normalized = items.map((it, i) => ({ ...it, order: i })); await ensureContainerChain(shellZone(podRoot), podRoot); await writeFileText(homeTtlUrl(podRoot), serializeHomeTtl(normalized), "text/turtle"); diff --git a/src/lib/shell/types.ts b/src/lib/shell/types.ts index 10569dc..0913f9a 100644 --- a/src/lib/shell/types.ts +++ b/src/lib/shell/types.ts @@ -207,11 +207,7 @@ export interface ShellContextValue { * The workspace-wide view is the union of these, computed under the requester's * own credentials (no-access projects simply don't resolve). */ -export function appZone( - podRoot: string, - appKey: string, - project?: Project | null -): string { +export function appZone(podRoot: string, appKey: string, project?: Project | null): string { const base = podRoot.endsWith("/") ? podRoot : podRoot + "/"; if (project) return `${base}projects/${project.id}/apps/${appKey}/`; return `${base}apps/${appKey}/`; diff --git a/src/lib/solid/account-login.ts b/src/lib/solid/account-login.ts index a48dc20..fe9128f 100644 --- a/src/lib/solid/account-login.ts +++ b/src/lib/solid/account-login.ts @@ -50,7 +50,7 @@ function acctPost(token: string) { export async function loginToAccount( email: string, password: string, - server?: string + server?: string, ): Promise { const root = serverRoot(server); const accountIndex = `${root}.account/`; @@ -111,7 +111,7 @@ export async function mintClientCredentials( token: string, webId: string, server?: string, - name = "mind-shell" + name = "mind-shell", ): Promise<{ id: string; secret: string }> { const root = serverRoot(server); const accountIndex = `${root}.account/`; diff --git a/src/lib/solid/account.ts b/src/lib/solid/account.ts index 2c5dc30..c39f2f8 100644 --- a/src/lib/solid/account.ts +++ b/src/lib/solid/account.ts @@ -1,7 +1,7 @@ "use client"; +import { type DidSigner, linkDidToAccount } from "./did-account"; import { storedIssuer } from "./session"; -import { linkDidToAccount, type DidSigner } from "./did-account"; /** * The CSS **account-session** plane (PRD-IDENTITY.md §4.1 mechanism B, used only @@ -146,7 +146,7 @@ export async function createPod(opts: { } if (!createRes.ok) throw new Error(`Pod creation failed (${createRes.status}).`); const created = await json<{ pod?: string; webId?: string }>(createRes).catch( - () => ({} as { pod?: string; webId?: string }) + () => ({}) as { pod?: string; webId?: string }, ); const podRoot = created.pod ?? `${root}${slug}/`; @@ -252,7 +252,7 @@ export async function createPassportAccount(opts: { throw new Error( createRes.status === 403 || createRes.status === 404 ? "This server doesn't allow creating new accounts." - : `Account creation failed (${createRes.status}).` + : `Account creation failed (${createRes.status}).`, ); } const { authorization: token } = await json<{ authorization?: string }>(createRes); @@ -279,7 +279,7 @@ export async function createPassportAccount(opts: { throw new Error( pwRes.status === 400 || pwRes.status === 409 ? "That account email is already taken — try again." - : `Setting the account password failed (${pwRes.status}).` + : `Setting the account password failed (${pwRes.status}).`, ); } @@ -399,7 +399,7 @@ export async function provisionWorkspaceAccount(opts: { throw new Error( createRes.status === 401 || createRes.status === 403 || createRes.status === 404 ? "This server doesn't allow creating new accounts." - : `Account creation failed (${createRes.status}).` + : `Account creation failed (${createRes.status}).`, ); } const { authorization: token } = await json<{ authorization?: string }>(createRes); @@ -425,7 +425,7 @@ export async function provisionWorkspaceAccount(opts: { throw new Error( pwRes.status === 400 || pwRes.status === 409 ? "Couldn't set up the workspace account — try again." - : `Setting the account password failed (${pwRes.status}).` + : `Setting the account password failed (${pwRes.status}).`, ); } diff --git a/src/lib/solid/auth.ts b/src/lib/solid/auth.ts index 8621f65..25f47d1 100644 --- a/src/lib/solid/auth.ts +++ b/src/lib/solid/auth.ts @@ -1,9 +1,6 @@ "use client"; -import { - handleIncomingRedirect, - type ISessionInfo, -} from "@inrupt/solid-client-authn-browser"; +import { handleIncomingRedirect, type ISessionInfo } from "@inrupt/solid-client-authn-browser"; import { session } from "./session"; const RETURN_TO_KEY = "mind-shell:return-to"; diff --git a/src/lib/solid/did-account.ts b/src/lib/solid/did-account.ts index 34a9074..0e86e65 100644 --- a/src/lib/solid/did-account.ts +++ b/src/lib/solid/did-account.ts @@ -47,7 +47,10 @@ function serverRoot(server?: string): string { return base.endsWith("/") ? base : base + "/"; } -async function readControls(accountIndex: string, headers: Record): Promise { +async function readControls( + accountIndex: string, + headers: Record, +): Promise { let res: Response; try { res = await fetch(accountIndex, { headers: { Accept: "application/json", ...headers } }); @@ -70,7 +73,10 @@ async function readControls(accountIndex: string, headers: Record): Promise { +async function readDidControls( + root: string, + headers: Record, +): Promise { try { const did = await readControls(`${root}.account/did/`, headers); if (did.challenge && did.login) return did; @@ -150,7 +156,9 @@ export async function loginWithDid(opts: { body: JSON.stringify({ did: opts.did, nonce: challenge.nonce, proofValue }), }); if (finishRes.status === 401 || finishRes.status === 403) { - throw new Error("This DID is not linked to an account on this server, or the proof was rejected."); + throw new Error( + "This DID is not linked to an account on this server, or the proof was rejected.", + ); } if (!finishRes.ok) throw new Error(`DID login failed (${finishRes.status}).`); const { authorization, webid, webId } = (await finishRes.json()) as { diff --git a/src/lib/solid/email-verification.ts b/src/lib/solid/email-verification.ts index 6070710..5dadbe9 100644 --- a/src/lib/solid/email-verification.ts +++ b/src/lib/solid/email-verification.ts @@ -47,7 +47,7 @@ export async function serverRequiresEmailVerification(server?: string): Promise< c?.password?.verify || c?.password?.confirm || c?.account?.emailVerification || - c?.account?.verifyEmail + c?.account?.verifyEmail, ); } catch { return false; diff --git a/src/lib/solid/pod-fs.ts b/src/lib/solid/pod-fs.ts index 590ba23..71eaf86 100644 --- a/src/lib/solid/pod-fs.ts +++ b/src/lib/solid/pod-fs.ts @@ -1,18 +1,18 @@ "use client"; import { - getSolidDataset, + createContainerAt, + deleteContainer, + deleteFile, getContainedResourceUrlAll, - getThing, + getContentType, getDatetime, - getInteger, - deleteFile, - deleteContainer, - overwriteFile, getFile, - createContainerAt, + getInteger, + getSolidDataset, getSourceUrl, - getContentType, + getThing, + overwriteFile, } from "@inrupt/solid-client"; import { getPlatform } from "@/lib/platform"; @@ -78,10 +78,10 @@ export async function readdir(containerUrl: string): Promise { const isContainer = url.endsWith("/"); const thing = getThing(dataset, url); const modified = thing - ? getDatetime(thing, "http://purl.org/dc/terms/modified") ?? undefined + ? (getDatetime(thing, "http://purl.org/dc/terms/modified") ?? undefined) : undefined; const size = thing - ? getInteger(thing, "http://www.w3.org/ns/posix/stat#size") ?? undefined + ? (getInteger(thing, "http://www.w3.org/ns/posix/stat#size") ?? undefined) : undefined; return { url, @@ -103,8 +103,9 @@ export async function exists(url: string): Promise { await getSolidDataset(url, { fetch: await noCacheFetch() }); return true; } catch (e: unknown) { - const status = (e as { statusCode?: number; response?: { status?: number } }) - ?.statusCode ?? (e as { response?: { status?: number } })?.response?.status; + const status = + (e as { statusCode?: number; response?: { status?: number } })?.statusCode ?? + (e as { response?: { status?: number } })?.response?.status; if (status === 404) return false; // 401/403 etc. — surface to caller. throw e; @@ -123,7 +124,7 @@ export async function readFileBlob(url: string): Promise { export async function writeFileText( url: string, contents: string, - contentType = "text/plain" + contentType = "text/plain", ): Promise { await overwriteFile(url, new Blob([contents], { type: contentType }), { contentType, @@ -134,7 +135,7 @@ export async function writeFileText( export async function writeFileBlob( url: string, blob: Blob, - contentType?: string + contentType?: string, ): Promise { const type = contentType ?? blob.type ?? "application/octet-stream"; const result = await overwriteFile(url, blob, { @@ -165,10 +166,7 @@ export async function mkdir(url: string): Promise { * in JS — the cause of console noise on a "create the zone" path). `container` * should be under `podRoot`; falls back to an existence check otherwise. */ -export async function ensureContainerChain( - container: string, - podRoot: string -): Promise { +export async function ensureContainerChain(container: string, podRoot: string): Promise { const base = ensureSlash(podRoot); if (!container.startsWith(base)) { if (!(await exists(container))) await mkdir(container); @@ -201,7 +199,7 @@ export async function ensureContainerChain( */ export async function resourceExistsByListing( resourceUrl: string, - podRoot: string + podRoot: string, ): Promise { const base = ensureSlash(podRoot); if (!resourceUrl.startsWith(base)) return exists(resourceUrl); diff --git a/src/lib/solid/profile.ts b/src/lib/solid/profile.ts index 9fd5e10..ab6e4ca 100644 --- a/src/lib/solid/profile.ts +++ b/src/lib/solid/profile.ts @@ -1,14 +1,14 @@ "use client"; import { + createThing, getSolidDataset, - getThing, getStringNoLocale, + getThing, getUrl, + saveSolidDatasetAt, setStringNoLocale, setThing, - saveSolidDatasetAt, - createThing, } from "@inrupt/solid-client"; import { getPlatform } from "@/lib/platform"; import type { AccountIdentity } from "@/lib/shell/types"; @@ -52,8 +52,7 @@ export async function readProfile(webId: string): Promise { const displayName = (me && (getStringNoLocale(me, FOAF_NAME) ?? getStringNoLocale(me, VCARD_FN))) ?? webIdLabel(webId); - const avatarUrl = - (me && (getUrl(me, VCARD_PHOTO) ?? getUrl(me, FOAF_IMG))) ?? undefined; + const avatarUrl = (me && (getUrl(me, VCARD_PHOTO) ?? getUrl(me, FOAF_IMG))) ?? undefined; return { webId, displayName: displayName ?? undefined, avatarUrl: avatarUrl ?? undefined }; } catch { return { webId, displayName: webIdLabel(webId) }; diff --git a/src/lib/solid/resume.ts b/src/lib/solid/resume.ts index d15de51..33542bd 100644 --- a/src/lib/solid/resume.ts +++ b/src/lib/solid/resume.ts @@ -21,11 +21,11 @@ * last-active-passport pointer (a registry id) plus the already-unlocked wallet. */ -import { getPlatform } from "@/lib/platform"; -import { hasWallet, getView, getPassports } from "@/lib/identity/wallet"; +import { isVerificationPending } from "@/lib/identity/email"; import { enterPassport } from "@/lib/identity/passport-login"; import { LAST_ACTIVE_PASSPORT_KEY, type Passport } from "@/lib/identity/types"; -import { isVerificationPending } from "@/lib/identity/email"; +import { getPassports, getView, hasWallet } from "@/lib/identity/wallet"; +import { getPlatform } from "@/lib/platform"; import { getActivePassportSession } from "./passport-session"; /** Where the front door should send the user on load. */ diff --git a/src/lib/solid/workspaces.ts b/src/lib/solid/workspaces.ts index c18d1ec..3e52e69 100644 --- a/src/lib/solid/workspaces.ts +++ b/src/lib/solid/workspaces.ts @@ -1,22 +1,22 @@ "use client"; import { - getSolidDataset, + buildThing, createSolidDataset, - saveSolidDatasetAt, + createThing, + getSolidDataset, + getStringNoLocale, getThingAll, getUrl, getUrlAll, - getStringNoLocale, - setThing, removeThing, - buildThing, - createThing, + saveSolidDatasetAt, + setThing, } from "@inrupt/solid-client"; import { getPlatform } from "@/lib/platform"; -import { resourceExistsByListing } from "@/lib/solid/pod-fs"; -import { shellZone } from "@/lib/shell/types"; import type { WorkspaceRef, WorkspaceRole } from "@/lib/shell/types"; +import { shellZone } from "@/lib/shell/types"; +import { resourceExistsByListing } from "@/lib/solid/pod-fs"; /** * The per-identity Workspace index (PRD-IDENTITY.md §4 — "Mechanism A"). @@ -59,9 +59,7 @@ async function noStoreFetch(): Promise { } function asRole(value: string | null): WorkspaceRole { - return value && (ROLES as readonly string[]).includes(value) - ? (value as WorkspaceRole) - : "owner"; + return value && (ROLES as readonly string[]).includes(value) ? (value as WorkspaceRole) : "owner"; } /** diff --git a/src/lib/tokens/api.ts b/src/lib/tokens/api.ts index dcbd89a..4c4798c 100644 --- a/src/lib/tokens/api.ts +++ b/src/lib/tokens/api.ts @@ -72,10 +72,7 @@ export function ledgerOrigin(webId: string): string { } /** Fetch the caller's balance + signed history. Never throws on HTTP errors. */ -export async function fetchTokens( - fetchFn: typeof fetch, - origin: string -): Promise { +export async function fetchTokens(fetchFn: typeof fetch, origin: string): Promise { let res: Response; try { res = await fetchFn(`${origin}.tokens`, { @@ -100,7 +97,7 @@ export async function fetchTokens( export async function registerSigningDid( fetchFn: typeof fetch, origin: string, - did: string + did: string, ): Promise { const res = await fetchFn(`${origin}.tokens/did`, { method: "POST", @@ -175,7 +172,7 @@ export type TransferResult = export async function submitTransfer( origin: string, transfer: SignedTransfer, - proofB64: string + proofB64: string, ): Promise { let res: Response; try { @@ -211,9 +208,10 @@ export async function submitTransfer( if (res.status === 403) { return { status: "forbidden", - detail: body.error === "transfers_disabled" - ? "Transfers are disabled on this server." - : "The server rejected the transfer signature.", + detail: + body.error === "transfers_disabled" + ? "Transfers are disabled on this server." + : "The server rejected the transfer signature.", }; } return { status: "error", detail: body.error ?? `Transfer failed (${res.status}).` }; diff --git a/src/lib/vault/autolock.ts b/src/lib/vault/autolock.ts index b310fc7..fb25118 100644 --- a/src/lib/vault/autolock.ts +++ b/src/lib/vault/autolock.ts @@ -27,7 +27,7 @@ const ACTIVITY_EVENTS: (keyof DocumentEventMap)[] = [ export function useAutoLock( active: boolean, onLock: () => void, - idleMs: number = DEFAULT_IDLE_MS + idleMs: number = DEFAULT_IDLE_MS, ): void { const timer = useRef | null>(null); const lockRef = useRef(onLock); diff --git a/src/lib/vault/clipboard.ts b/src/lib/vault/clipboard.ts index f33cbb8..3eb8029 100644 --- a/src/lib/vault/clipboard.ts +++ b/src/lib/vault/clipboard.ts @@ -19,7 +19,7 @@ export const DEFAULT_CLEAR_MS = 30_000; export async function copyWithAutoClear( text: string, - ms: number = DEFAULT_CLEAR_MS + ms: number = DEFAULT_CLEAR_MS, ): Promise { await navigator.clipboard.writeText(text); diff --git a/src/lib/vault/core.ts b/src/lib/vault/core.ts index 3001941..7640ccd 100644 --- a/src/lib/vault/core.ts +++ b/src/lib/vault/core.ts @@ -15,39 +15,39 @@ * behind the numeric handle. */ +import type { + CreatedIdentity, + CryptoCore, + HibpPrefix, + IdentityHandle, + KdfParams, + PwGenOptions, + SealedItem, + SessionHandle, + UnlockedIdentity, + VaultBootstrap, +} from "./crypto-contract"; import init, { init as initPanicHook, calibrateKdf as wasmCalibrateKdf, + changePassword as wasmChangePassword, createVault as wasmCreateVault, - unlock as wasmUnlock, - lock as wasmLock, - encryptItem as wasmEncryptItem, decryptItem as wasmDecryptItem, - changePassword as wasmChangePassword, - generatePassword as wasmGeneratePassword, + encryptItem as wasmEncryptItem, generatePassphrase as wasmGeneratePassphrase, - totpAt as wasmTotpAt, + generatePassword as wasmGeneratePassword, hibpPrefix as wasmHibpPrefix, identityCreate as wasmIdentityCreate, - identityUnlock as wasmIdentityUnlock, identityFromSeed as wasmIdentityFromSeed, + identityLock as wasmIdentityLock, + identityUnlock as wasmIdentityUnlock, + lock as wasmLock, masterDid as wasmMasterDid, signDetached as wasmSignDetached, + totpAt as wasmTotpAt, + unlock as wasmUnlock, verifyBinding as wasmVerifyBinding, - identityLock as wasmIdentityLock, } from "./pkg/crypto_core"; -import type { - CryptoCore, - KdfParams, - VaultBootstrap, - SealedItem, - PwGenOptions, - HibpPrefix, - SessionHandle, - CreatedIdentity, - UnlockedIdentity, - IdentityHandle, -} from "./crypto-contract"; let corePromise: Promise | null = null; @@ -63,7 +63,7 @@ function build(): CryptoCore { masterPassword: string, saltB64: string, params: KdfParams, - wrappedDataKeyB64: string + wrappedDataKeyB64: string, ): SessionHandle { return wasmUnlock(masterPassword, saltB64, params, wrappedDataKeyB64) as SessionHandle; }, @@ -74,7 +74,7 @@ function build(): CryptoCore { handle: SessionHandle, itemId: string, version: number, - plaintextJson: string + plaintextJson: string, ): SealedItem { return wasmEncryptItem(handle, itemId, version, plaintextJson) as SealedItem; }, @@ -82,15 +82,11 @@ function build(): CryptoCore { handle: SessionHandle, itemId: string, version: number, - sealed: SealedItem + sealed: SealedItem, ): string { return wasmDecryptItem(handle, itemId, version, sealed); }, - changePassword( - handle: SessionHandle, - newPassword: string, - params: KdfParams - ): VaultBootstrap { + changePassword(handle: SessionHandle, newPassword: string, params: KdfParams): VaultBootstrap { return wasmChangePassword(handle, newPassword, params) as VaultBootstrap; }, generatePassword(opts: PwGenOptions): string { @@ -114,9 +110,14 @@ function build(): CryptoCore { masterPassword: string, saltB64: string, params: KdfParams, - wrappedSeedB64: string + wrappedSeedB64: string, ): UnlockedIdentity { - return wasmIdentityUnlock(masterPassword, saltB64, params, wrappedSeedB64) as UnlockedIdentity; + return wasmIdentityUnlock( + masterPassword, + saltB64, + params, + wrappedSeedB64, + ) as UnlockedIdentity; }, identityFromSeed(seedB64: string): IdentityHandle { return wasmIdentityFromSeed(seedB64); diff --git a/src/lib/vault/crypto-contract.ts b/src/lib/vault/crypto-contract.ts index 8f85bde..5371dc8 100644 --- a/src/lib/vault/crypto-contract.ts +++ b/src/lib/vault/crypto-contract.ts @@ -60,26 +60,17 @@ export interface CryptoCore extends IdentityCore { masterPassword: string, saltB64: string, params: KdfParams, - wrappedDataKeyB64: string + wrappedDataKeyB64: string, ): SessionHandle; lock(handle: SessionHandle): void; encryptItem( handle: SessionHandle, itemId: string, version: number, - plaintextJson: string + plaintextJson: string, ): SealedItem; - decryptItem( - handle: SessionHandle, - itemId: string, - version: number, - sealed: SealedItem - ): string; - changePassword( - handle: SessionHandle, - newPassword: string, - params: KdfParams - ): VaultBootstrap; + decryptItem(handle: SessionHandle, itemId: string, version: number, sealed: SealedItem): string; + changePassword(handle: SessionHandle, newPassword: string, params: KdfParams): VaultBootstrap; generatePassword(opts: PwGenOptions): string; generatePassphrase(words: number, separator: string): string; totpAt(secretB32: string, unixSeconds: number, period: number, digits: number): string; @@ -146,7 +137,7 @@ export interface IdentityCore { masterPassword: string, saltB64: string, params: KdfParams, - wrappedSeedB64: string + wrappedSeedB64: string, ): UnlockedIdentity; /** * Mint a session from a base64 32-byte seed; returns its handle. The seed is diff --git a/src/lib/vault/model.ts b/src/lib/vault/model.ts index c030d59..1fd6237 100644 --- a/src/lib/vault/model.ts +++ b/src/lib/vault/model.ts @@ -14,18 +14,18 @@ * Nothing else — no plaintext password/note/totp ever reaches the pod or disk. */ +import type { AsyncCryptoCore } from "@/lib/platform"; import { exists, + mkdir, readdir, readFileBlob, readFileText, + unlink, writeFileBlob, writeFileText, - mkdir, - unlink, } from "@/lib/solid/pod-fs"; import type { SealedItem, SessionHandle, VaultBootstrap } from "./crypto-contract"; -import type { AsyncCryptoCore } from "@/lib/platform"; export const VAULT_SCHEMA_VERSION = 1; @@ -323,7 +323,7 @@ export async function createVaultOnPod( core: AsyncCryptoCore, zone: string, podRoot: string, - masterPw: string + masterPw: string, ): Promise { const params = await core.calibrateKdf(750); const bootstrap = await core.createVault(masterPw, params); @@ -354,7 +354,7 @@ export async function saveItem( handle: SessionHandle, vault: LoadedVault, meta: VaultItemMeta, - secret: VaultItemSecret + secret: VaultItemSecret, ): Promise { const nextVersion = meta.version + 1; const saved: VaultItemMeta = { @@ -378,7 +378,7 @@ export async function loadItemSecret( core: AsyncCryptoCore, zone: string, handle: SessionHandle, - meta: VaultItemMeta + meta: VaultItemMeta, ): Promise { const blob = await readFileBlob(itemUrl(zone, meta.id)); const bytes = new Uint8Array(await blob.arrayBuffer()); @@ -388,11 +388,7 @@ export async function loadItemSecret( } /** Delete an item: remove the .enc blob and drop it from the index. */ -export async function deleteItem( - zone: string, - vault: LoadedVault, - id: string -): Promise { +export async function deleteItem(zone: string, vault: LoadedVault, id: string): Promise { try { await unlink(itemUrl(zone, id)); } catch { @@ -412,7 +408,7 @@ export async function changeMasterPassword( zone: string, handle: SessionHandle, vault: LoadedVault, - newPw: string + newPw: string, ): Promise { const params = await core.calibrateKdf(750); const bootstrap = await core.changePassword(handle, newPw, params);