From 5c3217e3d5bd4f99b24fc5073e5c62ecd7a74fae Mon Sep 17 00:00:00 2001 From: Hammer Date: Thu, 29 Jan 2026 17:05:32 +0000 Subject: [PATCH] fix: replace frontend with Rust OPAQUE API + Flutter keypad UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Full OPAQUE auth flow via WASM client SDK (client-wasm crate) - New user: Key Register → Key Login → Code Register (icon selection) → done - Existing user: Key Login → get login-data → icon keypad → Code Login → done - Icon-based keypad matching Flutter design: - 2 cols portrait, 3 cols landscape - Key tiles with 3-col sub-grid of icons - Navy border press feedback - Dot display with backspace + submit - SVGs rendered as-is (no color manipulation) - SusiPage with Login/Signup tabs - LoginKeypadPage and SignupKeypadPage for code flows - Secret key display/copy on signup - Unit tests for Keypad component - WASM pkg bundled locally (no external dep) --- Dockerfile | 11 +- bun.lock | 237 ++++++++++++++- index.html | 9 +- package.json | 20 +- public/wasm/nkode_client_wasm_bg.js | 133 ++++++++- public/wasm/nkode_client_wasm_bg.wasm | Bin 296923 -> 412605 bytes src/App.tsx | 68 ++--- src/__tests__/AuthFlow.test.tsx | 223 +++++++++++++++ src/__tests__/Keypad.test.tsx | 194 +++++++++++++ src/__tests__/SusiPage.test.tsx | 103 +++++++ src/__tests__/setup.ts | 1 + src/components/Keypad.tsx | 398 ++++++++++++++++++-------- src/components/Layout.tsx | 79 ++--- src/hooks/useAuth.ts | 95 +++--- src/hooks/useTheme.ts | 34 --- src/index.css | 46 --- src/lib/nkode-client.ts | 134 --------- src/lib/types.ts | 26 -- src/main.tsx | 32 ++- src/pages/AdminPage.tsx | 44 --- src/pages/DeveloperPage.tsx | 57 ---- src/pages/HomePage.tsx | 109 +++---- src/pages/LoginKeypadPage.tsx | 114 ++++++++ src/pages/LoginPage.tsx | 124 -------- src/pages/NotFoundPage.tsx | 13 + src/pages/SignupKeypadPage.tsx | 165 +++++++++++ src/pages/SignupPage.tsx | 277 ------------------ src/pages/SusiPage.tsx | 187 ++++++++++++ src/services/api.ts | 112 ++++++++ src/services/auth.ts | 23 ++ src/types/index.ts | 36 +++ src/types/nkode-client-wasm.d.ts | 19 -- src/vite-env.d.ts | 9 + src/wasm.d.ts | 26 ++ tsconfig.app.json | 8 +- vite.config.ts | 28 +- 36 files changed, 2045 insertions(+), 1149 deletions(-) create mode 100644 src/__tests__/AuthFlow.test.tsx create mode 100644 src/__tests__/Keypad.test.tsx create mode 100644 src/__tests__/SusiPage.test.tsx create mode 100644 src/__tests__/setup.ts delete mode 100644 src/hooks/useTheme.ts delete mode 100644 src/lib/nkode-client.ts delete mode 100644 src/lib/types.ts delete mode 100644 src/pages/AdminPage.tsx delete mode 100644 src/pages/DeveloperPage.tsx create mode 100644 src/pages/LoginKeypadPage.tsx delete mode 100644 src/pages/LoginPage.tsx create mode 100644 src/pages/NotFoundPage.tsx create mode 100644 src/pages/SignupKeypadPage.tsx delete mode 100644 src/pages/SignupPage.tsx create mode 100644 src/pages/SusiPage.tsx create mode 100644 src/services/api.ts create mode 100644 src/services/auth.ts create mode 100644 src/types/index.ts delete mode 100644 src/types/nkode-client-wasm.d.ts create mode 100644 src/vite-env.d.ts create mode 100644 src/wasm.d.ts diff --git a/Dockerfile b/Dockerfile index 3571d92..30a9b85 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,18 @@ # Stage 1: Build FROM oven/bun:1 AS build WORKDIR /app -COPY package.json bun.lock ./ -RUN bun install --frozen-lockfile -COPY . . -ARG VITE_NKODE_API_URL=https://api.nkode.donovankelly.xyz + +ARG VITE_NKODE_API_URL ENV VITE_NKODE_API_URL=$VITE_NKODE_API_URL + +COPY package.json bun.lock* ./ +RUN bun install --frozen-lockfile || bun install +COPY . . RUN bun run build # Stage 2: Serve FROM nginx:alpine COPY --from=build /app/dist /usr/share/nginx/html -# SPA routing: all paths → index.html RUN printf 'server {\n listen 80;\n root /usr/share/nginx/html;\n index index.html;\n location / {\n try_files $uri $uri/ /index.html;\n }\n}\n' > /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] diff --git a/bun.lock b/bun.lock index b6056aa..42621c2 100644 --- a/bun.lock +++ b/bun.lock @@ -7,11 +7,17 @@ "dependencies": { "react": "^19.2.0", "react-dom": "^19.2.0", + "react-router-dom": "^6.30.3", + "vite-plugin-top-level-await": "^1.6.0", + "vite-plugin-wasm": "^3.5.0", }, "devDependencies": { "@eslint/js": "^9.39.1", "@tailwindcss/vite": "^4.1.18", - "@types/node": "^24.10.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^25.1.0", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", @@ -19,15 +25,26 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", - "react-router-dom": "^7.13.0", + "jsdom": "^27.4.0", "tailwindcss": "^4.1.18", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "^7.2.4", + "vitest": "^4.0.18", }, }, }, "packages": { + "@acemir/cssom": ["@acemir/cssom@0.9.31", "", {}, "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA=="], + + "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], + + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@4.1.1", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "lru-cache": "^11.2.4" } }, "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ=="], + + "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@6.7.6", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.4" } }, "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg=="], + + "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], + "@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], "@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], @@ -60,12 +77,26 @@ "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], "@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], "@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], + + "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@3.1.0", "", { "dependencies": { "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="], + + "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.0.26", "", {}, "sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], @@ -136,6 +167,8 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + "@exodus/bytes": ["@exodus/bytes@1.10.0", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-tf8YdcbirXdPnJ+Nd4UN1EXnz+IP2DI45YVEr3vvzcVTOyrApkmIB4zvOQVd3XPr7RXnfBtAx+PXImXOIU0Ajg=="], + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], @@ -154,8 +187,12 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@remix-run/router": ["@remix-run/router@1.23.2", "", {}, "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="], + "@rollup/plugin-virtual": ["@rollup/plugin-virtual@3.0.2", "", { "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.0", "", { "os": "android", "cpu": "arm" }, "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.0", "", { "os": "android", "cpu": "arm64" }, "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg=="], @@ -206,6 +243,36 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@swc/core": ["@swc/core@1.15.11", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.11", "@swc/core-darwin-x64": "1.15.11", "@swc/core-linux-arm-gnueabihf": "1.15.11", "@swc/core-linux-arm64-gnu": "1.15.11", "@swc/core-linux-arm64-musl": "1.15.11", "@swc/core-linux-x64-gnu": "1.15.11", "@swc/core-linux-x64-musl": "1.15.11", "@swc/core-win32-arm64-msvc": "1.15.11", "@swc/core-win32-ia32-msvc": "1.15.11", "@swc/core-win32-x64-msvc": "1.15.11" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w=="], + + "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.15.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg=="], + + "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.15.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-S52Gu1QtPSfBYDiejlcfp9GlN+NjTZBRRNsz8PNwBgSE626/FUf2PcllVUix7jqkoMC+t0rS8t+2/aSWlMuQtA=="], + + "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.15.11", "", { "os": "linux", "cpu": "arm" }, "sha512-lXJs8oXo6Z4yCpimpQ8vPeCjkgoHu5NoMvmJZ8qxDyU99KVdg6KwU9H79vzrmB+HfH+dCZ7JGMqMF//f8Cfvdg=="], + + "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.15.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-chRsz1K52/vj8Mfq/QOugVphlKPWlMh10V99qfH41hbGvwAU6xSPd681upO4bKiOr9+mRIZZW+EfJqY42ZzRyA=="], + + "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.15.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w=="], + + "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.15.11", "", { "os": "linux", "cpu": "x64" }, "sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ=="], + + "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.15.11", "", { "os": "linux", "cpu": "x64" }, "sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw=="], + + "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.15.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA=="], + + "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.15.11", "", { "os": "win32", "cpu": "ia32" }, "sha512-6XnzORkZCQzvTQ6cPrU7iaT9+i145oLwnin8JrfsLG41wl26+5cNQ2XV3zcbrnFEV6esjOceom9YO1w9mGJByw=="], + + "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.15.11", "", { "os": "win32", "cpu": "x64" }, "sha512-IQ2n6af7XKLL6P1gIeZACskSxK8jWtoKpJWLZmdXTDj1MGzktUy4i+FvpdtxFmJWNavRWH1VmTr6kAubRDHeKw=="], + + "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], + + "@swc/types": ["@swc/types@0.1.25", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g=="], + + "@swc/wasm": ["@swc/wasm@1.15.11", "", {}, "sha512-230rdYZf8ux3nIwISOQNCFrxzxpL/UFY4Khv/3UsvpEdo709j/+Tg80yXWW3DXETeZNPBV72QpvEBhXsl7Lc9g=="], + "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="], @@ -236,6 +303,16 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="], + "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], + + "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], + + "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="], + + "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="], + + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], @@ -244,11 +321,15 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/node": ["@types/node@24.10.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw=="], + "@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="], "@types/react": ["@types/react@19.2.10", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw=="], @@ -276,20 +357,44 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ=="], + "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], + + "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="], + + "@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="], + + "@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="], + + "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="], + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], + "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], @@ -298,6 +403,8 @@ "caniuse-lite": ["caniuse-lite@1.0.30001766", "", {}, "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -308,22 +415,38 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="], + + "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], + + "cssstyle": ["cssstyle@5.3.7", "", { "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", "css-tree": "^3.1.0", "lru-cache": "^11.2.4" } }, "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "data-urls": ["data-urls@6.0.1", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^15.1.0" } }, "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + "electron-to-chromium": ["electron-to-chromium@1.5.282", "", {}, "sha512-FCPkJtpst28UmFzd903iU7PdeVTfY0KAeJy+Lk0GLZRwgwYHn/irRcaCbQQOmr5Vytc/7rcavsYLvTM8RiHYhQ=="], "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -348,8 +471,12 @@ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], @@ -382,16 +509,26 @@ "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], @@ -400,6 +537,8 @@ "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "jsdom": ["jsdom@27.4.0", "", { "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", "@exodus/bytes": "^1.6.0", "cssstyle": "^5.3.4", "data-urls": "^6.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.0", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.1.0", "ws": "^8.18.3", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], @@ -442,10 +581,16 @@ "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "lru-cache": ["lru-cache@11.2.5", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="], + + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="], + + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -456,6 +601,8 @@ "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], @@ -464,10 +611,14 @@ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], @@ -476,44 +627,76 @@ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], - "react-router": ["react-router@7.13.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw=="], + "react-router": ["react-router@6.30.3", "", { "dependencies": { "@remix-run/router": "1.23.2" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw=="], - "react-router-dom": ["react-router-dom@7.13.0", "", { "dependencies": { "react-router": "7.13.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g=="], + "react-router-dom": ["react-router-dom@6.30.3", "", { "dependencies": { "@remix-run/router": "1.23.2", "react-router": "6.30.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag=="], + + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "rollup": ["rollup@4.57.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.0", "@rollup/rollup-android-arm64": "4.57.0", "@rollup/rollup-darwin-arm64": "4.57.0", "@rollup/rollup-darwin-x64": "4.57.0", "@rollup/rollup-freebsd-arm64": "4.57.0", "@rollup/rollup-freebsd-x64": "4.57.0", "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", "@rollup/rollup-linux-arm-musleabihf": "4.57.0", "@rollup/rollup-linux-arm64-gnu": "4.57.0", "@rollup/rollup-linux-arm64-musl": "4.57.0", "@rollup/rollup-linux-loong64-gnu": "4.57.0", "@rollup/rollup-linux-loong64-musl": "4.57.0", "@rollup/rollup-linux-ppc64-gnu": "4.57.0", "@rollup/rollup-linux-ppc64-musl": "4.57.0", "@rollup/rollup-linux-riscv64-gnu": "4.57.0", "@rollup/rollup-linux-riscv64-musl": "4.57.0", "@rollup/rollup-linux-s390x-gnu": "4.57.0", "@rollup/rollup-linux-x64-gnu": "4.57.0", "@rollup/rollup-linux-x64-musl": "4.57.0", "@rollup/rollup-openbsd-x64": "4.57.0", "@rollup/rollup-openharmony-arm64": "4.57.0", "@rollup/rollup-win32-arm64-msvc": "4.57.0", "@rollup/rollup-win32-ia32-msvc": "4.57.0", "@rollup/rollup-win32-x64-gnu": "4.57.0", "@rollup/rollup-win32-x64-msvc": "4.57.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA=="], + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], + + "tldts": ["tldts@7.0.19", "", { "dependencies": { "tldts-core": "^7.0.19" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA=="], + + "tldts-core": ["tldts-core@7.0.19", "", {}, "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A=="], + + "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="], + + "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], @@ -528,12 +711,36 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + "vite-plugin-top-level-await": ["vite-plugin-top-level-await@1.6.0", "", { "dependencies": { "@rollup/plugin-virtual": "^3.0.2", "@swc/core": "^1.12.14", "@swc/wasm": "^1.12.14", "uuid": "10.0.0" }, "peerDependencies": { "vite": ">=2.8" } }, "sha512-bNhUreLamTIkoulCR9aDXbTbhLk6n1YE8NJUTTxl5RYskNRtzOR0ASzSjBVRtNdjIfngDXo11qOsybGLNsrdww=="], + + "vite-plugin-wasm": ["vite-plugin-wasm@3.5.0", "", { "peerDependencies": { "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7" } }, "sha512-X5VWgCnqiQEGb+omhlBVsvTfxikKtoOgAzQ95+BZ8gQ+VfMHIjSHr0wyvXFQCa0eKQ0fKyaL0kWcEnYqBac4lQ=="], + + "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + + "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + + "whatwg-url": ["whatwg-url@15.1.0", "", { "dependencies": { "tr46": "^6.0.0", "webidl-conversions": "^8.0.0" } }, "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], @@ -542,6 +749,8 @@ "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], @@ -558,12 +767,20 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], + + "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "data-urls/whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], + + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], } } diff --git a/index.html b/index.html index 4db77b7..25e430c 100644 --- a/index.html +++ b/index.html @@ -2,11 +2,12 @@ + - nKode — Passwordless Auth - - - + + + + nKode
diff --git a/package.json b/package.json index e5bdc19..c64c65d 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,29 @@ { "name": "nkode-web", "private": true, - "version": "0.0.0", + "version": "0.1.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run" }, "dependencies": { "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "react-router-dom": "^6.30.3", + "vite-plugin-top-level-await": "^1.6.0", + "vite-plugin-wasm": "^3.5.0" }, "devDependencies": { "@eslint/js": "^9.39.1", "@tailwindcss/vite": "^4.1.18", - "@types/node": "^24.10.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^25.1.0", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", @@ -24,10 +31,11 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", - "react-router-dom": "^7.13.0", + "jsdom": "^27.4.0", "tailwindcss": "^4.1.18", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", - "vite": "^7.2.4" + "vite": "^7.2.4", + "vitest": "^4.0.18" } } diff --git a/public/wasm/nkode_client_wasm_bg.js b/public/wasm/nkode_client_wasm_bg.js index 9416f3b..3c4a898 100644 --- a/public/wasm/nkode_client_wasm_bg.js +++ b/public/wasm/nkode_client_wasm_bg.js @@ -96,6 +96,14 @@ function getStringFromWasm0(ptr, len) { return decodeText(ptr, len); } +let cachedUint32ArrayMemory0 = null; +function getUint32ArrayMemory0() { + if (cachedUint32ArrayMemory0 === null || cachedUint32ArrayMemory0.byteLength === 0) { + cachedUint32ArrayMemory0 = new Uint32Array(wasm.memory.buffer); + } + return cachedUint32ArrayMemory0; +} + let cachedUint8ArrayMemory0 = null; function getUint8ArrayMemory0() { if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { @@ -145,6 +153,13 @@ function makeMutClosure(arg0, arg1, dtor, f) { return real; } +function passArray32ToWasm0(arg, malloc) { + const ptr = malloc(arg.length * 4, 4) >>> 0; + getUint32ArrayMemory0().set(arg, ptr / 4); + WASM_VECTOR_LEN = arg.length; + return ptr; +} + function passArray8ToWasm0(arg, malloc) { const ptr = malloc(arg.length * 1, 1) >>> 0; getUint8ArrayMemory0().set(arg, ptr / 1); @@ -189,6 +204,12 @@ function passStringToWasm0(arg, malloc, realloc) { return ptr; } +function takeFromExternrefTable0(idx) { + const value = wasm.__wbindgen_externrefs.get(idx); + wasm.__externref_table_dealloc(idx); + return value; +} + let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); cachedTextDecoder.decode(); const MAX_SAFARI_DECODE_BYTES = 2146435072; @@ -400,6 +421,56 @@ export class NKodeClient { const ret = wasm.nkodeclient_updateLoginData(this.__wbg_ptr, ptr0, len0); return ret; } + /** + * Decipher key selections into OPAQUE passcode bytes for code login. + * Call this after the user taps their nKode sequence on the keypad. + * + * @param userId - The user's ID + * @param secretKeyHex - The user's secret key as hex + * @param loginDataBytes - Raw JSON bytes of login data (from server) + * @param keySelections - Array of key indices the user tapped + * @returns Uint8Array - The passcode bytes to pass to loginCode() + * @param {string} secret_key_hex + * @param {string} login_data_json + * @param {Uint32Array} key_selections + * @returns {Uint8Array} + */ + decipherSelection(secret_key_hex, login_data_json, key_selections) { + const ptr0 = passStringToWasm0(secret_key_hex, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(login_data_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + const ptr2 = passArray32ToWasm0(key_selections, wasm.__wbindgen_malloc); + const len2 = WASM_VECTOR_LEN; + const ret = wasm.nkodeclient_decipherSelection(this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2); + if (ret[3]) { + throw takeFromExternrefTable0(ret[2]); + } + var v4 = getArrayU8FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 1, 1); + return v4; + } + /** + * Prepare for code login: fetch login data, reconstruct keypad, and fetch icons. + * Returns the keypad configuration with icons for UI display. + * + * Also stores the raw login data JSON internally for decipherSelection(). + * + * @param userId - The user's ID + * @param secretKeyHex - The user's secret key as hex string + * @returns Promise - { keypadIndices, propertiesPerKey, numberOfKeys, mask, icons, loginDataJson } + * @param {string} user_id + * @param {string} secret_key_hex + * @returns {Promise} + */ + prepareCodeLogin(user_id, secret_key_hex) { + const ptr0 = passStringToWasm0(user_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(secret_key_hex, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + const ret = wasm.nkodeclient_prepareCodeLogin(this.__wbg_ptr, ptr0, len0, ptr1, len1); + return ret; + } /** * Generate a new random 16-byte secret key, returned as a hex string (32 chars). * @returns {string} @@ -416,6 +487,52 @@ export class NKodeClient { wasm.__wbindgen_free(deferred1_0, deferred1_1, 1); } } + /** + * Prepare icons for code registration (requires active key session). + * Fetches icons from the server and randomizes their names via ChaCha20. + * Stores intermediate state internally for completeCodeRegistration(). + * + * @returns Promise - JSON: { icons: [{ file_name, file_type, img_data }] } + * @returns {Promise} + */ + prepareCodeRegistration() { + const ret = wasm.nkodeclient_prepareCodeRegistration(this.__wbg_ptr); + return ret; + } + /** + * Complete code registration after icon selection. + * Enciphers the selection, registers OPAQUE code auth, and stores login data. + * + * @param selectedIndices - Array of icon indices the user selected (global indices, not key indices) + * @returns Promise + * @param {Uint32Array} selected_indices + * @returns {Promise} + */ + completeCodeRegistration(selected_indices) { + const ptr0 = passArray32ToWasm0(selected_indices, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.nkodeclient_completeCodeRegistration(this.__wbg_ptr, ptr0, len0); + return ret; + } + /** + * Complete code registration with email (full version). + * Enciphers the selection, registers OPAQUE code auth, and stores login data on server. + * + * @param email - User's email address + * @param selectedIndices - Uint32Array of icon indices the user selected + * @returns Promise + * @param {string} email + * @param {Uint32Array} selected_indices + * @returns {Promise} + */ + completeCodeRegistrationWithEmail(email, selected_indices) { + const ptr0 = passStringToWasm0(email, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passArray32ToWasm0(selected_indices, wasm.__wbindgen_malloc); + const len1 = WASM_VECTOR_LEN; + const ret = wasm.nkodeclient_completeCodeRegistrationWithEmail(this.__wbg_ptr, ptr0, len0, ptr1, len1); + return ret; + } /** * Create a new client pointed at the given nKode server base URL. * @param {string} base_url @@ -567,6 +684,10 @@ export function __wbg_fetch_8119fbf8d0e4f4d1(arg0, arg1) { return ret; }; +export function __wbg_getRandomValues_1c61fac11405ffdc() { return handleError(function (arg0, arg1) { + globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1)); +}, arguments) }; + export function __wbg_getRandomValues_b8f5dbd5f3995a9e() { return handleError(function (arg0, arg1) { arg0.getRandomValues(arg1); }, arguments) }; @@ -816,12 +937,6 @@ export function __wbg_versions_c01dfd4722a88165(arg0) { return ret; }; -export function __wbindgen_cast_151ffb1b798ab8ff(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { dtor_idx: 76, function: Function { arguments: [Externref], shim_idx: 77, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. - const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__h28b97059ae600264, wasm_bindgen__convert__closures_____invoke__h8f97ce5df83102bb); - return ret; -}; - export function __wbindgen_cast_2241b6af4c4b2941(arg0, arg1) { // Cast intrinsic for `Ref(String) -> Externref`. const ret = getStringFromWasm0(arg0, arg1); @@ -852,6 +967,12 @@ export function __wbindgen_cast_d6cd19b81560fd6e(arg0) { return ret; }; +export function __wbindgen_cast_f2cc0f2a96e2ef5b(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { dtor_idx: 115, function: Function { arguments: [Externref], shim_idx: 116, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__h28b97059ae600264, wasm_bindgen__convert__closures_____invoke__h8f97ce5df83102bb); + return ret; +}; + export function __wbindgen_init_externref_table() { const table = wasm.__wbindgen_externrefs; const offset = table.grow(4); diff --git a/public/wasm/nkode_client_wasm_bg.wasm b/public/wasm/nkode_client_wasm_bg.wasm index 4ac5f5783c9f54704ee71ae0ef83b2125ff3a872..035991b836b98fd78552e9a6e83d1f7cea6ee8c8 100644 GIT binary patch delta 185575 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 delta 75623 zcmdSC349er_CMNH)px&1I%Fq-^t}W~fFLTHfSQgQh$zhHh&nEy=$IgzIO@3E2oVt@ z1{x?7#tF(26%|DdIw)ZiM9`p&f+7aR5m6v2j;JXA@2T#4b0f~o@BQ9;pU-Es6@K}t+=&yDS!~aE0#p$EXD()C#XP5FEmhQ$bVI5WIZC>c61w=O%Oq9JuD;dODSgI_?mxQU$-S=YGkVOJ zFHo?=0MO4|Apx=JUlgF|Wv2PHI zs@O2U&mQkhvj=(8Szc1gK>Ka(TY1FGnLD1ApwG_r|Ir2VkiQtA_^3bf)2N+TRwAU^ zLj!Sa+!`pdXB+u;Ss-Mu4GeX=NR9UlnkwfZgeL?etdr`*@?Z&T?{+uBiU==uzfd~M zR%wLWicpT*GeX7Vp*+?ZrMjeFcYW_uPaQL6RIgJ<^||)s-dY!`u`)G$9#hRsNa+AH zGfdaBDcLT#=9G~tt&4qiDm5`NwJ+%EnQ2+}!n9mB_I6rNcQOx$ zeRiQy?500$w6iB09TTx$ZmFY27Z=MdL63b#dI2+3JBQQL?P=-hEI$$ZOS`0)adMxN zufKlu$)o$7)_>IK{?}h`&oT<^!|72wJ(Q1cy4}z&U3HYg>(kR?A@PL1OfT{1^dHlG zn�M3uD#kwD#~tfawKXz>0n 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≀NcfmSRrL4uy7eodsxu5Ye`<@*)*ZV`-&Y|@yvbk%#~F;CNehCp0TSBZ`p3I-K8jZ=zD!Bmj6L}f56Oya0fLn zd9Rd+cgK5OX*fT>*Pe!x4obQi7)e=0G(dx| z0H2)WX20=%*L*-pu~tC-D&iG{R?r3(FTgJPFk`3-7U@K;RbeU}RC*B@eu~v%P+<=; z)g<}M_GXwUhbR~&(}{vn%Bv{&#+{0SU*4%Gc=ygu-Q&;+z@$h(X`Lo?{whGI1mN~q zIPt-B+trzt^@v^h9o+$h%oH<#<;AEeFLkF8?B>_l{B)O#O@-UjKRAi-IPQaPgg^*x zg}}!?Xhq z6f@kH?+f7DzAtaabYaaR0(C(pnu)}f!*_R8n&a&s_2yXD&wiBE8AbgN!dQY4D3K&a zvn*1x@zDop5WkdcfALW?syGC=g5nMU4mmLD&12+0P_$wqo@ZqqF9An zsZ*koJ#Npq&_YrH{Z=k1sh{szlZpBF(tb%Pp2YN^c-pUgJSwzsQ=_9&F5F~yX;v!b z)6Ihe&wl{E$c)Ez`{NO?m@J^zoL0JiuYGKj|0#Jx) z**+HsL%INAf|v0!lay88e^XXJ*{hV*Zl92{dhI8jeXg?Fs-i9qCab75AxA}h{gV-- zqUL>Cod?wiY?4)&dNDWX59vOzQWA!)N?ftr18r$$j}8nkmqIHMZLb&Eb%gRm@Rz-I3Y{5B!DNSpH>x zu~L9lG18IUSq74cS?Q#b)lwRUfx>yeDy%xBCdY36a(rw=l&4^Wkc`4F(TP$V*?^Ek zq+7$b3dBMWR^^~ugB9v9HaN<)TN4CNGe>)o2*)NLC7g+OAf#bz)TblZ_W(d*xc1xo zQ^IZ!F^R?l_UB&>RH`|E?WD6J?2gVe1?lFxwbLP>vV+l+6-5>E#zc{TQ>0N9!DOYH zsij)3#VQ79xn`>Il5HPcn(8R?4n{$I=N(eo{5^**V)xqH59NoR2FppebfvU^f9RZ) zhiqaaaix98;Wf1747QB+)$Dde+s!S*V&8+e6!YrLNGg4+ARW(zl^((AnNT*eDy+@m zF(sPTPa`7@n9%yoUX55bp~7p`siI^K zpYNmHhkeP{g~W*e{Ph($c(LF8dPGR=X5HMAzUj~p?37tYAP7}P4XUXCgz8=|a`wk_ zUO-?Am4(Jk9NYRf*Iw{VDE5j7gTB*gsH?rkG3**3T9YEm9+2x~2+DB7`3`cN4287m znNB3$9Soxsoxr4{QPnRRnA1ZFsV){E;PjD_PV8i1?kn-_HI6|_wNJdnnwjx7UwPZN zcd&wo^$0d>)>p8>gIOEVR5Gd8zOYU+{;sb~`|f5Q+Qeu}M||=TM1xFE;>5S#rGmj2-(pp;6PWi!2oAX1W+zOLwVeSFe#p$Tq9S4kjiLp4GU-iW;FqO9!Na~2(##sdb_7|)SpW6jnmA`VG zW8Q)Zz^bFjZv26XOACD`FVNZ2NRtmr1k@1hlYE$LvZV=Yx*$!^i{-tCHNZ6>$XJt1 z_p+z2oA0uOi0Tb#Od)C={)Q^JpVBN^b7uvuYq2a}b&H^8oh)l2RRuwHDa%nGt( zyVqA}78WpRc4b+)%J8)s`l@0B0I?W~I>ERt$c93xkQ;+6M&>7-tU+MVx%7t&UvgxK zogO;oC=)&?6LL|A!Qv$kh8Rp6{LS!MQ7k|~wUKJi|*3MLWu$rT}%Eo)QQ>99M=w^LZp zpldJl8Y84Hm6`qGRxu@!dnLSJsdf(w*)^S!K$WA? z*<^OFd@r4K43Qly=-9#J@pOpBIx;F5F73+SwPQffi(MIh)~V#I)_b$qURbH*pR&Ov9+K~7vj@|# zsZvT2HWxbVRC0U{%VxL8`*PSgvV427SU=y0Mau> zOSIuqHYao(h9!V9Zfs~QWZihm3hW9~X-}61%|d=3VWUDX(B4g`y_>u?%05X2PBBSX zB-j-G$mH)kv7!(f1fD_o+k~9biCtzk)3L@I>NunV&0(tV;S{i1oDBI>Cw6i;zW-z3 z&$53JyEb$z&UlY)l1qym#@n+qyTD3p0kMw+FAT$s20sQrFGHNhY`MEaK^s6XG>a7L z7uxQnpkr!RD1W()<(WxaKxZ?owtzV4fN~R#Gxc8o*O+=(m-hu&CwYGttT*uX^<5Ni zKhlLIwuy?PcT*g_9>%&jN3RVijt(~sHWBU?q&OW@>Y}{5D~mv1lvBF0&SlUHJ;>VY z!#?Dw7h#P2h!C$JvI$d3F$A8YI--}@F>ogWvu<_+D4vRgwb ztGTI}ovkWA*`Vag2iXg$yKr~~(l`cXG6?o|%S=|tAJyf&nXHE=8VMU~<))de* zE5l4$%ZY?#do zJ11rewnO6saOMlB*Tof}9Gs*@eOOROnEYur8^~YhvcJuW3;*@sr*UNAftvq3G|Y8@ zY&VAuY^4=ai-=aPoWrKJrqU27wPg;wv2g2&GtdTMih^qAvOq2kWjGBbmxj`Wq3F&~ zo}0^xR4%rHa-$8qZ@j#)UC6a6V775|sV*i-xmCZBqob-X4h$}quRGISvSPejV#IB+{~IF=5M zr4(^aG8$%n7({8q1dFWkHLOQq8aU^_M;Ujp%%JCz=}1`+ND(d4RiDTbh0Q*+(XSUXGw^t#c>5)+ zG_aJSI@KP&g!Rd~586_RC@U8uQo%1(ZNpA z9 zIkTSS_Sp(Os8pNJEbn%lLuc@7 zR9ja)!7iXSeALEsPq1M{5b$nM>RS^d%YK_-GUFu~eUkO_hWZKk7s#uhWWT3TzVIY7 z(=ke@4xHEp*a8Ea{_IKColTMHPqCb07q8gD-G(yQi+E@+k|0Baa7*TEIs7RWZ9}2C zl%+PI_dNw=0~7wtQ*3NQ!AiEt!#`#6vsLU_-~R7m5jeu-f|prMBGU5}Hk$v;8)m=4 zE)=KLA4QP`ju8tUHrD(^JQcB{4gm1AG{mM;-VqoWOgQ_x|S7U7hegB`2&I=215B<6eBTKtEJwLu z#7V$nz_Xm8JSq}EfgJ}X;c31yI?HkES%*%4Q)8^+QHIrz(H=vmKoU;_2o!cS_PmhK ztY;nhav|SZ&rasMxMUlEIxx@<8`vonAAxvh;)^(sV?&fepqLuK?PhJdCu=vb?DME7 zY$5}+M(SX#qF#^1&rJ(iE8_UGTH^YERoC38StlDi$U7)0yh9$`!0Kr#nxx4*n?hY zn<7|foXq$H+69_hkql85ZDMnX9$wj`=;4D+ZRlY~d_0)rEiz-XqK8X2vtgizC7W5# zKCjXq+?o0l=wWw)9&ijn8wY1bwg~BegZ&PK@y9pVDHpkfK~{xU2pG@UHkc>EP>UXD zP;}t{2gqN;{^$)>7^(wVC?h|n-?s%~Yp;;l!<2U11wvJ;VPXn)A<#S9v?6cW(n=zy z|29c%W^Jla#~4xGRw&boexPE5eB|*hEU)B$Ob^m}lf9TEOpd?F&WM3;32Os+DZqw9 ze+GeX!DSNXP8yLp_Eeb|XF`BQanj-mcYp&1u2BSGAMbL>19dj}` zz#IQiYsw#ytGBWvXQC4xD-&!KXrn)Lqy~q5tO1XeLLKmsdImMV!lzr9w@Q4*rD;ue%sl#1CooIRGc&w zpgI_`{2>Jv;w!ljHa89~<^5I=>{kMX-mVc`Pe99(FtPXI=K0K8TbHn^@tq$0juMSb$Rdub~-yx zcK?u-@z0sO=|fi3^&TpV;r8V(kP3oz76u`%V5mq?0Bz^mA6u9+fjc2i|rtx$bk;iN2qI&K_^C zPG_Hl`HqqC49>?8-G2kPH-ucgpLO=uIpMeWvugq3)Gsh;sCwlWY`Rl*C)}JkRi*lg zs>i=zo#IuC4?vzj%jMt$Y$E>$lYc+Jx@OjJ+(K&S*ra#*NbN?6e;r_>sKjMovLgD% zzGOXn)$n|r7ZBB9$`ky@x*uXOT6N10vHom`tPS$^^7tW^%HEWzhgqkgSb1R4q4@G6p^h6GL99N~ z79bhbdf6PLYDF-!lXXOM= zSUEnma?k^{a%kJLo*aLY*Gh62L=1(jDie5s$qCyD0fj8o9N_*KyBk}{T7gRz*bTn^ zmdKV>B@Ft}*KCG6tJA|ut8!-bsJoVFR`-4F%xdO0imgujh7BaPTH~ij{nx-qtAWIqA7}xVhuaoviVzf>ab$08i!kA-{Nx1Oov+< zac&u};V{IyxPo@#WGjZ4*QA7bU4joTjPpUc;t0zQJBspjhabLk1pM${T>fwb$23rk zQ(S&{?swR?B2N6VlBVoL-uKnN;e9PmI++lZevf&=hOs#ZD619md4l5no9uAjzgTV& zJbp!j9khH3T>!Run%^qNcM=oseQ_GUO|V9JT=34PF2T|*CTl6}BQ|4RNV1JZcomou z-FRq(*uv2pj2IhG7F2kdyj17KY_Y7;`M}OdGcKe4=@r)U#VA09ks`>BryyDjm&j(F z=dxw;2c4hoRO}_kd3d{Ys)Q?M# zCjc!{wSW?ByYDd$Fo`SLokwh>j@QEti*}W&9?0cE-B9UJ!qli}v`$)Uhf%x@MzE+b zFk_j#&C5#);(eoaJ!h#JJvBsLG|S|xUfv}{*n|5iAUDcGUS2uS#SfB?;*4vxTggEX z(5%3>Hc5!5MFJ3jILC+G+G5$@xqE*RiSlNk`;Z5e59uB)`T zY*Z;N4jWAi!jnkx%VL*Ro*ZYDq(H3tj|_32%MepS&_uQic~XdHo$RU)^Aj9!&aXJ2 zNN~XE4hOs?1plcYxSSv2y*=I#IR+syB)f5ZG*ob(|n|AqO<2g42u zFHCVzct$qAG^3h~@;D5p4@?MMWxx6%))2zvK&_$d^ZOKoR6)&(hvO=Dm#j^?19~(R` z>o?hmOiAZY5RJ7#P(5J=&KvZBT_%p8jaii9A_h9HLQMVt3Ng$7|A`ova{eP?{_j!D z{Qs{breSRdex8uqig87MnVeq2d-f-e2TL|A(6pI>wVM)Fs8i|yqzeu9R7e@g3(zLDgoLlLw z?hwal5z?AUSn&EQ(n4=~xi_YhQnrFCr3^Vn1s(i5X1^M}A@wv~26YKThOAfWCawvP zPij1jqL72|=Z1Em5NN@!OQtx>uo@Hfz@$-6&K$_vBaEvIR81iEvcaayIOu6eyFwJ-Lc~|iZ$rEGv zRXo$T!O3f=yo&c_ISucWV`|B6qfRf6vx%h|(i&D>%>~PLa&cd;O{Uy_4WG}hZm1Z= zr!)OIkpeD!Ezgr_*Ycj}h(Z8h{ynA-rjm3aFT9oyXF~q%THcj%p}whHY8m7GciA1kJeVs_b`_>bV0`OVM$4S(c399lKH=H82UXJW}Jx{|^7V^yNd3NtrR0bpejSJCg z*4kc(CeggAM&5rtFFu{FX;Uv??iOSLU=iviI zcQ}Qd_K!5FmSo%ZTi*HKwO{payxzZUf0}oP)1n&w3a6ol^htaO@A@b_1^RM2Cw$?( zMyP4VK0OMR1LsgQrC`S760uL0_e|#PV{dU*>c_(t1(`j=IiQSNL8$XMUnBxd)bx{Ly zj5vWqVWjeqWTO;R+E3~d@evr=Jj6$Eh=t9fVeNDw#LLP>xAV(MWRs)ie%uv+$oBum z5!rYC1tQxK(=u=eFY!8cW&b<))meb;p~2PBp#^vp+1%jI|Qf-p6Uu~ac*JCi%^ z;6)tYAMfDZM{Q?VI1g;XL{J_ohN;)g*asj$_}@4@4zU<9i7Fq9(~95Qqzj=?>bNlq z)#!p=yaSEYkrre!>cF%+d89-&(*k9~0rT%$kWF{;Og2h>b|)VuXT-Sd)AVDx6`DLG zAKk)iOi~{{xtKfYCLikL@HRPeLKV*uD53(H$z}1#ld3EiRPnsjrXSa76=VT(($-b+ z%SFSEb+Ygte)`Z)_LL9M(nxo-^gml^P&8CZhDz}#rL z8@@(>1?s1+*CO0{a;mhZ@?reQR9QQf_wKS``uh0sxy3Em^{gW(cwRo9IWhfE;(Ra< zqwImHyd$rjCWFE{Cu$+fmu^|iTtXX2eXn}*DP|+rToynyiBY(UM?@Zm*cLMvBRm~C*MP{3vdCz-lLB@$4B?@jv(k4(}jQ zWYcWk18G0c=C_7k{1}wp2=)PWF=q3klPQ5D8r*nu?8<%OriCa1iMlqVc_Hf6$sSr#yq*O9Z+LMiN) zVER)YW?05x4A_4a%;RGL>Hc~AGknHNJVyur=FaoM8MfhwV?HmW@7Vdg#~-%g3I-^_ zy&Lbbj>9cGK@o)42<9l!?liKcS99Kw|gF84Gw}{svA*EUh3lD{%}hH!R>qIgMoa zhffvJjnXE`&nvV?9$&!6W0uD+1cSJ_Vbwx@CQH3pi8Hvo6F0tD`X1%C_-znZ5U)GP z>PPv0{xOr|{>Im&zXnyA$1uaNEu$>l_s_sFCZD3qeRb&aqtd&Gcjfo#@}xzGu9PDd zVW+hW*HRYoC(QL29Xw^}Jlxsh0}c?XQhOU8aA`fpJ9SX%Q86D(H^5N*0{4IdOA(rr zyyr1q&R@m-mdE(gY)lEtSiK?si+M5qp`%=R&*S{mws|JyDc86ykMk*Q^Aav-a`a-} zscjwU6Du>I+6Z~|-+%-?|(%f&;m_OD@i@PlQpn-r|nsI6434TA{#AUB1 zvBAK1)RVjyeQh_s!To;ce!D#7l)KdZPJfC|=5ydJ_Z08kYv=JYQ-jd3*yNMg&Z*X< zwGbSX!jaNbrBF;bvEya(NWZ`=8-u z1hm_;{66}we3p-=Z_x@qk@|wGLcl+EjP%iHDi%j?mid-0#N&2#LD(rhJ0)l9|$_xgLgaex$0%`GSP7F%X}a&Z#A4GPLq`x#oKBs z!BJ#WGZt|N$97T;?ZIj>2;A+sJa9z?8qWYDo?Ba0y|q|#uX!@UJ6rT7?(bADXMa77ccaF-u3_}Qz{ z5Kd^)ElyI&2TPn^6(?mGmnv{}|2Xk-SOrm%Zdnj4nCwv051ywo_s0-No?8{{jsRif zrEv-EX~4Ya(g4s=zwl0Y!M!TDA+)O0-7gy>cJmxEzb%ZP{|Z6 zM06~oJ~B&MFTjqPD2n}nKN?hNW&k!Lm>@B+*fmUYkGxO=mPQau9d}RF&<3EW(=W6J zu+Tt27#kVL8M8|jW!+jH8+7kSP;_axgOg$q3;Y0yW~`xbRPu|{Aqi^pX3dS2Xqn(8--rTwbtJ95NkenBAEmqWf-hc&r$GmjP=t!^Ztggf4M z(=`}~C@5Zic@LxolONhP2;oLHjra8e1l?O|(-w-9Gxxu;T? ze6*qek>a3oUZR}r`xXwxWe zAttJg+seCS$B96I2wICaO#7I3W6>l3gc|9PFnAN5b;1x$F(e-)t3Ov+@||Y>yR5}d z1cQRAWzBJ74X5qpBU$A6I6AQhYYU>u$&$~1!hfHcC`J&P-JHBn`Qyd!kpC81gv+3b z4~*Wr<4*^#3zA0kNBIlx`;_;}taq;8QDVZ&PZsavv%3QhNI+OHoDm##Zv@^8Nflb% z?>^hdXRsPM;WOSneT{nJ7kdD39g+o7F8_@8mpeb_ZySpl=80L)lk+c_#1h%CpT8SD zE|?%Cq>AA#Bh*tb_9dkAdzJAMc&n4kzu-6V54p@gz;AHBSIWHy;7jm`41USa$L-5M ze2J`&b-CtC{*ABRk*adlS9~ZXdJgi@zEy7GxPyGaf3m5sJBZEILV5Tg&;6fR(@PGW zU`_x1A>QYN$2z%%F{_s|lU`|#+orLPN<7v{TQw*HDhXPz^H?Y04Yujshj{Uc?@`K8 zhk4$qacjS0(ubyrB?sp!GLBOSCVFKuSE+H6IVyINkN=DRl_HU0;FYTZRR zd951%j-U99A@1(O$pde6PzOEEnZ%Ej;>WElx){P~Ju3fJVxmr!#Kxmx*)gcSvB$sx zhC!N#@Hp}Vx^;Y2PeZ>!1! zi%}(yL4JA(qV!L7a7@+yB zB61G0_}pfZ^=|>R0#}X6ku1@glW@#K39^ifU$>|F5zlCa)3%fO9y*B}mJHWP=pqzc z>Mq9d@2meBTvA+(HD1~6NNYpM-HrFc8B8l~+mx0kUE3D>^8_7hZURMj2YyHGdy!lAqrl3{D zK?5J*xq^_msN>tv#jF+3@O@x6uI*K+hbZ$llT$*XsEaDoLKbFBv0DajlhDy}Gab7J zER)J^4T+L`2Ob1BkC*flXJ`1+V1z|E``vtWjSgzjtWxsgHfjE zR#6U2mR-`s=ET*02}l zlyq?x?mfv(>Ecu`J(aRR%xfpEm(LW7`^h53h?kHer!+X?+(S#m?c{79&&j16@|9Q0 zpp{MB2sUFlM38n6FXbLQq5dx=qFXX#bqGYhW1l&6z(4bxckF{>8nY~3sp4L~)>0HCe z(&lyUO+XK!F`g*hzUeD=407}p)pr2HL!2x`ijS8RpGtvVQJVWI6Je3wjlzpefEuq(k20z1jq$+Nc))o}N~V7(@&{s1NHFP% zCe;!5%|8;|bkIs&GL-~(%F*3>Z2ZBAaryqDDS95gF!0>~(7<2iGXq5j{xO%^28t+M z+WdN;xD(0a28mO$KPI&RyO>Pq19+u6W`^@|Yev2_NOVJyy@SN%baVUk)5R&|);;lb zaY>hGU(#fuK@_8XSOv%`!K_6!*l!4$M_j|eZ94hY>Ea^aVjK#B`N;A!#Na;8V{D)& z+OR;kr_;qa9H_buoHy4iIP^-P182<{B8S@Ed4?F@5f^%trmUB6Fjpc@rb5sdjNQ;y z&J<^IbbslYqA_zfN4Kh~HfZOOKLP6DYL2Wc7cb#=F679w#MM4>Uchg8$k)yi7v-#` z2>rn#x9+CRWWlWy`{E^o!D6|uZZF1xBZKXO#R%UDC(=+dMD$@Pi?H~5S@ZFst38$H ziw=F4A<6YprBUHPN>?}Mt(rDDJ8@QpV?oODB1^t@z8DdO!|Pv(xM5z25V2xfxD}uw zKtohcQ1axVVhDLM-8obYQ6AJh*ggd z#)(!o>_tMMwGSVa@B@Byi8dL%>^mlz#tO~ zmL9xN;Hq#cssgxqTYY-$@rH|1Q8k(~TO-iJW0N7)+|A!PP(n1?K^Tez?OGD1f zGXD~hpI`lhdm0K-w{ZMIA3D%Px*Tzd$QkSuP{*PwuT2pgq$YAeCP|eOISFVhE)jXD z)juSTN2!JPFAd@_2Xc!6ro4NtVJsMBo@Zo*49FbfK=74@9`P( zl+hv@xt9nCOTP$$8TnXeN3hPUe*g*{MJ^HYmeJy@^n3C88scO+c9Y*2ErzGpJ%BUd z2$s9a@ry%XO0wIvqD%aDdE}+nidXbUyo^a!#u?cdMGP+$jD1h| zXtvi#!-l@pI!&s<^XBj9dWj3>Ncjc@=0xc zn(pxc25?U_9tGO#*Ok*50@83?zaC-Qq_(zE=eAKA*Sf4%=O#5mIs__WG?Y&v(9MUs zWH*T7805IcU__=X^e`mhZJW;32Iu|(GnQtXU)^G_W>EHfTk zL$Tg&>`&IW6zvhu{M(m^6~$xgDdzT+S=%X=XmB6J3gRWJzCw(!2Co5*!x3_IZFKG* zVXA1E4!uE~q*mo!H#n>E(HlfBm9X7S_~r&NL_U9`c%MHe{Kc4 zVs#CRD#bPyZX|K1c3GkA@-H`u{v)b!V2<6#I`=-WKi|TSaG8GfR;FutO@S3m(FgiN zHWkMn>!w;y5rCu&DaK)OK252xUL+N#6B{h;G<<45UMJ5TCwj&1z_}=%(orA`ia>cK zeO|SpX?htci?~K?Sob;2Ae`y6AAz-5BY?PAMz^`>c%9^}sfDV0>zOgn-Rw>_9>}EE z#nO?EtwR{c0mt#5ZwMp_c8T)qaU#cGMP5}AxRe^>MP4i&yO2mbDxDb#(kC}!kjSl! zW&=nzT(crMh?|wsOvE!G{UaeKp6YNv;(*RS{P=)bnd)QsB}3gquQnJ~>ACQ&%9MGImiNF3b!3fZaURnr%@ zte7%wrrA&PE8IoWoH>i}GEs&U?3hw2fQD#>Rdf-?nSsFr`x7Hs2zJ6O;)A_0rGycT zUIdqi3>fcK3LaCnBJ?{%LDL{0&Q#_i8g7lk++b$4M#;@1qoOnxFKtYk)H>b2F)oll z%`1@yO?a-Fa2V8*=Rb|+KMgO$Vg6Gr{|WOC<%xc7C7BFj8a-Z1vduzl ze}VopAT~X5d;$_bVFrjcX<=F{65wTKCf+J{TvBr&>hO3m*0T{Y=z|4k#k5Eo zmJ^;I3t@Qy9`?O|LN)wk3Lub@no65mjmQvbdL)Ho%GAGDBBYA@F?F20R8wdggP>=s zodP7|v;|3us)cno8fB3@XVifqF`{`;DF}*42N(L?bsBP(YsNV#UdjMahwkwJANc?f z-K(IYdlho%UIiVxS3!sFRnVb(74#F?Yu2riSkMZfH^0#|xoG-T|JB%k-7%t`Xb|q^ zD3V7fy1|N2Bnz~h7751@WTx~`sL%#GXkpfw6M_1mRX7p!A4A~k7=BF(1OvLs1$}uZ zo5mz+PL1)*iQr&fc*{|h3B*HFK=CMOplEoqQ;;T=sVxP$v4fFnNop#eoqZlze8TBreysy#tz z4{SD{@e2x6j|v7W z2{rCW@!)AB4Np**sQ^YrQ%NL1NT}1(RP`$egy&QX1`f>`R|bBCNmYPt5KYDC_lt@Y zMsm!|leCn9kQ6zXW?Hyu#4j>SuwFZunSCG`3y=l5lL@JKvZf6}ZjM=S1}qjiW`}+d zO?X>4gfM=Zf<&du1%jZo>{6}3SB{MJb(ah&90nOz zgTT*%0CHDqDBa3I1a4j6I4W+YpQLpj$ox(NJ^Dc*0%W*+^#%6ncUDM8Plp0crXOfC z7*aaAk2Qrt8nA&6A+|lZWD`^yut}q9BubC#<(SbftP_4S5!^Em+%wP2f%V8yPTC>f zPBC1INBjTY4iyHF@d#n@X~1I`jdd4VB!Uo80xvTo$Gp)3v!HyxFBYZ0ci$B? z%g?+o+R-eyQ)N^=?x7rfkQFPWybda_eB*;Vj{ELX`R=~47)&&ShMNUvmVou#cmA8A`SclEi9&_9K#f6UBIKy5GcJ-zOdnlc{k@+! zSJ=#q0TxQ*_hL%5dw;N^VUhs>Gh9ha8M`wmZq_V;!&@tcuT_5xn?)t&DA}5yh{gd_ zQr|*YqgiGq%}5qnqk}#Cyc79FWx;(=DF~!q2CTz8R%y+?i>|O|;Y2IH0@-7!v&WC8 zRa#T3u2DIq+R^Dy(yWxp7^FUV(j*kXTv$iXoM`PW9gIjiRgvq&#R8ZTa1BK$1SCx@ACgjFH*Iswn5u*olt$<6 zq+bHEszl%nQg+j=Z)mueq2nXAH~|I-4xX-5qL2) z*5bFovZBf1O*}JJ7%2dbTkva8e)I<}3uMW~R-9x3oVv&`59dBI^!m*L+Wgb6ZGo-O z*%GDcO z)N#0l=a8F0^lzvOfpFlmynl{rPdnB6(;2D;H*s=iKNCMVs>(OgrU}EvcpNux@Fggq zsOxr<+|j7pc))y3)-%7f<)#=k&RS8f-O{|M}_rJH3(jZ7(xG=u=@{yWay`R zKn$3@Fccic#nicqAPT|HRSwWr;e|;OKUUpm2@(kq6cbW=T9Zj2B?w?j& zjT&qkkpU7APMhwOqx3?AQ5}-6|Mco=SmRo#&u~wO1WBVdb?Z#1XY{!_3R?wCE7&7K zWFHHWBBfjXCtCffK)}MGkwt6+mRH=fSQSU0MTiry+^Gv1Ko&*^TB8IM#=|gC&6(>) zkz9YXTOU?EDvEcQz@ZdD)T4vtbxQe+_2b8=yCBaa_&SZ#g+VolqY$^$u_pS-AHrX- zFW(BpB1v_kFW-!nV&L)z`Fn1LF&;l#LdW8G1Plv3V8Kvd>@1D|7$r>#{XVdobAnYT zVg#%Nt@RVr2{70W{U6to(4%@v!TMMzfJT@~HSAL$qLo}-j67O=3ITA0-H$j4T3n?C zxkJiEsJ{r{y)wuu<)w#vX}Obe!h`8j_jwSE;kpq;5zPJbB;1|FxPdT$rAfeBnxJ|* zvj?EXg3K&1mO?9_W2^vO)Ur%`fgW&<`7XYxV<2Le1rZiN-EItX3>{g~zLVmNA7t)~ zA8T1b#H0e^eg`6nc3S07TWEktAbw7$Ph>ktb{9AL1{vT#I%d-jJs5lqeLXbX{c&PJIcy%jP|Ki=NKFJM$g760Xq zUW6q|zv0rZRgi+Rd}JGgLZmXGYsb(=vnwpY?MuYSa&~7z#yW%XnOY?l7)q0xp#&}z ze40h#_D(HCm3VHg+m7Tj`HEqyLxo-03|A7TIg-o40Woyk#Si(o^oJ@HA2G-x>Tx)% zUpa>8@d(HvA)+ZwO+Mx^o{t%f2QhbUkUp3vY6a^7oIuT347`hEIHsVAgsusEm~q7e z>846|rjx2*0#*5)io>_cd9Yhdu%3|nBmrPFj>ieJb&&i`Fj50P7$V5@dy>D!5W^9Y zo|w%Rq>|wviy}wy_9ki0S)_dd6oF`B*n2|FHc2~EZ&WJ118K@0t-7NkP-&Icfd~n1 zI-?|g!TGk3I}k)6To1wU}aW>S+70|%cj~^>`?d^vrJE2VD2ZW&j zKrh$TdT)n?OxnBGN%TP}@RiRe_71JQ7-1V>a9D04JQLwMgmV$Tzp5%$g)u?7q~8jJ zPKL7yh$UD;l;|Ql$5I9X4Tb(o8Yq4wVpx7eAQ6*sE{rJc$nFWFI&G&(b;+e2EcB)p z7-69OemDyT2&542X-5W8j}m{FFPss)y7K`5~Qua}+6RD-R za8Y5EI&2J_A6J0km}QB{m}xTc1(hO(AFF0y&OxilI)=ubJ)3~dCi-lm3fg+%d@wthHqCT2(ri>K4#1PSNg&Rsh*q5Gf|rAVzD8 zY6_DSXI$XXXmQ}7!JQy|s~=L#3TrckYXMOGZkE%F{?4jVZ8%|^Cz^D8gIybKqS8sl zRh|_s|A5cPKjA7dg}d(8*lywFK5$_p)w$A%we^Ty3(* zZ>*7Mp%MJrxqB(ACWy@TRA3Eg7T!R}Q^EqrpHPaPmd{LpzvXiI)&y}%tnKYfEo6N~ z*D*7kX05FhsoH^F?tyi!MQ~qC9ZHp9vd~R(w27i$pmO3#=mypTd2(2}W1*PQY{99$ zAc~!PkrU31LTxfpk#1dL?gy(%V6TV*3Z0z67MeRwRfc69zZ(m~dV8ZoBuFMAth6pP zpx#s)bcr3P^BB7>AD$>i#=v)wi(5)KX!GOfNWG>{#Q~NM;xq#0v(lV0lk; zJa5#+|IX0SOdtOGupKu_FdRD0LPNyH$U_KZ3MXmmp89zg!o1qBqnmz9PMib{6;~sl zm?W#9=_scQ5+B7f3xUZIFK|%dh38}Eb3JYtLnLR^fTp! z5f6UoWZTW6cjhS+#GCl02Z*3u9{mtS)?_hMnGcd*N{|yKi@b)HZh`-rEV@n9{H70LJ5 zAv$J#gX=^%TP2mkN~Ns@r!sS?%#r_G=D;1oWMgFLPVpDt^901WP}bZjE@RUg{&}a! zWo()}ewVo9|J3#+fK?S&|96@Dvb+RtfGi{-ycYt5ec!iSRzbxDv|_D_h{_@Y;%<3C z14cGcmO%w%Q$RscuyMf}EhyrKl~z<#KoHcZ*rL)F_5b_L-23tp(6-uR>RHI=yDN3*^Fbn9{-c z?wfM^7Mj$JzuUf`9>`LPS42frKzHRkciR^+z13{{0vvU$xMz+XrZMRJInapUFa~~i z$jj&1Te4Rv*VPu#$<_{-Fch%vkS*uiGh_FoU(?)IK~P<(PEFIUf}`nGFexRT1iX(Y zRn|;Gc855UpF#s3^G;By~n=Uv^Q6k$fSGiLVbWDaM-PAfjV5#?_Rrg%{1^&APzLL zopG;yzS;2*jB|hypb;J)JuVF1aUF@J>6rhdPZ$&$-qG38S!g%6PRy2V7s4(Pze5(< z(bS_lBMQi=3+*mxgh4#;pSw+NSqK?U$FTaU+48_bdtzt53~Z15*F+(iV}x+j3VBq! zBA6W52C@{D0(&!C&RAq;p2Y^nvsk{E$*R>i_}}E0IpNpDtDd>`jE5~!*|X^E!&sS& z@Q?D~BKx>GRqkDksd2!P))ISM#{PL&^j@Hq-!c6pZ(o8*^<9=cChg4nWci@RhaSi( z;o4^uCgHXiJ3kksGe#o~1$-Ug4>wg*F@?@48>wV5*E>=6w`l@HQ&+xMs7L!B_KU^7TB(fTYe439)#! zk0ii`SgM&2|7rVMaUjo)_k6IXwap zxS-aKLIj9fD2YrsUGCuk6-6pFCe(6AOr)xUBDrP`iT;a9!U&VG)aq3Gy+niMoYrNY&I*}z^$^eC7?%GDa$az+;798 zo8qpQZ6C7l#gu|z)*8c$?KDVTD&1E$s3eX&9Hq8L0tE`7G?r@YhJt2rsp6)hvd72MNVE|hiS3#!AlD)@lHLU)rO<&DpI}8@yv?K; zs-mQXobhqwm|efpjWcKK%5KDwD2qtI$yso?IU2OXW(tD26Nd+DCHO zLm~cg$W!4-L{r#Ru+LIg1Js%n7)G*rg((reWvy}4Iow;AQx3ASl0^gH<^PgVo+Uh97Ovy^;|TuQWgd>8-MJlNK-?r+2PcWjDnXFJEQn zXC{&Km_Er-^aZ|*ZV zMD;#33NA_ft;Be)&b6Z;8bQzvhyRJr_xG|*O#v-A?*luj#TvQcr+MRLKFG6 z+Gc~1GDMBH!7K(hU!Kln{_>z za>}E2lg2;nKp$H`YaH??XUQH@E5F6_BMsz3*&;K1=e_Yv@|;KQ+?sW?c{iaBAPN~4 zZ|BoM%f#dOWID+`$Y(a%y^{iA``gFutfWd`@~-oQO>d^M-zIx_qd((Ic?OnpTB)z) zgxd^1>!X|OK}o)gRH-TnT%!I5mG~Y~<-?Dol5+oCovP&M#}L+WoaR5MiJY}DQpP^gqMoFc9zBy0Y149obl-cS7r<5rOvfcjF ze_iC-E%xUwy2NYgErQ=bc&ReNC*@UJZ7*~0i!VdC;!49md=hY|@WNL6>ejHLHz_qh zgX!+&asfPOVJDNpGeJsJDJ~Vz<-aV?ecC>^kuqi`HHr^+k z7>$)nD(qXjAkNVrAoK^op@3T3yrcVG!cxygQE7dY#)on4EFlLzWA|XuP>rIB$>lUX z=@*5`wW^DJJL6dNsb}nV?b}C_w1OiE3XWP{B9WPEBV{n{I?X*|%Zz93HaR@7WGnV1 zV%CMw;_cBVoaBYi+7DabnDWPG?JJYYpTX!oXjfeGoIO({ov2WdJpM=f{G_8kaQ2J# z%A~zP%8T}`O?DDE4)qrob8medf-LPlqak^3r9C>cyhhSeKPl-Y`~&{P@`- zFaaorq=Vtf$4~O6h`p(QWL6n8>)syb=KgB_cyI3!Jnv^Y90MKpvOSq@>bJdYH|N<4 z&^>N*(ihMwA^-le{d3E==tlzN8eQ%mGA8>_WQ30=nPILU$FQ}e)cSJcLeV%DDl!lW z0|o(J6}N`(t<*8W?(7jcpo5(Dum)>eNxE>saL{Q5UA_DiVPZ| zl~4$w54*@Y5`K$d)d#~t+%mOD7}FuBiw5T!cJbT{HWrW~|b^l1WN);qZtnDrVK8LCj}=llsykOkl!(;)#|EFWGh zI>jjUBJbRXgS@b)fEqJJXU1+ZN;>Yv_?}7(sDr_Cl%lx~vV2C5Q0cgBd|Ct+s64BD z7oGiJs|?xTWKi$z()Jz=+)u~?LmM|tgwnfbk)iyNq_KDiEC}kRcnFm$W^EeHFffSF zABgi~P)~t3j{+A#FBy+5QuPLkQxLY=7@N`cv8E?e^ z==X4E62pMTY8c=%%5XtjRobYDYzU^JJ1{WJOyU3{2B!W@j{llACui7J$=RvKm{54H^)o$DtN0`y^mE_o~4~Sx1EO#Bbi(={+^!70`$UnYn zUosM9l)X)AP;%=!EMa~wj!`f+I5*r0CRv?`CNUM!eiZe9y(hrRdiK%|C zU}GRe#hZbxSnaMK_B9aM)c2`+RsA&AAemBb&Cs-36$6P&usfg#5YA1=jj!3w%`tM% zYkCOGU3MmiB4?M~3Fo@c+GS6zgT5B*>qv^~@i%taE#%9u+qY@GIMz$>gYmQX^1L_f zrim$-1zh!UUVru*cF&Z}0*WQ9twqw!5bfm2H*g&I4iNJP>zT3xZm`qm@NRh1zR)^o z$mkl8)ArrDm0TJ@L8eomWO_e*4mw>AyL=?drg!%j0D1l@95vGF$V=a{8#O;hvd*Jj zxlmycWV%adg9|`iH9B*PYy(QlR3znk zR5PHgHoZH40GP=aL9v3Pbp%rgQYj6*qeCr95Ey{IZ`(PI-k+}qK-H&2h*Pp;KA(`q z;)yBjavO5SV7mb<_0UO>xQK2k7(NQtN? zTGpw}7mZKe#zf!-Pr4nEM z@to?Ht1f(>FIOFmE;POG<886S7XV&F<5o4Y3V+uTB>HI#`y;`+0MTx(Vp3?mik0x> zVjx)I;H9Firu(u*Ts)IAL=Dh8G zmllReZ%9$l6@IthDf_--H|fF#DRQCO_g%a$HMX0Xs^+vB#McOt9>^u{*lh!nmYYb@ z5}V+YJa^k%)!?8Bj$HbV-8x?S0o_N6AfBf5#Y<<^C@paE5TBrq9ct#l^mdX0R+G?Y z2`Us+r+Ysj*ch!=&?o!`eK1y5-Vc>Qy?5=L7_F#2~svew0`WrNjdJHUGM))~-&UxvZ92y8PD!!QyJR>KioC89KF|W`x|CSM;7q@hx{z)sJ)B^B7ypN zJ8PCyL>9>J&3FuXz?(5oP?SY5COhF`XRm#4x6?_J#5d)y4i?E+n-jA5!ZDV~$KSWh zu=IBN!0zdgdmSaGeqeXb4irsrmZTL;^rG^Ot(>QHSBj-yaGruMI;#Am5Rd6QMJbl5 zJ*DL5uyo)i#eExS4BoRR+@{FA`|JsknIHA!(z`}?KK}k?~Bc`4?nUmrBv1HW4jrD$9!zRmsB|qA{R8I z-Y52?q}@L7hfnOz%u)3Tl!5ro*l!MfdhhR*==-(<2 z9I{)>OODuW;1Q$!7g)MC9kC0%g&$TDpMK;Wp$0W#rnj0=NsL0={()a!Zurc;PJZx(eG}ay zjrh{;A;(q0R}!S|$E)miX?K)XqW6txmN%nReqM!s`ctWleg$_0<$u8N!)MCyuk6i=gve&S%Mrm%6T%GNo-`E8`_A*t8>?tZeC7zzp^z(2aGF2s~ z#*^iF$KYrOUpm?Uy~qE3-2Z*k|E)S^ua7svg@1nv-snUqbWwWax4M?s{ogPA-;}@U z_+tNe2!5mSPK)5P&jV6qtLn0=tDE^ZdrkahIVx3~%c+-@{9RXing2W8|6T6?Zu5UX z^nd^9|28~suSxRzQU2+;eRI-M-EFe}clI4URkQd-**FS`1IZQ#T9I%F!STc#Bo0Zy z0Vc_xzEeXb8-8!!lyp>&l$77w%hA@!Puo!?+Ec*YS&p6tp*UiR0T=&dMQwMBC=@vJS3eq}wh;1T zGvuS<2J)<1K^d&(Zb(f{q6NyztX$8~JE*7wE0SflXcn#3h5C9=kN~QQOmE{yXa*FO zuiN6|r0u?jakGG;*Ou=J(JT$x0c@>9oKJN(OSnzw+51VTb3tyoC!sf@z;I!WVrnlEZ6 zLf~ki4We!rCPMFp#N{cARW}NEuq;Uut?4apQj#dgT?{`YiIL6s?60KKvGf4;4XAW{ z*fdSzL42`+s@int%X$4puKb-Vip;a*9j=&ZZ5A>mLtJX!A+O00JyQd@x6IUn?}r)i zz;%Z$lNyMjks~%y3EE80;)xPGHGl!1-sa+!;&O5WF*xav-=hn&MDySMne8&&HA-lJ zc13BMBN$}&ahr-X;+!C9K!V!fgi3+j$yT*r#cgTt-JlBOQ8GEmC?t7QAhWYYqqMD9 z9JTpg_<329Er!y6G&%1t40&I+C>&Zf3tdkJ%NNX9=_1Os4V28U2UN*8Oj*yA%!Cy3 zOVlieAC^C5i>5L9Uq)PR93&8k?Z7C_s_7P^ z1SQEt5pHgEm3}+q+8mJ!yU!PM#Mtz^@fvlr5t?_u?9fmY_UcO85t9{DxemT>_eU6l zljOy@VS%H4^ecFRk=^jZqkP!RJtQcT7c>;j>K-BvVhDhXSUiQ?(NIjl%;=CSt}yq= zXr5@^Y-!wikVVCAQ`7@xEcwiZF!s_@^8KZ9Ql7}O;1F|8p19ClC=cd|OLL%Hg|ZyV zRd{|ha0CXw$8dCck{p#UF3W~i6pC-pw{)d#2y~Y_aVuNCcqgs z(zvYyHKO6YV#rm6BD2xFge-2&EIV|T{KPC2d?RVC?BpJ*mWT9vj;e{T-&05B0HGiu zGU(u(@5%o$Aq(^|Y>GbdYIt|4N@9}os^3_pqS?F^L?@Mkj8f4&7YCJ!W;;QJWJ*P| zo1hW}mAw2h!DN=zS{H(LTqgRQpdhGZqOS-JP<1Jx=pTeB=V+d^9ILWc6)A3VF+uev zASO<<;#2vDFO#=45&OeS=X)>+YtX`8FkfC-Bs#lmF$cW;(4vO=LxbzCmFtVdpEFe~ zb*nfbf|CU1?S)Lbr>V%StCC74=oCb5X)3POk+h4@l~lBACU)A^$`9o~T8J@qOXp)s z--$nPdg$FXUruN#&d*ij;&Ll}x|;jm(yB`R;c{|H2+asV#BN z5#L3%Xq#4I674saw-VRqz(kWD%*`?rKBrY*&;yq&ZY>5g*W}isTk@Z5jIHgSC7)<5 zdNiA9(LR;KitEMk!)1@t0WDeqv{f1oOuss9M0>6mC2fRPsOF|#Cpb5O+gxUVczE;| zi#2UTaqdF3SSU?DWQ7~SZ-NSg4x^sKZNyc{Gd{z&robF`L|gG2b7sY3Z3WD6XUgsE z#M>zwnV9UtQg&H;@fWMk)+2!1dxsDW0}>hQB(l!gLtvTU?pDOaS}DBkHDb7xv9(4F z8@sGV4Cm`y9U~8Q63w%BL2koWMA5;(nJ6|nfr#kD!p>qq3$^)5B|mBTGK`geJ-y|s z9>7T?zX#@b7Ps}nX;`unAh+J4hZL2dNLw-iVtFXM*f*Srua4NF!q|ZPgNNuX2X+z7 ztX-Cz*aeM#SkCJr^3s>Qf*EML(<6!|{-KK)s2tz_h^(gwm2epk`UYDmugS z?2TQ;$g~Ao1^~I9kE?e)QQv|D_+vNGPqykNIzd3Y3_qza0f8tgz)B$_B8JL&-9?r; zU!Lp+WLj;iICHb?+Cy}RtpwJ9@cQ%~U!Yd5z=N8iZfYq4y9neWjDeEa!rE1^N9^yM*X7ou<(JtkW?DP;XG0N^Grpr6~ zi0R>FP^W5#Vi7rctKCSp=qqx6VaI(`U(q3Z>C0R~u=oUxRg3RDxvH<|W&bOG%yC1jbso}G#*SI3WP40;?)~M|1TiG<4FjZABpw^ z4;&OS4W0_)a#b0dib}(n$l>tqTE^IK1lj?r?ZL7gC8NPrje2qgLRhGthQy|mF9`0!nJ%M-0=vWKiTWEWS zn8CqR`qAViu&8U$=Zs2KUL8z)ao9E;?LG7UJ?}VW*rpr`hnPLT6c!{ zUG`#3xVmUHoIda6^H%&JgO)c`TpQdZIe(}a>@PgFUU=9@e2H3ks)ma8GIFM9oQC7m zG{DAJ;7ZCf&lHnWCn6JAQvp}XJI;hfA1)>Wrd7(SGsSb%VXZ$)^v~L)mV4~>!m5CX zN?MGxIU~f?%yWE%xX=3Bl;tBuH2V=CDP94~MHlCxF=SYYT_jeH6#Y(rkOXor@Q5n6 zE*FS4@q;AI6Azoj^PssLzVLTG*~Sej(|d~?3%^Tyiz2Jakbmqg+Vwlm^_#8bS_8@c zsGbej(BcF@=eU0X@_WZ{63Xl6?!hzqKw$^)>q|=x>?7JV*rL{6Ridsg@T4tQUm%KN zigh;%1^2CiMMl(#3);Fv(F;UJNOEY$x0sKl3j+Ew7?f)nCt*&hPvVqdCYnYNF$=|n zvoRlFy#b31d`1$yNSvu^Efl!FX+p$4N1bzG3Pv>vmY+(lKv_uA&>vXSFs|UB`D>FF zfXdOlN0CFQ;EN3HxpPPBm9Q+^wIk$Y^^Q(*q0`bY-;f-3MJCI}!0T;6}77}l{J zjB;Nx3vsBunTfHdIEY_}gXD)X45$sOYCX8;^tSqpg+JqMFXc-$Nij(b-V(Eu-!hj1I^VE3=Kpe*Oq7A(lA z-GUXD{b~z*;b73^r*FZrUvEK!)3l&L?H2s+@L<6kYAy^p{apBu@W2Rfv9x3Bm9{MT zohXn$UM^DQjpIbGU!Eb${2B83xYPGcsczRV%+Y11nIS7r!O#C)&usqHvH6dBMjn_b z+I3zH%FERWI$(0E_rnrXEnjNIa_>ZHZ~+m>@P}9su-wam*NRc~?*l>f4avZ@LLZ5% zUN&DV+FD?e_FpS5^CNnk3G|ErTSH#pLK&v#ymOS~^O0yiRnH{U?j==lAB(AKsblAHfEp zS9dEQjus~oZ_}JgveEDXF^9``dth?-+W6@JYy}_^T$xp^2PnHvoK5b3(`4v0pE(Q- z3APVG_tB3Kq_B1?R{qaSXndN4Z<)~jGzl9%L&8oa3Dm0VTc&)Lm=d0ED&dRX^T;{y zaD=h@x@M+2;E2@+JDuvNqdLcrZ4iB|qfi~tMMW^)3C>iWa+oqW-N_9To};9Me7b{^ z4TE;y(!`*tJ0F^PKD+Y49c}rMSo>-{=AS+PZd45gddxVOE=JQ;p@c!-cfq) z_2PT1wL_+fjCgL4m0iFt*b!Xft8kyz=6<6IPcA3@@OLn|P ztjjo}4o*67HgtTs;=SY-w}>4hheaAFeM2OTevIp4K5es<5;BrD6cJD-dRtkqvf^gx zvN%Lfd5hu@bFS5pv~Lq*`slZS1$h!rUIzH)pCjYFZ8(15ZD-oY;QqXo1iN{M7@>Uq zl~HemfnRf*xF2`B%D%UYvqP*FM;q?FU9>p!f(Q&0=>nEY02^LN4+Tx|{IX!b;d<2D z9*?K1X2|^wwI-?_`?E9|CrPFAdvO^L5nc6rQD7dC^F!e7i-AAj3uT?@qJz0gmQ2TB z0r4)HE-q~8??_e8Sfzl(lnM5u;wf_PbWxBBF-OVPuZblwk=H<3VnFIq(M5XWX-TQ* zZ0$2;*HV~E<2}~_ZoW=wGbz$31)@IlN)Q_fuai^9NmRCfSE(2X*K^XHA#$^L2#Srg*&)ONHX}EKgwzZI0;T2)e3~{c7`;FU!%_Z-Avw zyaZ?b9ir()KR^&DwoZnDubQ01Pf?Yo6+#JYo~gYjf!aXP@NaXUt@KIU+B$NFXq@>N z7~W!Qt68IVnRlmX*z&YV5P*uUXH50t>Z&fiQ#8Kld)3gY(=_y0xMo9-hoOJ?ce$%; ze{!a%FOS|S(k?oo`y!=AYgLmw^>jJlf_$LVbgM8modB1Z!D$V1o&8N%aUiLqg? zoT{=YRdrgmW`J?Uf6#;KGrE<$D#q|0S%7ZFi91?7vYYP}jmN{@BZUfz6vgP_?76pW zgWgDhB?nkPbcsS0wU4Bbg~}J(&qYH=ux+^S3?BLUDLm7qjJdtXxCPHMB;LvO`h8E{ zEBe*;ne)@VSfhu@_V;0PewaM-KJbbV&}ZEzI$TUm&u>BI@JaCvr>$?2OAIvQkOBa) zi@@m9bRkul1>k{vesCiXn&-_YpHE~VpKr-J3q|j~cYq?%dk^$4P^co6C>uUozY*>` zOm-qU19YxdM5?_*pB<3i2$5^!4m$a<6nS!!6m_g4rhGw)?9^&5fr%}w*G(?Ne!TLw zC0i~P?Z^d3*NTX|ZK>$yt;0m&r$BwRAJ6V9+WyDerxLJ>6kvNe--wU0cZgl5MFn0KqUH2k4cEVr4!p|00G~gj<`(g|mk5lA`xMM}?iUwvZnb$J-kRoe z<^#a$mGZF%M1I`^Gu0qsi`XJ_CmTE{T8vnaF{gy+OONyIC})vYbO((D1vv{n$))LN z{IbCIAiN&kb%JyFsPx1V#3i0-@ z{_EmL6N|s|D~%5)G+wq@4s^aop0QlCXfT`06n3H#J50`8E=HNNQ8*Q^n(JAkCrpe*{Z zh~nTuV_ff@**<0QR>H&slVr(S(HMTH)R-<`E1F;`Va>|1q%A*OD>@ZOz4OV<%^IvW z`SxSBZ2hokmi`@FFOV2;bZvRr!;qMLR-qIL7r6U)~v zt!2~H(kq=K-(Qctpwc<|3OSq#0!Ip}!<5_&uqmSvC`qa!OjAIZX!D5(?vVQypU_?S zrkqibUKn+HWd}+0X^f@vMlBkE&fI{botSgvjn-Gl+r7e7&PR57BBx9nR# z2e9ZJj1^v`lnm%7Y|D8o=Y@t=eDSzwZzgSI&gVr%bh8*?W^N;-v=OF<>o_zd?|xGJ z)Nuh2AN`LtkWb$3?~wB~n)AN-FQQlhUKNfr7+?inLSoqB^*HLL9rkTH zE~vq=h(g015wC$yx`Gk$^7!bVIqyaghfZQsiLaxk$11^p6+NF8_c<^ic4aMZq z=R`9zR)K!GCXC-(Ujg45tK5$-R>t)i=$n0i4|*yMP*nh-NrX=lR(Z)h7klxtX>J%c zYMwuC+n7y_y+5)W;}QPYFN;SE_t9f+JjO?$lYa{;S%VgOx%y?QRx-K?FUPYFVda^; z6B}x)1mA2vX)aQ1JruK_?IJgs}{HBWB*RT$DdFCLhZ0haDk7_8p3w>u{^Vc&$ z1*%841;^jgqq}~W=-n8j3)--VK5!RkLz86QkF%5!e4+h1>|^=DcCU*Lt#E&9Vr~!} zAFousW?r-~{w_J^b+I-LQxNm5SWQ6L>kZK`2UBeP1X!i_o3g2}Mftj@$$7`=Ht`Kn zbZ+@^eYH=-JUiWi*^B3Q-#*R_)mv3~@`}6#!Gy$j)|fl~J+ixXdd2=X#93z1NjtNxySJ5#k zX&(#Wv!DG)cu7?RpiebF7H3-78+8(>3GCs}LvQF*IcC4OWAHW&s6?F2uAWSIj^ndR z#l}NIYR%~&TOELz)hc<_0nwh6N8Wz`_{VF5cN`F1#@!UbiT-3Q8I#*etO(to2$yI% z1iR{xLu^h512QSxK;sE(*I{t7&_(YAN9uhBMKvsZx#UBJMS95udGerW)DWyE^w4C~ zl>vp%s6$$i-y9SzGfqjCM-GaPr-^R)DY_2|n43NoZ}4}}AsE!*citg*frAor^C6)Q zaSb~R+fEn&K6hBONML1?$;y6v7)E>LFUinnVp#D?6Y{8*&jPnnD$4bCmsM&(4Ldmy zQE&cCGz)T@__A8L*WHEOT&{KQSCG4vUpKq6R_^`G4VAvmZG;EPVPA+wSFE$ZMj<|~ zs`a5lgPK~@qBOPP-Y823i-+^Mw}toOj}KxO3NZir5i~_GwLyZrLk{^uL<*|k13gpC zcep!+d=_~c?7}QHxE-~N1FJ;Eu$+mM`Z@(N40KdV1l?uJ$d{t9(K>A~q71f9t@W}m z#pu+{{iA4jv32wbx$8@@uFjK~O<;$f!7n>GRwY`rp=xR;6B>DYb-V#3Yq5n@mt@!g zla01fv~)KL`IjoO(mM8{eBvu`^T$lN;47%q=2phxonMLOy_Iq&S(`^HwUd$|!)I9L zbub-_Ma(va3BJnB$BCAg;HW=hV+V^02X}>#W50%Iq$H%M-2V+$k@+(Hm}ml%Crqki zqG=8Px^a^ydF?UGw}XD3Nxy0q)kDuP@UF__#QOrTvA#Tj-* zNlLcx#^KAB7_t4foMJomeK`&>B*iN?fkWLt&1w@q)4mSoi#fMT2X^mZl@IADM zVFQf9Wbd8*l^6yX2Fjmor;~->PQq!Lf3(~yOoCYwr@T5R2Cj>b6PRU++veSn8YNRQ27->9XQ+?92eH8E2Ph5MoBnv9QLb`f=OkA-0N*!DDnUA^4ujmr#sDD)#B1Rfj5|GazeV(s2)BV>;WeNHfvY% z{Isk}cUmSL$7W(z%aFtCIN5nNtOb*anVc8U(GKcqK>N$_H7cyS&Sa}_HAd-d^o5Cw zCsGaM#DFQUNrf<`P>FI;hI38yQ;d}|nm{^ONKRvP_I)PkM|(iwt7W5lPD5XVYp7(r zl)Ni=bT)z0N2{oRdD&j`N+mO5*`^ywBb*KBG^8`!hLciJOw-+u<%A?kp|~8B?9|Ix zZH9|$MmWn?-9EkV}Zdq6>(Ad{lKz;0k6KG)3%)u>*s%5<{obU001r_;oxohGhJ&A87q zohH_MP?~2sCDu_Gd1N_7bvDNp$_}Z;9a&E2b~uNkcri3eoyE)NrVv{gRX+`)YNdi$ z46H+vJe=h;k&$dCGo_My-D;PpZ?@AJJDG|7B}=oN!Pa3*?#y;tlj9Vd56W&iPM2<=5tc?xju_|xTBnc*ln#cMy!-;KBacV`BoL=E*{C%+ zPS?%~4X{%0LXT@S*dAZ%fgLN!oMG9anz~7_6v*g0JIi*V=4;l_XNpe}3v3LZcxgXnM7@5RyD;Q2KVIfz`AMW(g zhHUV{p;C9&Ox+nz&5HP6?rKkuTd73Ee%<&yr)l2@M7S=RmoQCLKjL$+$JIxDS0NO8 z+H#4@On7#Z9m07R^Ez+A9J#NNQ<8y0&ajTnkAzSJ9@24MpiP03llin@kKnWzxDF;| zs-%kxoc!3+f=1f#!|P@-(k#TDF29?KU+y#%<9xL`o1;(PA|9`1J=pMYvvm>JkgFoF z&uxt&t;f56pn4CL7HqcMe>t2`bX*p(zi>E`J-&W&eO+vRAYY6$C(X_o=4 zDw5RA(ytePG#7-vmCB1UJ;fZY$)uM22FlP(>Z76R0D_in#7QUA3S2Ys~ z91e#YaSh~b<52))*xu#A{>-@k!T#Kt<76jGeH?QDEG60lxGD~Wv(*dTma=i7lPyE= z@gOfNbdoRJ2>ZEs{cV5?g_v%X)fx%U;y??0zI>;-li5^@DOl`4-H^XDjmtXj z1r77pYzkz>AjmOIoheD%{n|fl>a=Rk+BpDoxx7OcPe#2Q&t5qQsoD{Rc>=bVX6Ly zrPTPa{1&Kvng3ynS~#uXeyFmAldm^*arWB(gy_Hh1exE`$>F|#*OpF6jGRjjIuOXk zKUjMBggP_(PS@bk#3$UzkjNQL6n6pyBG6bQ>bZmwWHZx^60(Non9 zAr`i~;f8ph_elNwfgyWv09|2m9+nD7X@griovo#Fu{QRg6h0_1pNrsblZ|ydB*@6dRz2tA`Mu;qJ4;F2@lw z2X-Ou-!0Fm5%IMpliNFkvX6+U-2^O()<+OMAafiM^2+v3&%%}57$LXp@yR>388e+5 zBd8O6AeOHpS=rwCtG)jRs7E?D{dy_oOLvGR)t=i`l=9pT1}cH;F80drtW;YFn#Gp& zJ36T{~6D^L#IMJ1<^iHl*5XAt=A)TEolD7HyvbnR< zxsk?cxTMuw3n(DiKh@{NGqa16(*Wa}fnlgI!DO2*&PC>8Ijf6vcHKAh7Uf_Zb9KC( z^X0)VPD?nGO7H3nHs{EZU7d5R%K7r)u1;CnPGnUlLp{WF>E;Yco@2tjnzwzvEbHc6 z(0}hsn6YiW@|Q8vWf0%Jr8;6KessjY%OSt+>ST8SFG-7UEO59-73a<4uVq`63v2KV z>Oj3R$AzPmG2L+)!qxJj?oL)_of?>{@YZwXPu-y$*zyS)2&6`DC^{i~^>C)S%8hq| z-a@|E!^w=!s+HG(6KHfTRMB)~{0SH9zA;(Z1@17YS+8RWb2#TJ)&I4_26n) zQ%_Z{99^!8s__c%37M`||KKt;19wG=sDOghS7xQcw%LrPTP2Ft7lyq;)kTH4mBuZs z%TF_qMm7{>xGjU-2r(gw%O0aR1`&0v5~v=aPZ%G~UV ztulu~#xEQkn~I-(2F^#5YGatOW1tS$Vci%XwaLigL@qD<0ja=!D&X08ZXmfeYO_3J zIQR&)b)Y$F2QR_i7Q8cCu-Ay_%yHN45}re9tD>lNn+cRBJOyd z2PX@_X{&56#)dH;9PpwJc96FZl9cw5KrBK=$XyeVo7eKd7CMF3X;>CIP|j^`MPmkG z8$K94biY?hemNIfQnA?5bxN8T5|rRV8!{ z92T%^2AUI8$FLWS2$7vA9)&v=?O`I4PMU!x#krlS;S6xYS`>veNMTF1>h1K7U5zIF zdm^XgN^kDrO4bUeJQ_YR`=tOTQuqYgn4nWP&IR97o!iBcS@a$5ur3y7b6rxVqbU{a9hebD>?j`(&lMo{bD}6W57!s}qyVnGJ5~S?ysoNQ_ z;_6rSvuT;rt(e@=*BR9E4iicNoN9$zC)h!&JY zY=v~LhOXg##r&`0^{N0m1S(w~QFlqn75D<3Ix5%Eb&N}qyqhpRBkSbD1nJ~7oWxon zJg8EYFg#ZLF*GpTrSeiZNzg^E_9bSxgTp%KNnoMQd80hTa~d~HrM?0VhSZ_n2o>Q< zyY9UzOFbttV!>U&S_+EVcMhhU`xs`Np}PQEyGYZUjp~^)=_8hY)k<8ms$Z3cY!Q>5 zj?hh)$)mVRD{IejPp?nVi7nzYs}jmfQYYn|M86b~#K|zL;CwXQxpsJ>LIoAkL~);X zk^JXxMZR)Cz>yo0IC5&IP1(!pr8S{0T*RpQ@AyL)C%WIQ7His4tf_dpzw?7>E|Sj; zgqm)V{B)qxA3x0oIfeKcI!HltHT1+Fr#B+2208ioNg3?)E?8tmej6$3X0PXOD_n1F zMJ|h69=Q~Y-dJ~$9D_W_x^l28@q&i_siEv4LHYK6`EIKG6_G0=V^MxI%1=Qalz)7P zD*w8MlFv|3(=&qdz5Vh7RQbyzzl)4U`AbkfhCC?0O_z8-USgnM;>=Ut*lDOLF?pzZ z$2<+~($En=`isx?%a5%6#tLtkvo5d8dJ#@h7~ys?!dMcP@+?Ijk5Z_iA*N8Zj3i+4 ziS0JQ80i&gF?f54A`9qw~2-x!MUIm)ySqXrNRuSf{`NbL9wW6`-qZA zP5?bmC;~`NB2X@m{;Sm70%Zv&>sDT_noDXbZ@$K<+ul< zeC8Ubs9q(-1K;%yrzW%>mDf1=MY~UlH`JR!H=|+4aOc_KJ7n=Vr6lW* z9VU9}l5@v7Eqf~oCW57nb0$Gb%0U!&vHd7o)brZI`^qqvl6Rc4C z{Q+CS)}ihP#yj0(r6d@$@dr|%Ya}{=z>0(>q8CsTHQVj^r!!U57T!q!zTl~%zTbx1 zC_Je@X*{coLNKJpnFMfYP^nPD#8C4sYMCTx;R<=!fXMSEI1Mg`A{_ZJmRKcSuw+`? zNzl6?I~_ieq;F*{^mhj{{i+!*E@a9it*E&PNv*)gVSC9 z^9HAdJnKfMwY>L6=USOL+xeaR>2CP6EV~IpmTX$?oFN~+#d)^k#ao>=c*hKSn~Tj zo%3YnT)^*_IcM2&@l8%f#Y^Q*KZ}oz-+Y!|J<64&tvuXohfefoqJ%?_q>Y zgfPOP6vGhsO+#ps?;1Dbw>JXQ(-EfOqnTc(7F-vwi*N{)FdbZ@Q4c|&BEt0z!!sJ- zVY^=eE6iesRGlFk&urW>KfNKI6JMAMSTKuNBUPv4Z97Es*58;yJ%ijD(~poNPA1yDx6 zjcgxT%-0y*NTm*C+K0NIa=c?HCW2fFYc4}nOb(6-7y>|4N{~rzcZ`uJ{w|CGF@0+k- zcS_Fx-}gf8m&c3N8F@+~gEkU(}Q4 zBhGNSb-mN?%=mxT{mgymGav4&JekIjnfY2 zIW(yNJH~sX!p&sHTBkT^!TWgri%^}}en(y18JxBxErS}m#zg#HkC2}LPxE2<%{r$; z_ut5;bh9_YS=`y@6fSR z=Pq5lb?-6yiZNG?y$YO!YjnZOInOyQ`H%C8p$LI^&3{20=}U7JQyo_fxJ9kFmVjH; zife7e3~tV-e?f(90kx|Y*B&r?i~oXr9r4^r#ZngIR(p9u;X+8F^o z*n)=q7B8HKa4LQ9_J4x*ud?J5>#&?Yv&N|-8$Rmf*@dlp%08Q%)+IG(lwqWT?vs8k zM&Nf@Pm6vSH~13F&k1EXaS`GfSjfLkZP4FF>;Hm=BU6%)}bWT^Es5oUb> z+^$wSVWi9JyN2plw0zRJ(5e??-G;dWRmLBxDk~OmaUMyJEyF^81?t*j>IEx+4YYyl zPXMO?=0FFq1DJCyfcXtQ6W~Ws-=GBeF~CHYAU{bw>bDUgUwFysG-&(XFBxI;5?(xy zhhxoalcAv~9E{d5JacyPg)gT1ECF87R!->}%B(Z)%9|s-BLlp1y#Yfy97JLk8XRB)Dh$oz^!XV$sB*Va*kC)qxjwA-2kuB#u;S4@E$Y&#- z^?#PB*YS%`KWS9}zk+8H#{kYkI%!(~uflT?LPh&OITzQ99eIkqveNK=P!Bud4|xk? z{%Lqi_+XVR!(m|cbS{LpKd7f7_)6QsC%bgXQmCrbV`SSO4ezjeES~VS;eDqb|6wf# zkFyndMM=eg=inAxJ-ZL@Icj)ox5k*z@<`dehPQ-rop>N^a~qA^fr7-SIS`qj!rYug*5S`xJD=#vNbYyvguZ z5lSk(HM^qOdwb!}Qh^IL=1l5%^Q^lJ?n;E@E7JT)mx!1$KonFR3`jN?TA znd?vh2MbyNb8h4!1dR1_crHqSH{-c(0=xjvzymqtOQ#LYs^s$pp@NtT@^%>#ZUs_s zrTw3h&uVV7HlL;8E|@D^On!$D%&ALBWBBsRaae0VM&ocEav7r)3ABbasJODqxzHrj zwe2gXZO30~+T$q0ehB#5YDzBizjg`=1B(3%6dVM+V%afgxS2i{l{7%$cwHrv{^o3H zbTuA>=3awm{$n!3xQh4w=3Hi4YqMqd<4#ZhPCM?jO1aTB@{l|~SFS(q^evd5t4Z)J zh^vP%bkdEZCyu)^a>2;K-H}Ip=g5kK-#ZUleQ&wRH8vr9i;#Y^Yji@m1YrZhPK3h< z<}I$#9^op4xd`hJDiJ=sMQ;0tGeJK7v-1 zzKcD!E=f~GqVg1kpu5PsaFM*cqH%gC+v>j*d<;kbUkGF}1NdYu_{UoCPd_~0WJ8%YBECq_Y-t^o@>lR z8*fGcKh?|#BJM>5@s}S}tHBGFR#estrI_Z8a##IOlR7sc%Q@$I=Up(UjnR9a{8#-@ zUW<9I!kQ6Esyy2pfGpR<NSb?as5TqT6xjCG1Y@>vF14}6%n2!`k3H?8FuEt2f_&*LG#WSbwL|>)Q2G2zZzuuL|b!uN~s=geBn?kP# zrG5B$rLnk0PRR;o*EX^C~)k0A6Hm;RJb3zx?`yM1@0!n4u`n$h(D>mnZEHktY8UK(E6wFE*E!d(dK5q2PWyEiNB{2QK| zNY^+Qp%kHbhii03C_y+2;ity(`hw8lV2S7NhXtYju>nwZT!Ci&4ZR0GzzEFQv`ojm%=D*+m7 zvo+5%8+kY#BG9`29>0wdnkB&G-OJ0C&eT^NX0DfrC$uEhsP=&qRa( z=5onG0Zd}TkqF@C_$Yp40RIEegmbafaw%Pb!1@CCAs@aXh*(l9gRBKVPz$~ba8rb! zd^w(5CBP+kCPB%~l^KPhTkF-$bd7OI10lRDyLcj=+ z06&31e)LHM7lA|d6v7sStq4y`&kEH~t-#|m2+ta_O*7bUFKHmh;@5mmi$_m3!wUOz zzI>-ysBgi~5F?4U6{w4ylK9H!QNDTT(b)E7uCWK z$Tb=xv_lw#a52JIgxe6x5f&n>Kqzj3FTg+c3&S>qU<$l|=b$hDi02m(u$i)T2d2{e zbac4!5)SGVhc0eb1w`g3%(k?uWz6WwqsNS!c1z?sXmYQbcpcZ}i}ETawg{z$I(+hH zluxFj0RJoqckco=@vu!jfb6~(JUp`pNoybiRf3@m)fL(+@(mP=NR{v=|CbNX_!kT|##>yafzL6U1}e zIimykS-GlfsG*$IHB`^KB~L!kHPkgdNK?{E#euG&DVF=$5j4&zS@zb?0*dq_FIQaA zBecNkvjTm=xyXr4wlILd!3T5F2Jok~;5P{C=1uM$N)mk|?N`WidWSlV3QBy04`4$B zxD!fnG6Zl}z?=gCJh2x1ajo(j)Q4tY+4kCTlWw>^f)^$fepDrfuWUPcqE1^O+w}|cA5+8mFqt^~) z!+zrfIIR}U@~l5ducP5uJfd!`3{wFYB@`ffkj)G#xCt;{9Kcrr=5!C>vj7(+z$Da( z4Is`tXkSvj!C9^*`-tj_FgQ!zJ~Y%^?!%v4k{^cG5%@2F z-!NokSZMfVo432hQwUoTfY~Y}8Qw~mDuuf+; import('@/pages/HomePage').then((m) => ({ default: m.HomePage }))); -const LoginPage = lazy(() => import('@/pages/LoginPage').then((m) => ({ default: m.LoginPage }))); -const SignupPage = lazy(() => import('@/pages/SignupPage').then((m) => ({ default: m.SignupPage }))); -const AdminPage = lazy(() => import('@/pages/AdminPage').then((m) => ({ default: m.AdminPage }))); -const DeveloperPage = lazy(() => import('@/pages/DeveloperPage').then((m) => ({ default: m.DeveloperPage }))); +function ProtectedRoute({ children }: { children: React.ReactNode }) { + const { isAuthenticated } = useAuth() + if (!isAuthenticated) return + return <>{children} +} -function Loading() { - return ( -

-
-
- ); +function GuestRoute({ children }: { children: React.ReactNode }) { + const { isAuthenticated } = useAuth() + if (isAuthenticated) return + return <>{children} } export default function App() { - const auth = useAuthState(); - return ( - - - }> - - }> - } /> - } /> - } /> - } /> - } /> - - - - - - ); + + + }> + {/* Guest routes */} + } /> + } /> + } /> + + {/* Protected routes */} + } /> + + {/* 404 */} + } /> + + + + ) } diff --git a/src/__tests__/AuthFlow.test.tsx b/src/__tests__/AuthFlow.test.tsx new file mode 100644 index 0000000..729f8dd --- /dev/null +++ b/src/__tests__/AuthFlow.test.tsx @@ -0,0 +1,223 @@ +/** + * Auth flow integration tests — verify the sequence of API calls + * and navigation for login and signup flows. + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { MemoryRouter } from 'react-router-dom' +import SusiPage from '../pages/SusiPage' +import * as api from '../services/api' + +// Mock the entire api module +vi.mock('../services/api', () => ({ + loginKey: vi.fn(), + prepareCodeLogin: vi.fn(), + generateSecretKey: vi.fn(), + registerKey: vi.fn(), + prepareCodeRegistration: vi.fn(), +})) + +// Mock useNavigate +const mockNavigate = vi.fn() +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom') + return { + ...actual, + useNavigate: () => mockNavigate, + } +}) + +function renderSusiPage() { + return render( + + + , + ) +} + +describe('Auth Flow Integration', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('Login flow', () => { + it('calls loginKey → prepareCodeLogin → navigates to /login-keypad', async () => { + const user = userEvent.setup() + const mockSession = { + sessionId: 'sess-123', + userId: 'user-abc', + createdAt: '2025-01-01', + expiresAt: '2025-01-02', + } + const mockCodeLoginData = { + keypadIndices: [0, 1, 2], + propertiesPerKey: 3, + numberOfKeys: 6, + mask: [1, 0, 1], + icons: [], + loginDataJson: '{}', + } + + vi.mocked(api.loginKey).mockResolvedValue(mockSession) + vi.mocked(api.prepareCodeLogin).mockResolvedValue(mockCodeLoginData) + + renderSusiPage() + + // Fill in email + await user.type(screen.getByPlaceholderText('email'), 'test@example.com') + + // Fill in secret key (32 hex chars) + await user.type( + screen.getByPlaceholderText('secret key (32 hex chars)'), + 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4', + ) + + // Submit (find the submit button, not the tab) + const loginButtons = screen.getAllByRole('button', { name: /login/i }) + const submitBtn = loginButtons.find(b => b.getAttribute('type') === 'submit')! + await user.click(submitBtn) + + await waitFor(() => { + // Step 1: loginKey called with email + key + expect(api.loginKey).toHaveBeenCalledWith( + 'test@example.com', + 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4', + ) + }) + + await waitFor(() => { + // Step 2: prepareCodeLogin called with userId + key + expect(api.prepareCodeLogin).toHaveBeenCalledWith( + 'user-abc', + 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4', + ) + }) + + await waitFor(() => { + // Step 3: navigate to /login-keypad with state + expect(mockNavigate).toHaveBeenCalledWith('/login-keypad', { + state: { + email: 'test@example.com', + secretKeyHex: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4', + userId: 'user-abc', + codeLoginData: mockCodeLoginData, + }, + }) + }) + }) + + it('shows error on login failure', async () => { + const user = userEvent.setup() + vi.mocked(api.loginKey).mockRejectedValue(new Error('Invalid credentials')) + + renderSusiPage() + + await user.type(screen.getByPlaceholderText('email'), 'test@example.com') + await user.type( + screen.getByPlaceholderText('secret key (32 hex chars)'), + 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4', + ) + const loginButtons = screen.getAllByRole('button', { name: /login/i }) + const submitBtn = loginButtons.find(b => b.getAttribute('type') === 'submit')! + await user.click(submitBtn) + + await waitFor(() => { + expect(screen.getByText('Invalid credentials')).toBeInTheDocument() + }) + }) + }) + + describe('Signup flow', () => { + it('calls generateSecretKey → registerKey → loginKey → prepareCodeRegistration → navigates to /signup-keypad', async () => { + const user = userEvent.setup() + const mockSecretKey = 'deadbeef12345678deadbeef12345678' + const mockSession = { + sessionId: 'sess-456', + userId: 'user-xyz', + createdAt: '2025-01-01', + expiresAt: '2025-01-02', + } + const mockIcons = { + icons: [ + { file_name: 'icon1.svg', file_type: 'svg', img_data: '' }, + ], + } + + vi.mocked(api.generateSecretKey).mockReturnValue(mockSecretKey) + vi.mocked(api.registerKey).mockResolvedValue(undefined) + vi.mocked(api.loginKey).mockResolvedValue(mockSession) + vi.mocked(api.prepareCodeRegistration).mockResolvedValue(mockIcons) + + renderSusiPage() + + // Switch to signup tab + await user.click(screen.getByRole('button', { name: /sign up/i })) + + // Fill in email + await user.type(screen.getByPlaceholderText('email'), 'newuser@example.com') + + // Submit — the submit button in signup tab also says "Sign Up" + // We need the form submit button, not the tab button + const buttons = screen.getAllByRole('button', { name: /sign up/i }) + // The last one is the form submit button + const submitBtn = buttons[buttons.length - 1] + await user.click(submitBtn) + + await waitFor(() => { + // Step 1: generateSecretKey + expect(api.generateSecretKey).toHaveBeenCalled() + }) + + await waitFor(() => { + // Step 2: registerKey + expect(api.registerKey).toHaveBeenCalledWith('newuser@example.com', mockSecretKey) + }) + + await waitFor(() => { + // Step 3: loginKey + expect(api.loginKey).toHaveBeenCalledWith('newuser@example.com', mockSecretKey) + }) + + await waitFor(() => { + // Step 4: prepareCodeRegistration + expect(api.prepareCodeRegistration).toHaveBeenCalled() + }) + + await waitFor(() => { + // Step 5: navigate to /signup-keypad + expect(mockNavigate).toHaveBeenCalledWith('/signup-keypad', { + state: { + email: 'newuser@example.com', + secretKeyHex: mockSecretKey, + userId: 'user-xyz', + icons: mockIcons.icons, + }, + }) + }) + }) + }) + + describe('No method selection anywhere', () => { + it('login tab has no method selection step', () => { + renderSusiPage() + + expect(screen.queryByText(/choose.*method/i)).not.toBeInTheDocument() + expect(screen.queryByText(/key-based/i)).not.toBeInTheDocument() + expect(screen.queryByText(/code-based/i)).not.toBeInTheDocument() + expect(screen.queryByRole('radio')).not.toBeInTheDocument() + }) + + it('signup tab has no method selection step', async () => { + const user = userEvent.setup() + renderSusiPage() + + await user.click(screen.getByRole('button', { name: /sign up/i })) + + expect(screen.queryByText(/choose.*method/i)).not.toBeInTheDocument() + expect(screen.queryByText(/key-based/i)).not.toBeInTheDocument() + expect(screen.queryByText(/code-based/i)).not.toBeInTheDocument() + expect(screen.queryByRole('radio')).not.toBeInTheDocument() + }) + }) +}) diff --git a/src/__tests__/Keypad.test.tsx b/src/__tests__/Keypad.test.tsx new file mode 100644 index 0000000..e6953dc --- /dev/null +++ b/src/__tests__/Keypad.test.tsx @@ -0,0 +1,194 @@ +/** + * Keypad component tests — verify rendering, selection, backspace, submit, SVG rendering. + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import Keypad from '../components/Keypad' + +// Simple SVG test data +const makeSvg = (id: number) => + `` + +function makeSvgs(count: number): string[] { + return Array.from({ length: count }, (_, i) => makeSvg(i)) +} + +describe('Keypad', () => { + let onComplete: (selection: number[]) => void + + beforeEach(() => { + onComplete = vi.fn() as unknown as (selection: number[]) => void + }) + + it('renders correct number of keys based on numbOfKeys prop', () => { + const numbOfKeys = 6 + const attrsPerKey = 3 + const svgs = makeSvgs(numbOfKeys * attrsPerKey) + + render( + , + ) + + const keys = screen.getAllByRole('button', { name: /Key \d+/ }) + expect(keys).toHaveLength(numbOfKeys) + }) + + it('each key has attrsPerKey icon cells', () => { + const numbOfKeys = 4 + const attrsPerKey = 3 + const svgs = makeSvgs(numbOfKeys * attrsPerKey) + + render( + , + ) + + // Each key button should contain attrsPerKey icon cells + const keys = screen.getAllByRole('button', { name: /Key \d+/ }) + keys.forEach((key) => { + const cells = key.querySelectorAll('.keypad-icon-cell') + expect(cells).toHaveLength(attrsPerKey) + }) + }) + + it('pressing a key adds to selection (dot appears)', async () => { + const user = userEvent.setup() + const numbOfKeys = 4 + const attrsPerKey = 3 + const svgs = makeSvgs(numbOfKeys * attrsPerKey) + + render( + , + ) + + // Initially shows placeholder + expect(screen.getByText(/tap icons to enter/i)).toBeInTheDocument() + + // Click first key + await user.click(screen.getByRole('button', { name: 'Key 1' })) + + // Placeholder gone, dot appears + expect(screen.queryByText(/tap icons to enter/i)).not.toBeInTheDocument() + expect(screen.getByText('•')).toBeInTheDocument() + + // Click second key + await user.click(screen.getByRole('button', { name: 'Key 2' })) + expect(screen.getByText('••')).toBeInTheDocument() + }) + + it('backspace removes last selection', async () => { + const user = userEvent.setup() + const numbOfKeys = 4 + const attrsPerKey = 3 + const svgs = makeSvgs(numbOfKeys * attrsPerKey) + + render( + , + ) + + // Add two selections + await user.click(screen.getByRole('button', { name: 'Key 1' })) + await user.click(screen.getByRole('button', { name: 'Key 2' })) + expect(screen.getByText('••')).toBeInTheDocument() + + // Click backspace + await user.click(screen.getByRole('button', { name: /delete last selection/i })) + expect(screen.getByText('•')).toBeInTheDocument() + + // Click backspace again + await user.click(screen.getByRole('button', { name: /delete last selection/i })) + expect(screen.getByText(/tap icons to enter/i)).toBeInTheDocument() + }) + + it('submit calls onComplete with selection array', async () => { + const user = userEvent.setup() + const numbOfKeys = 4 + const attrsPerKey = 3 + const svgs = makeSvgs(numbOfKeys * attrsPerKey) + + render( + , + ) + + // Select keys 0, 2, 1 + await user.click(screen.getByRole('button', { name: 'Key 1' })) + await user.click(screen.getByRole('button', { name: 'Key 3' })) + await user.click(screen.getByRole('button', { name: 'Key 2' })) + + // Click submit + await user.click(screen.getByRole('button', { name: /submit/i })) + + expect(onComplete).toHaveBeenCalledOnce() + expect(onComplete).toHaveBeenCalledWith([0, 2, 1]) + }) + + it('submit is disabled when no selection', () => { + const numbOfKeys = 4 + const attrsPerKey = 3 + const svgs = makeSvgs(numbOfKeys * attrsPerKey) + + render( + , + ) + + const submitBtn = screen.getByRole('button', { name: /submit/i }) + expect(submitBtn).toBeDisabled() + }) + + it('renders SVGs as-is (no color manipulation)', () => { + const numbOfKeys = 2 + const attrsPerKey = 1 + const svgs = [ + '', + '', + ] + + render( + , + ) + + // SVGs should be rendered with original colors intact + const circles = document.querySelectorAll('circle') + expect(circles).toHaveLength(1) + expect(circles[0].getAttribute('fill')).toBe('#ff0000') + + const rects = document.querySelectorAll('rect') + expect(rects).toHaveLength(1) + expect(rects[0].getAttribute('fill')).toBe('blue') + }) +}) diff --git a/src/__tests__/SusiPage.test.tsx b/src/__tests__/SusiPage.test.tsx new file mode 100644 index 0000000..aec39e3 --- /dev/null +++ b/src/__tests__/SusiPage.test.tsx @@ -0,0 +1,103 @@ +/** + * SusiPage tests — verify login/signup tabs render correctly + * and that NO method selection UI exists. + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { MemoryRouter } from 'react-router-dom' +import SusiPage from '../pages/SusiPage' + +// Mock the api module so no WASM/OPAQUE calls are made +vi.mock('../services/api', () => ({ + loginKey: vi.fn(), + prepareCodeLogin: vi.fn(), + generateSecretKey: vi.fn(), + registerKey: vi.fn(), + prepareCodeRegistration: vi.fn(), +})) + +function renderSusiPage() { + return render( + + + , + ) +} + +describe('SusiPage', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('Login tab (default)', () => { + it('renders email input, secret key input, and Login button', () => { + renderSusiPage() + + expect(screen.getByPlaceholderText('email')).toBeInTheDocument() + expect(screen.getByPlaceholderText('secret key (32 hex chars)')).toBeInTheDocument() + // There are two "Login" buttons: tab + submit. Verify submit exists. + const loginButtons = screen.getAllByRole('button', { name: /login/i }) + expect(loginButtons.length).toBeGreaterThanOrEqual(1) + const submitBtn = loginButtons.find(b => b.getAttribute('type') === 'submit') + expect(submitBtn).toBeDefined() + }) + + it('does NOT render any method selection UI', () => { + renderSusiPage() + + expect(screen.queryByText(/key-based/i)).not.toBeInTheDocument() + expect(screen.queryByText(/code-based/i)).not.toBeInTheDocument() + expect(screen.queryByText(/choose method/i)).not.toBeInTheDocument() + expect(screen.queryByText(/select.*method/i)).not.toBeInTheDocument() + }) + }) + + describe('Signup tab', () => { + it('renders email input and Sign Up button (no secret key input)', async () => { + const user = userEvent.setup() + renderSusiPage() + + // Switch to signup tab + await user.click(screen.getByRole('button', { name: /sign up/i })) + + // Signup has only email input + expect(screen.getByPlaceholderText('email')).toBeInTheDocument() + expect(screen.queryByPlaceholderText('secret key (32 hex chars)')).not.toBeInTheDocument() + + // Submit button says "Sign Up" (tab + form button both say "Sign Up") + const signUpButtons = screen.getAllByRole('button', { name: /sign up/i }) + const submitBtn = signUpButtons.find(b => b.getAttribute('type') === 'submit') + expect(submitBtn).toBeDefined() + }) + + it('does NOT render method selection or key-based/code-based options', async () => { + const user = userEvent.setup() + renderSusiPage() + + await user.click(screen.getByRole('button', { name: /sign up/i })) + + expect(screen.queryByText(/key-based/i)).not.toBeInTheDocument() + expect(screen.queryByText(/code-based/i)).not.toBeInTheDocument() + expect(screen.queryByText(/choose method/i)).not.toBeInTheDocument() + }) + }) + + describe('Tab switching', () => { + it('switches between Login and Sign Up tabs', async () => { + const user = userEvent.setup() + renderSusiPage() + + // Initially on Login tab — secret key field present + expect(screen.getByPlaceholderText('secret key (32 hex chars)')).toBeInTheDocument() + + // Click Sign Up + await user.click(screen.getByRole('button', { name: /sign up/i })) + expect(screen.queryByPlaceholderText('secret key (32 hex chars)')).not.toBeInTheDocument() + + // Click Login to switch back + await user.click(screen.getByRole('button', { name: /login/i })) + expect(screen.getByPlaceholderText('secret key (32 hex chars)')).toBeInTheDocument() + }) + }) +}) diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts new file mode 100644 index 0000000..c44951a --- /dev/null +++ b/src/__tests__/setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom' diff --git a/src/components/Keypad.tsx b/src/components/Keypad.tsx index 6177ea7..fc6309d 100644 --- a/src/components/Keypad.tsx +++ b/src/components/Keypad.tsx @@ -1,153 +1,301 @@ -import { useState, useCallback, useEffect } from 'react'; +/** + * SVG Icon Keypad — matches Flutter keypad.dart + keypad_interface.dart. + * SVGs render as-is (no color application per DoDNKode branch). + */ +import { useState, useCallback, useEffect, useRef } from 'react' interface KeypadProps { - /** Number of digits expected */ - length: number; - /** Called when all digits entered */ - onComplete: (digits: number[]) => void; - /** Optional: called on each digit press */ - onDigit?: (digit: number, current: number[]) => void; - /** Show the entered digits (vs dots) */ - showDigits?: boolean; - /** Label text above the indicator */ - label?: string; - /** Disable input */ - disabled?: boolean; - /** Error message to show */ - error?: string; - /** Reset trigger — increment to clear */ - resetKey?: number; + svgs: string[] + attrsPerKey: number + numbOfKeys: number + onComplete: (selection: number[]) => void + loading?: boolean } -const KEYS = [ - [1, 2, 3], - [4, 5, 6], - [7, 8, 9], - [null, 0, 'del'], -] as const; +function keyAspectRatio(attrsPerKey: number): number { + if (attrsPerKey <= 3) return 21 / 7 + if (attrsPerKey <= 6) return 21 / 15 + if (attrsPerKey <= 9) return 1 + if (attrsPerKey <= 12) return 21 / 27.5 + if (attrsPerKey <= 15) return 21 / 34 + return 21 / 40.5 +} -export function Keypad({ - length, +export default function Keypad({ + svgs, + attrsPerKey, + numbOfKeys, onComplete, - onDigit, - showDigits = false, - label, - disabled = false, - error, - resetKey = 0, + loading = false, }: KeypadProps) { - const [digits, setDigits] = useState([]); + const [selection, setSelection] = useState([]) + const [pressedKey, setPressedKey] = useState(null) + const [columns, setColumns] = useState(2) + const pressTimerRef = useRef | null>(null) - // Reset when resetKey changes + // Reset selection when svgs change + const svgsRef = useRef(svgs) useEffect(() => { - setDigits([]); - }, [resetKey]); + if (svgsRef.current !== svgs) { + svgsRef.current = svgs + setSelection([]) + } + }, [svgs]) - const handlePress = useCallback( - (key: number | 'del') => { - if (disabled) return; - - if (key === 'del') { - setDigits((d) => d.slice(0, -1)); - return; - } - - setDigits((prev) => { - if (prev.length >= length) return prev; - const next = [...prev, key]; - onDigit?.(key, next); - if (next.length === length) { - // Slight delay so the UI shows the last dot - setTimeout(() => onComplete(next), 150); - } - return next; - }); - }, - [length, onComplete, onDigit, disabled] - ); - - // Keyboard support + // Responsive: 2 cols portrait, 3 cols landscape useEffect(() => { - const handler = (e: KeyboardEvent) => { - if (disabled) return; - const n = parseInt(e.key); - if (!isNaN(n) && n >= 0 && n <= 9) { - handlePress(n); - } else if (e.key === 'Backspace') { - handlePress('del'); + const update = () => setColumns(window.innerWidth > window.innerHeight ? 3 : 2) + update() + window.addEventListener('resize', update) + return () => window.removeEventListener('resize', update) + }, []) + + useEffect(() => { + return () => { + if (pressTimerRef.current) clearTimeout(pressTimerRef.current) + } + }, []) + + const handlePress = useCallback((keyIndex: number) => { + setSelection(prev => [...prev, keyIndex]) + setPressedKey(keyIndex) + if (pressTimerRef.current) clearTimeout(pressTimerRef.current) + pressTimerRef.current = setTimeout(() => setPressedKey(null), 200) + }, []) + + const handleBackspace = useCallback(() => { + setSelection(prev => prev.slice(0, -1)) + }, []) + + const handleSubmit = useCallback(() => { + if (selection.length > 0) { + onComplete(selection) + } + }, [selection, onComplete]) + + // Keyboard: Backspace to delete, Enter to submit + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Backspace') { + e.preventDefault() + handleBackspace() + } else if (e.key === 'Enter' && selection.length > 0 && !loading) { + e.preventDefault() + handleSubmit() } - }; - window.addEventListener('keydown', handler); - return () => window.removeEventListener('keydown', handler); - }, [handlePress, disabled]); + } + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [handleBackspace, handleSubmit, selection.length, loading]) + + const aspect = keyAspectRatio(attrsPerKey) + const iconRows = Math.ceil(attrsPerKey / 3) return ( -
- {/* Label */} - {label && ( -

- {label} -

- )} - - {/* Dot indicators */} -
- {Array.from({ length }).map((_, i) => ( -
- {showDigits && i < digits.length && ( - - {digits[i]} +
+ {/* Input display: [• dots + ⌫] [Submit] */} +
+
+
+ {selection.length === 0 ? ( + Tap icons to enter your nKode + ) : ( + + {'•'.repeat(selection.length)} )}
- ))} + +
+
- {/* Error */} - {error && ( -

{error}

- )} + {/* Key tile grid */} +
+ {Array.from({ length: numbOfKeys }).map((_, keyIndex) => { + const isActive = pressedKey === keyIndex + const startIdx = keyIndex * attrsPerKey + const keyIcons = svgs.slice(startIdx, startIdx + attrsPerKey) - {/* Keypad grid */} -
- {KEYS.flat().map((key, i) => { - if (key === null) { - return
; - } - if (key === 'del') { - return ( - - ); - } return ( - ); + ) })}
+ +
- ); + ) } diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 3647310..89dad66 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,70 +1,53 @@ -import { Link, Outlet, useLocation } from 'react-router-dom'; -import { useAuth } from '@/hooks/useAuth'; -import { useTheme } from '@/hooks/useTheme'; -import { ROUTES } from '@/lib/types'; +/** + * App shell layout. + */ +import { Outlet, Link } from 'react-router-dom' +import { useAuth } from '../hooks/useAuth' -export function Layout() { - const { isAuthenticated, email, logout } = useAuth(); - const { resolved, setTheme, theme } = useTheme(); - const location = useLocation(); - - const isAuthPage = - location.pathname === ROUTES.LOGIN || location.pathname === ROUTES.SIGNUP; +export default function Layout() { + const { isAuthenticated, email, logout } = useAuth() return ( -
+
{/* Header */} -
+
- - n - Kode + +
+ nK +
+ nKode -
- {/* Theme toggle */} - - +
+
- {/* Content */} -
- + {/* Main content */} +
+
+ +
+ + {/* Footer */} +
+ © {new Date().getFullYear()} nKode — Passwordless Authentication +
- ); + ) } diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index f571b26..2a7e002 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -1,64 +1,51 @@ -import { useState, useCallback, useEffect, createContext, useContext } from 'react'; -import type { AuthState, NKodeSession } from '@/lib/types'; +/** + * Auth context — stores email + secretKey for OPAQUE-based auth. + * No tokens needed; the WASM client handles session keys internally. + */ +import { createContext, useContext, useState, useCallback, type ReactNode } from 'react' +import { createElement } from 'react' +import type { AuthState } from '../types' +import { loadAuth, saveAuth, clearAuth } from '../services/auth' +import { resetClient } from '../services/api' -const SESSION_KEY = 'nkode_session'; -const EMAIL_KEY = 'nkode_email'; - -function loadStoredSession(): AuthState { - try { - const raw = localStorage.getItem(SESSION_KEY); - const email = localStorage.getItem(EMAIL_KEY); - if (raw) { - const session: NKodeSession = JSON.parse(raw); - // Check expiry - if (new Date(session.expiresAt) > new Date()) { - return { isAuthenticated: true, session, email }; - } - localStorage.removeItem(SESSION_KEY); - } - } catch {} - return { isAuthenticated: false, session: null, email: null }; +interface AuthContextValue extends AuthState { + /** Call after successful registration + code setup to persist credentials. */ + login: (email: string, secretKeyHex: string, userId: string) => void + /** Clear persisted credentials. */ + logout: () => void + /** True if we have stored credentials (email + secretKey). */ + isAuthenticated: boolean } -export interface AuthContextType extends AuthState { - login: (email: string, session: NKodeSession) => void; - logout: () => void; -} +const AuthContext = createContext(null) -export const AuthContext = createContext(null); +export function AuthProvider({ children }: { children: ReactNode }) { + const [state, setState] = useState(loadAuth) -export function useAuth(): AuthContextType { - const ctx = useContext(AuthContext); - if (!ctx) throw new Error('useAuth must be inside AuthProvider'); - return ctx; -} - -export function useAuthState() { - const [state, setState] = useState(loadStoredSession); - - const login = useCallback((email: string, session: NKodeSession) => { - localStorage.setItem(SESSION_KEY, JSON.stringify(session)); - localStorage.setItem(EMAIL_KEY, email); - setState({ isAuthenticated: true, session, email }); - }, []); + const login = useCallback((email: string, secretKeyHex: string, userId: string) => { + const next: AuthState = { email, secretKeyHex, userId } + setState(next) + saveAuth(next) + }, []) const logout = useCallback(() => { - localStorage.removeItem(SESSION_KEY); - localStorage.removeItem(EMAIL_KEY); - setState({ isAuthenticated: false, session: null, email: null }); - }, []); + setState({ email: null, secretKeyHex: null, userId: null }) + clearAuth() + resetClient() + }, []) - // Auto-logout on expiry - useEffect(() => { - if (!state.session) return; - const ms = new Date(state.session.expiresAt).getTime() - Date.now(); - if (ms <= 0) { - logout(); - return; - } - const timer = setTimeout(logout, ms); - return () => clearTimeout(timer); - }, [state.session, logout]); + const value: AuthContextValue = { + ...state, + login, + logout, + isAuthenticated: !!state.email && !!state.secretKeyHex && !!state.userId, + } - return { ...state, login, logout }; + return createElement(AuthContext.Provider, { value }, children) +} + +export function useAuth(): AuthContextValue { + const ctx = useContext(AuthContext) + if (!ctx) throw new Error('useAuth must be used within AuthProvider') + return ctx } diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts deleted file mode 100644 index 054cefc..0000000 --- a/src/hooks/useTheme.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; - -type Theme = 'light' | 'dark' | 'system'; - -function getSystemTheme(): 'light' | 'dark' { - return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; -} - -export function useTheme() { - const [theme, setThemeState] = useState(() => { - return (localStorage.getItem('nkode_theme') as Theme) || 'system'; - }); - - const resolved = theme === 'system' ? getSystemTheme() : theme; - - useEffect(() => { - document.documentElement.classList.toggle('dark', resolved === 'dark'); - }, [resolved]); - - useEffect(() => { - if (theme !== 'system') return; - const mq = window.matchMedia('(prefers-color-scheme: dark)'); - const handler = () => setThemeState('system'); // re-trigger - mq.addEventListener('change', handler); - return () => mq.removeEventListener('change', handler); - }, [theme]); - - const setTheme = useCallback((t: Theme) => { - localStorage.setItem('nkode_theme', t); - setThemeState(t); - }, []); - - return { theme, resolved, setTheme }; -} diff --git a/src/index.css b/src/index.css index 5b56308..f1d8c73 100644 --- a/src/index.css +++ b/src/index.css @@ -1,47 +1 @@ @import "tailwindcss"; - -@custom-variant dark (&:is(.dark *)); - -:root { - --nkode-primary: #6366f1; - --nkode-primary-hover: #4f46e5; - --nkode-surface: #ffffff; - --nkode-surface-alt: #f8fafc; - --nkode-border: #e2e8f0; - --nkode-text: #0f172a; - --nkode-text-muted: #64748b; -} - -.dark { - --nkode-primary: #818cf8; - --nkode-primary-hover: #6366f1; - --nkode-surface: #0f172a; - --nkode-surface-alt: #1e293b; - --nkode-border: #334155; - --nkode-text: #f1f5f9; - --nkode-text-muted: #94a3b8; -} - -body { - margin: 0; - font-family: 'Inter', system-ui, -apple-system, sans-serif; - background: var(--nkode-surface); - color: var(--nkode-text); - -webkit-font-smoothing: antialiased; -} - -/* Keypad button styles */ -.keypad-btn { - @apply w-16 h-16 rounded-full text-2xl font-semibold - transition-all duration-150 ease-out - active:scale-95 select-none - bg-white dark:bg-slate-800 - text-slate-900 dark:text-slate-100 - border border-slate-200 dark:border-slate-700 - hover:bg-slate-50 dark:hover:bg-slate-700 - shadow-sm; -} - -.keypad-btn:active { - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); -} diff --git a/src/lib/nkode-client.ts b/src/lib/nkode-client.ts deleted file mode 100644 index 7c26a33..0000000 --- a/src/lib/nkode-client.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** - * nKode WASM client wrapper. - * - * This module lazily loads the WASM package and provides a typed API. - * In development, it falls back to a mock client until the WASM package is linked. - */ - -import type { NKodeSession } from './types'; - -// We'll import the WASM module dynamically -let wasmModule: any = null; -let wasmClient: any = null; - -const API_BASE = import.meta.env.VITE_NKODE_API_URL || 'http://localhost:3000'; - -/** - * Try to load the WASM module from /wasm/ directory. - * Uses dynamic import with vite-ignore to avoid bundling. - */ -async function tryLoadWasm(): Promise { - try { - // Check if WASM files are available - const check = await fetch('/wasm/nkode_client_wasm_bg.wasm', { method: 'HEAD' }); - if (!check.ok) return null; - - // Dynamic import — vite-ignore prevents bundler from resolving - const wasmUrl = new URL('/wasm/nkode_client_wasm.js', window.location.origin).href; - const module = await import(/* @vite-ignore */ wasmUrl); - await module.default('/wasm/nkode_client_wasm_bg.wasm'); - module.init(); - return module; - } catch { - return null; - } -} - -/** - * Initialize the WASM module. Call once at app startup. - */ -export async function initNKode(): Promise { - try { - const wasm = await tryLoadWasm(); - if (wasm) { - wasmModule = wasm; - wasmClient = new wasm.NKodeClient(API_BASE); - console.log('[nKode] WASM client initialized'); - } else { - console.warn('[nKode] WASM not available, using mock client'); - } - } catch (e) { - console.warn('[nKode] WASM init error, using mock client:', e); - } -} - -/** - * Check if WASM client is loaded. - */ -export function isWasmReady(): boolean { - return wasmClient !== null; -} - -/** - * Generate a random 16-byte secret key (hex string). - */ -export function generateSecretKey(): string { - if (wasmModule) { - return wasmModule.NKodeClient.generateSecretKey(); - } - // Fallback: browser crypto - const bytes = new Uint8Array(16); - crypto.getRandomValues(bytes); - return Array.from(bytes) - .map((b) => b.toString(16).padStart(2, '0')) - .join(''); -} - -/** - * Register with key-based OPAQUE flow. - */ -export async function registerKey(email: string, secretKeyHex: string): Promise { - if (wasmClient) { - await wasmClient.registerKey(email, secretKeyHex); - return; - } - // Mock - console.log('[nKode mock] registerKey', email); - await new Promise((r) => setTimeout(r, 500)); -} - -/** - * Register with code-based OPAQUE flow. - */ -export async function registerCode(email: string, passcode: Uint8Array): Promise { - if (wasmClient) { - await wasmClient.registerCode(email, passcode); - return; - } - console.log('[nKode mock] registerCode', email); - await new Promise((r) => setTimeout(r, 500)); -} - -/** - * Login with key-based OPAQUE flow. - */ -export async function loginKey(email: string, secretKeyHex: string): Promise { - if (wasmClient) { - return await wasmClient.loginKey(email, secretKeyHex); - } - console.log('[nKode mock] loginKey', email); - await new Promise((r) => setTimeout(r, 500)); - return { - sessionId: 'mock-session', - userId: 'mock-user', - createdAt: new Date().toISOString(), - expiresAt: new Date(Date.now() + 3600000).toISOString(), - }; -} - -/** - * Login with code-based OPAQUE flow. - */ -export async function loginCode(email: string, passcode: Uint8Array): Promise { - if (wasmClient) { - return await wasmClient.loginCode(email, passcode); - } - console.log('[nKode mock] loginCode', email); - await new Promise((r) => setTimeout(r, 500)); - return { - sessionId: 'mock-session', - userId: 'mock-user', - createdAt: new Date().toISOString(), - expiresAt: new Date(Date.now() + 3600000).toISOString(), - }; -} diff --git a/src/lib/types.ts b/src/lib/types.ts deleted file mode 100644 index 8376dff..0000000 --- a/src/lib/types.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** Authentication flow type */ -export type AuthFlow = 'key' | 'code'; - -/** User session from successful login */ -export interface NKodeSession { - sessionId: string; - userId: string; - createdAt: string; - expiresAt: string; -} - -/** App-level auth state */ -export interface AuthState { - isAuthenticated: boolean; - session: NKodeSession | null; - email: string | null; -} - -/** Route paths */ -export const ROUTES = { - LOGIN: '/login', - SIGNUP: '/signup', - HOME: '/', - ADMIN: '/admin', - DEVELOPER: '/developer', -} as const; diff --git a/src/main.tsx b/src/main.tsx index 98e77aa..4bd3d2a 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,14 +1,20 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import './index.css'; -import App from './App'; -import { initNKode } from '@/lib/nkode-client'; +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import './index.css' +import App from './App' +import { initClient } from './services/api' -// Initialize WASM client (non-blocking) -initNKode(); - -createRoot(document.getElementById('root')!).render( - - - -); +// Initialize WASM client, then render +initClient().then(() => { + createRoot(document.getElementById('root')!).render( + + + + + , + ) +}).catch((err: unknown) => { + console.error('Failed to initialize WASM client:', err) + document.getElementById('root')!.textContent = 'Failed to load application. Please refresh.' +}) diff --git a/src/pages/AdminPage.tsx b/src/pages/AdminPage.tsx deleted file mode 100644 index 0c1c98f..0000000 --- a/src/pages/AdminPage.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useAuth } from '@/hooks/useAuth'; -import { Navigate } from 'react-router-dom'; -import { ROUTES } from '@/lib/types'; - -export function AdminPage() { - const { isAuthenticated } = useAuth(); - - if (!isAuthenticated) { - return ; - } - - return ( -
-

Admin Dashboard

- -
- {[ - { label: 'Total Users', value: '—', icon: '👤' }, - { label: 'Active Sessions', value: '—', icon: '🔐' }, - { label: 'Registered Clients', value: '—', icon: '📱' }, - ].map((stat) => ( -
-
- {stat.icon} -
-

{stat.label}

-

{stat.value}

-
-
-
- ))} -
- -
-

- Admin features coming soon. This will display user management, session monitoring, and OIDC client configuration. -

-
-
- ); -} diff --git a/src/pages/DeveloperPage.tsx b/src/pages/DeveloperPage.tsx deleted file mode 100644 index 3205595..0000000 --- a/src/pages/DeveloperPage.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { useAuth } from '@/hooks/useAuth'; -import { Navigate } from 'react-router-dom'; -import { ROUTES } from '@/lib/types'; - -export function DeveloperPage() { - const { isAuthenticated } = useAuth(); - - if (!isAuthenticated) { - return ; - } - - return ( -
-

Developer Dashboard

- -
-

OIDC Client Setup

-

- Register your application to use nKode as an identity provider. Configure redirect URIs, scopes, and authentication flows. -

- -
-
- - -
-
- - -
-
- -

- Client registration coming soon. This will generate client_id and client_secret for OAuth2/OIDC integration. -

-
-
- ); -} diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index f9db0d3..d1b4dc4 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -1,85 +1,46 @@ -import { useAuth } from '@/hooks/useAuth'; -import { Keypad } from '@/components/Keypad'; -import { useState } from 'react'; +/** + * Home page — shown after full authentication (key + code login). + */ +import { useAuth } from '../hooks/useAuth' +import { useNavigate } from 'react-router-dom' -export function HomePage() { - const { isAuthenticated, session, email } = useAuth(); - const [practiceResult, setPracticeResult] = useState(null); - const [resetKey, setResetKey] = useState(0); +export default function HomePage() { + const { email, userId, logout } = useAuth() + const navigate = useNavigate() - if (!isAuthenticated) { - return ( -
-

- nKode -

-

- Passwordless authentication powered by OPAQUE. -

-

- Replace passwords with a memorized numeric code or a cryptographic key. - Zero-knowledge proof means the server never sees your secret. -

-
- ); + const handleLogout = () => { + logout() + navigate('/', { replace: true }) } - const handlePractice = (digits: number[]) => { - setPracticeResult(`✅ You entered: ${digits.join('')}`); - setTimeout(() => { - setPracticeResult(null); - setResetKey((k) => k + 1); - }, 2000); - }; - return ( -
- {/* Welcome */} -
-

- Welcome back -

-

{email}

+
+
+
+

Welcome

+

{email}

+

User: {userId}

+
+
- {/* Session info */} -
-

Session

-
-
-
Session ID
-
- {session?.sessionId} -
+
+
+
+ + + +
-
-
Expires
-
- {session?.expiresAt - ? new Date(session.expiresAt).toLocaleString() - : '—'} -
-
-
-
- - {/* Practice keypad */} -
-

- Practice your nKode -

- - {practiceResult && ( -

- {practiceResult} -

- )} +

Authenticated via OPAQUE

+

Key + Code authentication complete

+
- ); + ) } diff --git a/src/pages/LoginKeypadPage.tsx b/src/pages/LoginKeypadPage.tsx new file mode 100644 index 0000000..6df488f --- /dev/null +++ b/src/pages/LoginKeypadPage.tsx @@ -0,0 +1,114 @@ +/** + * Login keypad page — code-based OPAQUE login. + * Displays keypad from prepareCodeLogin data, user taps their nKode, + * then deciphers selection and performs code login. + */ +import { useState } from 'react' +import { useLocation, useNavigate, Navigate } from 'react-router-dom' +import Keypad from '../components/Keypad' +import { useAuth } from '../hooks/useAuth' +import * as api from '../services/api' +import type { CodeLoginData, NKodeIcon } from '../types' + +interface LocationState { + email: string + secretKeyHex: string + userId: string + codeLoginData: CodeLoginData +} + +export default function LoginKeypadPage() { + const location = useLocation() + const navigate = useNavigate() + const auth = useAuth() + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + const state = location.state as LocationState | null + if (!state?.email || !state?.codeLoginData) { + return + } + + const { email, secretKeyHex, userId, codeLoginData } = state + + // Build SVG strings from icons for the keypad. + // Icons are already ordered by keypadIndices from prepareCodeLogin. + const svgs = buildSvgsFromCodeLoginData(codeLoginData) + + const handleLogin = async (pressedKeys: number[]) => { + setLoading(true) + setError(null) + try { + // Decipher key selections into passcode bytes + const passcodeBytes = api.decipherSelection( + secretKeyHex, + codeLoginData.loginDataJson, + pressedKeys, + ) + + // OPAQUE code login + await api.loginCode(email, passcodeBytes) + + // Save credentials and navigate home + auth.login(email, secretKeyHex, userId) + navigate('/home', { replace: true }) + } catch (err) { + setError(err instanceof Error ? err.message : 'Login failed') + } finally { + setLoading(false) + } + } + + return ( +
+
+
+ +

Login

+
+

{email}

+
+ + {error && ( +
+
+ {error} +
+
+ )} + + +
+ ) +} + +/** Build SVG strings array from CodeLoginData for the Keypad component. */ +function buildSvgsFromCodeLoginData(data: CodeLoginData): string[] { + const { keypadIndices, icons } = data + // keypadIndices maps each position to an icon index + return keypadIndices.map((iconIdx: number) => iconToSvg(icons[iconIdx])) +} + +/** Convert an icon object to an SVG/HTML string for rendering. */ +function iconToSvg(icon: NKodeIcon): string { + if (!icon) return '' + if (icon.file_type === 'svg') { + return icon.img_data + } + // For non-SVG: render as img with base64 + const mime = icon.file_type === 'png' ? 'image/png' + : icon.file_type === 'webp' ? 'image/webp' + : 'image/jpeg' + return `${icon.file_name}` +} diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx deleted file mode 100644 index a396bd2..0000000 --- a/src/pages/LoginPage.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { useState, useCallback } from 'react'; -import { useNavigate, Link } from 'react-router-dom'; -import { Keypad } from '@/components/Keypad'; -import { useAuth } from '@/hooks/useAuth'; -import { loginCode } from '@/lib/nkode-client'; -import { ROUTES } from '@/lib/types'; - -type Step = 'email' | 'keypad'; - -export function LoginPage() { - const [step, setStep] = useState('email'); - const [email, setEmail] = useState(''); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - const [resetKey, setResetKey] = useState(0); - const { login } = useAuth(); - const navigate = useNavigate(); - - const handleEmailSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (!email.trim()) return; - setError(''); - setStep('keypad'); - }; - - const handleKeypadComplete = useCallback( - async (digits: number[]) => { - setLoading(true); - setError(''); - try { - const passcode = new Uint8Array(digits); - const session = await loginCode(email, passcode); - login(email, session); - navigate(ROUTES.HOME); - } catch (err: any) { - setError(err?.message || 'Authentication failed'); - setResetKey((k) => k + 1); - } finally { - setLoading(false); - } - }, - [email, login, navigate] - ); - - return ( -
-
- {/* Logo */} -
-

- nKode -

-

- {step === 'email' ? 'Sign in to your account' : 'Enter your nKode'} -

-
- - {step === 'email' ? ( -
-
- - setEmail(e.target.value)} - placeholder="you@example.com" - autoFocus - required - className="w-full px-4 py-2.5 rounded-xl border border-slate-200 dark:border-slate-700 - bg-white dark:bg-slate-800 text-slate-900 dark:text-white - focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 - placeholder:text-slate-400 dark:placeholder:text-slate-500" - /> -
- -

- Don't have an account?{' '} - - Sign up - -

-
- ) : ( -
- - - {loading && ( -

- Authenticating… -

- )} -
- )} -
-
- ); -} diff --git a/src/pages/NotFoundPage.tsx b/src/pages/NotFoundPage.tsx new file mode 100644 index 0000000..679162b --- /dev/null +++ b/src/pages/NotFoundPage.tsx @@ -0,0 +1,13 @@ +import { Link } from 'react-router-dom' + +export default function NotFoundPage() { + return ( +
+

404

+

Page not found

+ + ← Back to Home + +
+ ) +} diff --git a/src/pages/SignupKeypadPage.tsx b/src/pages/SignupKeypadPage.tsx new file mode 100644 index 0000000..bdfb796 --- /dev/null +++ b/src/pages/SignupKeypadPage.tsx @@ -0,0 +1,165 @@ +/** + * Signup keypad page — code registration flow. + * Displays icons for the user to select their nKode sequence. + * On submit, completes OPAQUE code registration and stores login data. + */ +import { useState, useCallback } from 'react' +import { useLocation, useNavigate, Navigate } from 'react-router-dom' +import { useAuth } from '../hooks/useAuth' +import * as api from '../services/api' +import type { NKodeIcon } from '../types' + +interface LocationState { + email: string + secretKeyHex: string + userId: string + icons: NKodeIcon[] +} + +/** Default keypad dimensions (matching Flutter: 9 props/key, 6 keys) */ +const ATTRS_PER_KEY = 9 +const NUM_KEYS = 6 + +export default function SignupKeypadPage() { + const location = useLocation() + const navigate = useNavigate() + const auth = useAuth() + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [secretKeyCopied, setSecretKeyCopied] = useState(false) + + const state = location.state as LocationState | null + if (!state?.email || !state?.icons) { + return + } + + const { email, secretKeyHex, userId, icons } = state + + // Build SVG strings from icons for the keypad + const svgs = icons.map((icon: NKodeIcon) => iconToSvg(icon)) + + const handleCopyKey = useCallback(async () => { + try { + await navigator.clipboard.writeText(secretKeyHex) + setSecretKeyCopied(true) + setTimeout(() => setSecretKeyCopied(false), 3000) + } catch { + // Fallback: select text + setSecretKeyCopied(false) + } + }, [secretKeyHex]) + + const handleSetNKode = async (pressedKeys: number[]) => { + setLoading(true) + setError(null) + try { + // Convert key presses to global icon indices + // Each key press corresponds to keys on the keypad. + // The selectedIndices for completeCodeRegistration are the KEY indices pressed. + // The WASM SDK handles mapping keys to icon indices internally. + await api.completeCodeRegistrationWithEmail(email, pressedKeys) + + // Save credentials and navigate home + auth.login(email, secretKeyHex, userId) + navigate('/home', { replace: true }) + } catch (err) { + setError(err instanceof Error ? err.message : 'Registration failed') + } finally { + setLoading(false) + } + } + + return ( +
+
+
+ +

Set Your nKode

+
+

{email}

+ + {/* Secret key display — user must save this */} +
+

+ Save your secret key — you'll need it to log in: +

+
+ + {secretKeyHex} + + +
+
+ +

+ Tap keys to create your nKode pattern, then submit. +

+
+ + {error && ( +
+
+ {error} +
+
+ )} + + +
+ ) +} + +/** Convert an icon object to an SVG/HTML string. */ +function iconToSvg(icon: NKodeIcon): string { + if (!icon) return '' + if (icon.file_type === 'svg') { + return icon.img_data + } + const mime = icon.file_type === 'png' ? 'image/png' + : icon.file_type === 'webp' ? 'image/webp' + : 'image/jpeg' + return `${icon.file_name}` +} + +/** + * Signup keypad — reuses the same keypad UI but allows selecting + * icons to define the nKode pattern. + */ +import Keypad from '../components/Keypad' + +interface SignupKeypadProps { + svgs: string[] + attrsPerKey: number + numbOfKeys: number + onComplete: (selection: number[]) => void + loading: boolean +} + +function SignupKeypad({ svgs, attrsPerKey, numbOfKeys, onComplete, loading }: SignupKeypadProps) { + return ( + + ) +} diff --git a/src/pages/SignupPage.tsx b/src/pages/SignupPage.tsx deleted file mode 100644 index 7ef9ca3..0000000 --- a/src/pages/SignupPage.tsx +++ /dev/null @@ -1,277 +0,0 @@ -import { useState, useCallback } from 'react'; -import { useNavigate, Link } from 'react-router-dom'; -import { Keypad } from '@/components/Keypad'; -import { useAuth } from '@/hooks/useAuth'; -import { registerCode, loginCode, generateSecretKey, registerKey } from '@/lib/nkode-client'; -import { ROUTES } from '@/lib/types'; - -type Step = 'email' | 'method' | 'keypad' | 'confirm' | 'key-show' | 'done'; - -export function SignupPage() { - const [step, setStep] = useState('email'); - const [email, setEmail] = useState(''); - const [_method, setMethod] = useState<'code' | 'key'>('code'); - const [firstCode, setFirstCode] = useState([]); - const [secretKey, setSecretKey] = useState(''); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - const [resetKey, setResetKey] = useState(0); - const { login } = useAuth(); - const navigate = useNavigate(); - - const handleEmailSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (!email.trim()) return; - setError(''); - setStep('method'); - }; - - const handleMethodSelect = (m: 'code' | 'key') => { - setMethod(m); - if (m === 'code') { - setStep('keypad'); - } else { - // Generate key immediately - const key = generateSecretKey(); - setSecretKey(key); - setStep('key-show'); - } - }; - - const handleFirstCode = useCallback((digits: number[]) => { - setFirstCode(digits); - setResetKey((k) => k + 1); - setStep('confirm'); - }, []); - - const handleConfirmCode = useCallback( - async (digits: number[]) => { - // Check codes match - if (digits.join('') !== firstCode.join('')) { - setError("Codes don't match. Try again."); - setResetKey((k) => k + 1); - return; - } - - setLoading(true); - setError(''); - try { - const passcode = new Uint8Array(digits); - await registerCode(email, passcode); - // Auto-login after registration - const session = await loginCode(email, passcode); - login(email, session); - navigate(ROUTES.HOME); - } catch (err: any) { - setError(err?.message || 'Registration failed'); - setResetKey((k) => k + 1); - } finally { - setLoading(false); - } - }, - [email, firstCode, login, navigate] - ); - - const handleKeyRegister = async () => { - setLoading(true); - setError(''); - try { - await registerKey(email, secretKey); - setStep('done'); - } catch (err: any) { - setError(err?.message || 'Registration failed'); - } finally { - setLoading(false); - } - }; - - return ( -
-
- {/* Logo */} -
-

- nKode -

-

- {step === 'email' && 'Create your account'} - {step === 'method' && 'Choose your authentication method'} - {step === 'keypad' && 'Create your nKode'} - {step === 'confirm' && 'Confirm your nKode'} - {step === 'key-show' && 'Save your secret key'} - {step === 'done' && 'You\'re all set!'} -

-
- - {step === 'email' && ( -
-
- - setEmail(e.target.value)} - placeholder="you@example.com" - autoFocus - required - className="w-full px-4 py-2.5 rounded-xl border border-slate-200 dark:border-slate-700 - bg-white dark:bg-slate-800 text-slate-900 dark:text-white - focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 - placeholder:text-slate-400 dark:placeholder:text-slate-500" - /> -
- -

- Already have an account?{' '} - - Sign in - -

-
- )} - - {step === 'method' && ( -
- - - -
- )} - - {step === 'keypad' && ( -
- - -
- )} - - {step === 'confirm' && ( -
- - - {loading && ( -

- Creating account… -

- )} -
- )} - - {step === 'key-show' && ( -
- -
-

- ⚠️ Save this key — you won't see it again! -

- - {secretKey} - -
- - {error &&

{error}

} - -
- )} - - {step === 'done' && ( -
-
-

- Your account has been created with key-based auth. -

- - Sign in - -
- )} -
-
- ); -} diff --git a/src/pages/SusiPage.tsx b/src/pages/SusiPage.tsx new file mode 100644 index 0000000..25828d5 --- /dev/null +++ b/src/pages/SusiPage.tsx @@ -0,0 +1,187 @@ +/** + * SUSI (Sign Up / Sign In) page. + * Login tab: email → OPAQUE key login → navigate to keypad for code login. + * Signup tab: email → OPAQUE key register → key login → navigate to keypad for code registration. + */ +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import * as api from '../services/api' + +export default function SusiPage() { + const [tab, setTab] = useState<'login' | 'signup'>('login') + + return ( +
+ {/* Logo area */} +
+
+ nK +
+

nKode

+

Passwordless Authentication

+
+ + {/* Tab bar */} +
+ + +
+ + {/* Tab content */} + {tab === 'login' ? : } +
+ ) +} + +/** Login tab — email + secret key → OPAQUE key login → code login keypad */ +function LoginTab() { + const [email, setEmail] = useState('') + const [secretKeyHex, setSecretKeyHex] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const navigate = useNavigate() + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + const trimmedEmail = email.trim() + const trimmedKey = secretKeyHex.trim() + if (!trimmedEmail) { setError('Enter an email'); return } + if (!trimmedKey || trimmedKey.length !== 32) { + setError('Enter your 32-character secret key (hex)') + return + } + + setLoading(true) + setError(null) + try { + // Step 1: OPAQUE key login + const session = await api.loginKey(trimmedEmail, trimmedKey) + + // Step 2: Prepare code login (fetch login data + reconstruct keypad) + const codeLoginData = await api.prepareCodeLogin(session.userId, trimmedKey) + + // Navigate to keypad page for code login + navigate('/login-keypad', { + state: { + email: trimmedEmail, + secretKeyHex: trimmedKey, + userId: session.userId, + codeLoginData, + }, + }) + } catch (err) { + setError(err instanceof Error ? err.message : 'Login failed') + } finally { + setLoading(false) + } + } + + return ( +
+ setEmail(e.target.value)} + className="w-full px-4 py-3 rounded-lg bg-zinc-800 border border-zinc-700 text-white placeholder-zinc-500 focus:outline-none focus:border-emerald-500" + /> + setSecretKeyHex(e.target.value)} + className="w-full px-4 py-3 rounded-lg bg-zinc-800 border border-zinc-700 text-white placeholder-zinc-500 focus:outline-none focus:border-emerald-500 font-mono" + /> + {error &&

{error}

} + +
+ ) +} + +/** Sign Up tab — email → generate key → OPAQUE key register + key login → code registration keypad */ +function SignupTab() { + const [email, setEmail] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const navigate = useNavigate() + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + const trimmedEmail = email.trim() + if (!trimmedEmail) { setError('Enter an email'); return } + + setLoading(true) + setError(null) + try { + // Step 1: Generate secret key + const secretKeyHex = api.generateSecretKey() + + // Step 2: OPAQUE key registration + await api.registerKey(trimmedEmail, secretKeyHex) + + // Step 3: OPAQUE key login (needed for authenticated endpoints) + const session = await api.loginKey(trimmedEmail, secretKeyHex) + + // Step 4: Prepare code registration (fetch icons) + const iconsData = await api.prepareCodeRegistration() + + // Navigate to keypad page for code registration + navigate('/signup-keypad', { + state: { + email: trimmedEmail, + secretKeyHex, + userId: session.userId, + icons: iconsData.icons, + }, + }) + } catch (err) { + setError(err instanceof Error ? err.message : 'Registration failed') + } finally { + setLoading(false) + } + } + + return ( +
+ setEmail(e.target.value)} + className="w-full px-4 py-3 rounded-lg bg-zinc-800 border border-zinc-700 text-white placeholder-zinc-500 focus:outline-none focus:border-emerald-500" + /> + {error &&

{error}

} + +
+ ) +} diff --git a/src/services/api.ts b/src/services/api.ts new file mode 100644 index 0000000..b2b3ac8 --- /dev/null +++ b/src/services/api.ts @@ -0,0 +1,112 @@ +/** + * nKode API service using WASM client SDK for OPAQUE crypto. + * All OPAQUE operations are performed client-side via WebAssembly. + */ +import { NKodeClient } from 'nkode-client-wasm' +import type { NKodeSession, IconsResponse, CodeLoginData } from '../types' + +const API_BASE = import.meta.env.VITE_API_URL || '' + +let client: NKodeClient | null = null + +/** Initialize WASM module and create client. Must be called once before use. */ +export async function initClient(): Promise { + // With vite-plugin-wasm + top-level-await, WASM is auto-initialized on import. + if (!client) { + client = new NKodeClient(API_BASE) + } + return client +} + +/** Get or create a fresh client (call after initClient). */ +export function getClient(): NKodeClient { + if (!client) throw new Error('Call initClient() first') + return client +} + +/** Reset the client (e.g. on logout). */ +export function resetClient(): void { + if (client) { + client.clearSession() + client.free() + } + client = null +} + +/** Generate a new random secret key (hex string). */ +export function generateSecretKey(): string { + return NKodeClient.generateSecretKey() +} + +// ── Registration Flow ── + +/** Register key-based auth via OPAQUE. */ +export async function registerKey(email: string, secretKeyHex: string): Promise { + const c = getClient() + await c.registerKey(email, secretKeyHex) +} + +/** Login key-based auth via OPAQUE. Returns session info. */ +export async function loginKey(email: string, secretKeyHex: string): Promise { + const c = getClient() + return await c.loginKey(email, secretKeyHex) as NKodeSession +} + +/** Register code-based auth via OPAQUE. */ +export async function registerCode(email: string, passcodeBytes: Uint8Array): Promise { + const c = getClient() + await c.registerCode(email, passcodeBytes) +} + +/** Login code-based auth via OPAQUE. Returns session info. */ +export async function loginCode(email: string, passcodeBytes: Uint8Array): Promise { + const c = getClient() + return await c.loginCode(email, passcodeBytes) as NKodeSession +} + +// ── Icon & Login Data Flows ── + +/** Prepare icons for code registration (after key login). */ +export async function prepareCodeRegistration(): Promise { + const c = getClient() + return await c.prepareCodeRegistration() as IconsResponse +} + +/** Complete code registration with selected icon indices. */ +export async function completeCodeRegistrationWithEmail( + email: string, + selectedIndices: number[], +): Promise { + const c = getClient() + await c.completeCodeRegistrationWithEmail(email, new Uint32Array(selectedIndices)) +} + +/** Prepare code login: fetch login data, reconstruct keypad. */ +export async function prepareCodeLogin( + userId: string, + secretKeyHex: string, +): Promise { + const c = getClient() + return await c.prepareCodeLogin(userId, secretKeyHex) as CodeLoginData +} + +/** Decipher key selections into passcode bytes for code login. */ +export function decipherSelection( + secretKeyHex: string, + loginDataJson: string, + keySelections: number[], +): Uint8Array { + const c = getClient() + return c.decipherSelection(secretKeyHex, loginDataJson, new Uint32Array(keySelections)) +} + +/** Get user ID from current session. */ +export function getUserId(): string | undefined { + const c = getClient() + return c.getUserId() +} + +/** Check if client has an active session. */ +export function hasSession(): boolean { + return client?.hasSession() ?? false +} diff --git a/src/services/auth.ts b/src/services/auth.ts new file mode 100644 index 0000000..cdc79f7 --- /dev/null +++ b/src/services/auth.ts @@ -0,0 +1,23 @@ +/** + * Auth state management — persists email + secretKey to localStorage. + * Uses OPAQUE-based auth via WASM client (no tokens stored). + */ +import type { AuthState } from '../types' + +const STORAGE_KEY = 'nkode_auth' + +export function loadAuth(): AuthState { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (raw) return JSON.parse(raw) as AuthState + } catch { /* ignore corrupt data */ } + return { email: null, secretKeyHex: null, userId: null } +} + +export function saveAuth(state: AuthState): void { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)) +} + +export function clearAuth(): void { + localStorage.removeItem(STORAGE_KEY) +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..e36226d --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,36 @@ +/** Auth state stored in localStorage */ +export interface AuthState { + email: string | null + secretKeyHex: string | null + userId: string | null +} + +/** Icon from the WASM client */ +export interface NKodeIcon { + file_name: string + file_type: string + img_data: string +} + +/** Response from prepareCodeRegistration */ +export interface IconsResponse { + icons: NKodeIcon[] +} + +/** Response from prepareCodeLogin */ +export interface CodeLoginData { + keypadIndices: number[] + propertiesPerKey: number + numberOfKeys: number + mask: number[] + icons: NKodeIcon[] + loginDataJson: string +} + +/** Session returned from loginKey / loginCode */ +export interface NKodeSession { + sessionId: string + userId: string + createdAt: string + expiresAt: string +} diff --git a/src/types/nkode-client-wasm.d.ts b/src/types/nkode-client-wasm.d.ts deleted file mode 100644 index cd20b93..0000000 --- a/src/types/nkode-client-wasm.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Type stub for the nKode WASM package. - * Will be replaced by real types when the package is linked via `bun link`. - */ -declare module 'nkode-client-wasm' { - export class NKodeClient { - constructor(base_url: string); - loginCode(email: string, passcode_bytes: Uint8Array): Promise; - loginKey(email: string, secret_key_hex: string): Promise; - registerCode(email: string, passcode_bytes: Uint8Array): Promise; - registerKey(email: string, secret_key_hex: string): Promise; - static generateSecretKey(): string; - free(): void; - } - - export function init(): void; - - export default function __wbg_init(module_or_path?: any): Promise; -} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..8ab4f00 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_BASE_URL: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/src/wasm.d.ts b/src/wasm.d.ts new file mode 100644 index 0000000..052cd36 --- /dev/null +++ b/src/wasm.d.ts @@ -0,0 +1,26 @@ +declare module 'nkode-client-wasm' { + export class NKodeClient { + constructor(base_url: string) + static generateSecretKey(): string + registerKey(email: string, secret_key_hex: string): Promise + loginKey(email: string, secret_key_hex: string): Promise + registerCode(email: string, passcode_bytes: Uint8Array): Promise + loginCode(email: string, passcode_bytes: Uint8Array): Promise + hasSession(): boolean + getUserId(): string | undefined + clearSession(): void + free(): void + getNewIcons(count: number): Promise + setIcons(icons_json: string): Promise + getLoginData(user_id: string): Promise + postLoginData(login_data_json: string): Promise + updateLoginData(login_data_json: string): Promise + prepareCodeRegistration(): Promise + completeCodeRegistration(selected_indices: Uint32Array): Promise + completeCodeRegistrationWithEmail(email: string, selected_indices: Uint32Array): Promise + prepareCodeLogin(user_id: string, secret_key_hex: string): Promise + decipherSelection(secret_key_hex: string, login_data_json: string, key_selections: Uint32Array): Uint8Array + } + + export function init(): void +} diff --git a/tsconfig.app.json b/tsconfig.app.json index 5db930a..c328724 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -8,7 +8,6 @@ "types": ["vite/client"], "skipLibCheck": true, - /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, @@ -16,17 +15,12 @@ "noEmit": true, "jsx": "react-jsx", - /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true, - "baseUrl": ".", - "paths": { - "@/*": ["src/*"] - } + "noUncheckedSideEffectImports": true }, "include": ["src"] } diff --git a/vite.config.ts b/vite.config.ts index 4d96565..6bed9d1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,12 +1,36 @@ +/// import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' +import wasm from 'vite-plugin-wasm' +import topLevelAwait from 'vite-plugin-top-level-await' +import path from 'path' export default defineConfig({ - plugins: [react(), tailwindcss()], + plugins: [react(), tailwindcss(), wasm(), topLevelAwait()], + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./src/__tests__/setup.ts'], + css: true, + }, resolve: { alias: { - '@': '/src', + 'nkode-client-wasm': path.resolve(__dirname, 'pkg'), }, }, + server: { + proxy: { + '/v1': { + target: 'http://localhost:3000', + changeOrigin: true, + }, + }, + fs: { + allow: ['..'], + }, + }, + optimizeDeps: { + exclude: ['nkode-client-wasm'], + }, })