diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fe55d95..d38558d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,15 +2,7 @@ name: Build on: push: branches: [main, next] - paths-ignore: - - "**.md" - - "**.txt" - - "docs/**" pull_request: - paths-ignore: - - "**.md" - - "**.txt" - - "docs/**" permissions: contents: read @@ -23,8 +15,37 @@ env: CARGO_PROFILE_DEV_DEBUG: 0 jobs: + # Pre-flight: detect whether any non-docs files changed. We deliberately + # trigger this workflow on EVERY push/PR (no top-level paths-ignore) so + # required status checks always report a state — `paths-ignore` at the + # workflow level prevents the workflow from running at all on docs-only + # PRs, which leaves required checks stuck in "Expected" forever and blocks + # merging. Instead, every downstream job gates on this filter's output and + # reports `skipped` when there's nothing relevant to build (skipped jobs + # satisfy required checks). + changes: + name: Detect relevant changes + runs-on: ubuntu-24.04 + outputs: + non_docs: ${{ steps.filter.outputs.non_docs }} + steps: + - uses: actions/checkout@v6 + - uses: dorny/paths-filter@v3 + id: filter + with: + # `non_docs` matches whenever a changed file is NOT a doc/text file + # under one of the listed paths. Mirror of the previous + # `paths-ignore` set. + filters: | + non_docs: + - '!**/*.md' + - '!**/*.txt' + - '!docs/**' + build-wasm: name: Build Client for Wasm + needs: [changes] + if: needs.changes.outputs.non_docs == 'true' runs-on: ubuntu-24.04 env: # See test.yml's build-web-client-dist-folder for rationale. diff --git a/.github/workflows/check-publish.yml b/.github/workflows/check-publish.yml new file mode 100644 index 0000000..de5690e --- /dev/null +++ b/.github/workflows/check-publish.yml @@ -0,0 +1,109 @@ +name: Check Publish +permissions: + contents: read +on: + push: + branches: [main, next] + paths-ignore: + - "**.md" + - "**.txt" + - "docs/**" + pull_request: + paths-ignore: + - "**.md" + - "**.txt" + - "docs/**" + +concurrency: + group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" + cancel-in-progress: true + +env: + CARGO_PROFILE_DEV_DEBUG: 0 + RUST_CACHE_KEY: rust-cache-2026.02.18 + +jobs: + check-publish: + # Runs publint + arethetypeswrong (attw) against the three published + # packages: @miden-sdk/miden-sdk (WASM web-client), @miden-sdk/react, + # @miden-sdk/vite-plugin. Each tool runs against the actual `pnpm pack` + # tarball, so the dist/ output must exist first — hence we build all + # three packages before invoking the gates. + # + # WASM build setup mirrors test.yml's build-web-client-dist-folder job + # (sccache + rust-cache + binaryen + MIDEN_FAST_BUILD on PRs) so PR CI + # stays in the same ballpark. + name: publint + attw + runs-on: ubuntu-24.04 + env: + SCCACHE_GHA_ENABLED: "true" + steps: + - uses: actions/checkout@v6 + + - name: Install Rust (needed for WASM build) + run: | + rustup update --no-self-update + rustup target add wasm32-unknown-unknown + + # See test.yml for why these are forwarded explicitly. + - name: Configure sccache cache backend (v2) + uses: actions/github-script@v7 + with: + script: | + core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Run sccache + uses: mozilla-actions/sccache-action@v0.0.10 + continue-on-error: true + id: sccache-install + + - name: Probe sccache and enable wrapper if healthy + if: steps.sccache-install.outcome == 'success' && env.SCCACHE_PATH != '' + shell: bash + run: | + probe_out=$("$SCCACHE_PATH" --start-server 2>&1) && rc=0 || rc=$? + echo "$probe_out" + if [ $rc -eq 0 ]; then + echo "RUSTC_WRAPPER=$SCCACHE_PATH" >> "$GITHUB_ENV" + echo "sccache enabled as RUSTC_WRAPPER ($SCCACHE_PATH)" + else + echo "sccache --start-server failed (rc=$rc); running without wrapper" + fi + + - name: Install binaryen (wasm-opt) + run: | + sudo apt-get update && sudo apt-get install -y binaryen + + - name: Add Rust Cache + uses: Swatinem/rust-cache@v2 + with: + shared-key: rust-wasm + prefix-key: ${{ env.RUST_CACHE_KEY }} + save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next' }} + + - name: Install pnpm + uses: pnpm/action-setup@v3 + with: + version: 9 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + # MIDEN_FAST_BUILD skips wasm-opt and uses the release-fast cargo + # profile. The publint/attw gates inspect the package shape and type + # surface, not the optimization level of the WASM blob, so the fast + # profile is fine here. The canonical full-optimization build is + # exercised by test.yml's verify-release-build job on push. + - name: Run check:publish (build + publint + attw) + run: MIDEN_FAST_BUILD=true pnpm run check:publish + + - name: Show sccache stats + if: always() + run: sccache --show-stats || true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6140dba..8f27b39 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -54,6 +54,25 @@ jobs: run: pnpm install --no-frozen-lockfile - run: make rust-client-ts-lint + knip: + name: Knip (unused code/deps) + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + - name: Install pnpm + uses: pnpm/action-setup@v3 + with: + version: 9 + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: 'pnpm' + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + - name: Knip + run: pnpm run check:knip + clippy-wasm: name: Clippy WASM runs-on: ubuntu-24.04 diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/.prettierignore b/.prettierignore index 02a91c0..c1cf928 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,5 +2,16 @@ *.yml *.yaml +# Build / tooling output +**/dist/** +**/target/** +**/node_modules/** + +# Generated TypeScript declarations +**/*.d.ts + # Generated JS (codegen) crates/idxdb-store/src/js/** + +# Vendored docs +docs/** diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..39021b4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,127 @@ +# CLAUDE.md — repo notes for AI agents + +Conventions and tooling notes for `0xMiden/web-sdk`. End-user docs live in [README.md](README.md); per-package usage guides live alongside the packages (e.g. [`packages/react-sdk/CLAUDE.md`](packages/react-sdk/CLAUDE.md)). + +## What this repo is + +A pnpm monorepo holding the JS / WASM / React bits previously part of [`0xMiden/miden-client`](https://github.com/0xMiden/miden-client). Five published artifacts: + +| Artifact | Path | Registry | +|---|---|---| +| `@miden-sdk/miden-sdk` | `crates/web-client/` (Rust + WASM + JS bindings) | npm | +| `@miden-sdk/react` | `packages/react-sdk/` | npm | +| `@miden-sdk/vite-plugin` | `packages/vite-plugin/` | npm | +| `@miden-sdk/node-{darwin-arm64,darwin-x64,linux-x64-gnu}` | `packages/node-sdk-*` | npm (platform-specific native binaries; consumed via `optionalDependencies` on `@miden-sdk/miden-sdk`) | +| `miden-idxdb-store` | `crates/idxdb-store/` | crates.io | + +The `Cargo.toml` workspace dep `miden-client = "x.y.z"` pins compatibility with the upstream Rust crate. Changes to shared types (Account, Note, gRPC schema, …) usually need a coordinated PR in `0xMiden/miden-client` first. + +## Toolchain + +- **Package manager**: pnpm 9 (workspace at `pnpm-workspace.yaml`). **Never** use `yarn` or `npm install` — they will desync the lockfile. +- **Node**: ≥ 20 (`engines.node` in `package.json`, `.nvmrc`). +- **Rust**: stable 1.93 + nightly (for `cargo +nightly fmt`, `clippy`, and `fix`). Pinned in `rust-toolchain.toml`. +- **Lefthook** runs pre-commit; `pnpm install` wires it via the `prepare` script. + +## Build / lint / test + +Drive everything through the `Makefile` — never call `cargo fmt` directly (the project requires nightly + an exact prettier/eslint pass that vanilla `cargo fmt` skips). + +```bash +make help # list targets + +# Build +make build-wasm # WASM crates only (wasm32-unknown-unknown) +make build-web-client # WASM + JS bindings + dist +make build-react-sdk # everything @miden-sdk/react needs + +# Lint + format +make format # nightly cargo fmt + prettier write + eslint --fix +make format-check # CI form (no writes) +make clippy-wasm # clippy for both WASM crates +make typos-check # spellcheck +make lint # umbrella: fix-wasm + format + clippy-wasm + typos + checks +make web-client-check-methods # verifies every WASM method is classified in the JS proxy + +# Test +make test-coverage # all coverage gates (react-sdk + idxdb-store + vite-plugin + web-client unit) +make test-react-sdk # vitest unit (jsdom) +make test-web-client-unit # vitest unit (web-client) +make integration-test-web-client # playwright (chromium); accepts SHARD_PARAMETER +make integration-test-web-client-webkit +``` + +CI (`.github/workflows/test.yml`) runs all of the above on every PR. `main` and `next` warm sccache + Swatinem/rust-cache. + +## Coverage thresholds + +`packages/react-sdk/vitest.config.ts` enforces `lines / branches / functions / statements ≥ 95`. Two files are excluded because they require the real WASM binary and are covered by Playwright integration tests: + +- `src/utils/accountBech32.ts` — covered by `test/accountBech32.test.ts` +- `src/hooks/useAssetMetadata.ts` — covered by `test/useAssetMetadata.test.ts` + +**Always run `make test-react-sdk` locally before pushing** — CI will block the merge if any threshold dips. Lowering thresholds is not the right fix; either add tests or move the file to the excluded list with justification. + +## WASM concurrency: `runExclusive` + +The wasm-bindgen `WebClient` is **not** safe under concurrent access. Calls that go through it from multiple call sites must serialize via the AsyncLock exposed by `MidenProvider`: + +```ts +const { runExclusive } = useMiden(); +await runExclusive(async (client) => { /* … */ }); +``` + +Symptom of a violation: `Error: recursive use of an object detected which would lead to unsafe aliasing in rust`. The `crates/web-client/test/sync_lock.test.ts` integration test guards against regressions — if you add a hook that touches the client, route it through `runExclusive` (or one of the existing serialized helpers) or the lock test will fail. + +## Eager vs lazy entry points + +`@miden-sdk/miden-sdk` ships two entry points with identical APIs but different init behaviour: + +| Specifier | When WASM loads | Use when | +|---|---|---| +| `@miden-sdk/miden-sdk` | At import (top-level await) | Vite/Webpack browser bundles where TLA is fine | +| `@miden-sdk/miden-sdk/lazy` | On first `await MidenClient.ready()` (or first awaited SDK method) | SSR (Next.js, Remix, SvelteKit), Capacitor WKWebView hosts, anywhere TLA is unsafe | + +Same split applies to `@miden-sdk/react` (`react/lazy` pulls `miden-sdk/lazy`). The eager/lazy contract is guarded by `crates/web-client/test/eager_entry.test.ts` — if you change the public API in one entry, mirror it in the other and re-run the type-check scripts under `crates/web-client/scripts/`. + +## Releases + +Two long-lived branches: + +- **`main`** → npm `latest` dist-tag. Released on GitHub release events. +- **`next`** → npm `next` dist-tag. Released when a PR merges into `next` carrying the `patch release` label. + +Both branches have protection enabled; required status checks mirror across the two. + +The release-publish gate compares the local `package.json` version against the **npm registry** (not against the previous git commit) — see `scripts/check-{web-client,react-sdk,vite-plugin}-version-release.sh`. So a release tag publishes whichever of the four packages have versions not yet on npm; bumping a single package is a clean release of just that one. + +WASM size is gated at 25 MB in the publish workflow — if `wasm-opt` ever silently fails, the bloated binary never reaches npm. + +Crate publishing (`miden-idxdb-store`, `miden-client-web`) goes through `.github/workflows/publish-crates-release.yml` and uses the `CARGO_REGISTRY_TOKEN` org secret. + +## Gotchas worth remembering + +- **No yarn.** The repo migrated from yarn to pnpm. If you see a doc, comment, or script that says `yarn ...`, it's stale — fix it (or flag it). +- **Don't chain `pnpm --filter ... -- arg` through npm-script `&&`.** pnpm's argument forwarding only wires through to the LAST command in the chain. The Makefile splits multi-step playwright invocations across explicit Make recipes for this reason; preserve that pattern (see `integration-test-web-client` in `Makefile`). +- **Test sharding is manually balanced.** `packages/react-sdk/playwright.config.ts` defines four CI shard projects (`ci-shard-1` … `ci-shard-4`) with explicit `testMatch` arrays sized empirically from observed run timings. Rebalance by moving file paths between arrays — no workflow edits needed. Comment block at the top of the config explains the history. +- **Network-bound tests don't belong in CI.** Anything that hits a live RPC node (testnet/devnet) is excluded. If you add such a test, gate it on an env var and skip by default. +- **Account ID display.** Hooks accept hex (`0x…`) and bech32 (`mtst1q…`) interchangeably. Bech32 prefix tracks the active network — `mtst1` for testnet/devnet, `mid1` for mainnet (when it lands). Don't hardcode prefixes. + +## Cross-repo coordination + +| Concern | Repo | +|---|---| +| Shared Rust types, gRPC schema, `MidenClient` semantics | [`0xMiden/miden-client`](https://github.com/0xMiden/miden-client) | +| Account compiler, MASM standard library, base protocol types | [`0xMiden/miden-base`](https://github.com/0xMiden/miden-base) | +| MidenFi browser-extension wallet adapter | [`0xMiden/miden-wallet-adapter`](https://github.com/0xMiden/miden-wallet-adapter) | +| Para signer integration | [`0xMiden/miden-para`](https://github.com/0xMiden/miden-para) | +| Turnkey signer integration | [`0xMiden/miden-turnkey`](https://github.com/0xMiden/miden-turnkey) | + +PRs that touch the WASM/JS boundary often need a synchronized PR in miden-client — bump the workspace dep and verify the integration tests still pass. + +## Contributing checklist + +1. `make lint` clean. +2. `make test-coverage` clean (and locally verify thresholds before pushing). +3. For changes to public API: update the relevant per-package CLAUDE.md (e.g. `packages/react-sdk/CLAUDE.md` for hook signatures) and the type-check scripts under `crates/web-client/scripts/`. +4. For changes to release flow: cross-check both `publish-web-client-release.yml` (latest channel) and `publish-web-client-next.yml` (next channel) — they intentionally mirror each other. diff --git a/README.md b/README.md index b7cbb20..3d4419d 100644 --- a/README.md +++ b/README.md @@ -111,15 +111,76 @@ export default defineConfig({ ## Eager vs lazy entry points -The SDK ships with two parallel entry points so you can trade WASM init time against bundle reach: +The SDK ships with two parallel entry points with an identical public API. They differ only in **when** the WASM module is initialized: | Specifier | When it loads WASM | Use this when | |---|---|---| -| `@miden-sdk/miden-sdk` | At import (top-level await) | Server-rendered apps where the user will definitely use the SDK | -| `@miden-sdk/miden-sdk/lazy` | Deferred until first method call | Apps where most users never touch crypto (multi-megabyte WASM stays uncached until needed) | +| `@miden-sdk/miden-sdk` | At import (top-level `await`) | Plain browser apps with a synchronous bundler (Vite, CRA, esbuild, Webpack client bundles). After `import` resolves, every wasm-bindgen constructor (`new Felt(…)`, `AccountId.fromHex(…)`, `TransactionProver.newLocalProver()`, etc.) is safe to call synchronously — no `await MidenClient.ready()` needed. | +| `@miden-sdk/miden-sdk/lazy` | Only when you ask — via `await MidenClient.ready()`, or implicitly the first time you `await` an SDK method that needs WASM | Anywhere top-level `await` is unsafe or you want to control when to pay the WASM-init cost: **server-side rendering** (Next.js, Remix, SvelteKit), **Capacitor WKWebView hosts** (the iOS/Android scheme handler hangs on TLA), and any code path where you want to defer the multi-megabyte WASM download until the user actually performs a crypto-touching action. | + +### Using the lazy entry: `await MidenClient.ready()` first + +The lazy entry runs no top-level `await`, so **until you await initialization, every wasm-bindgen type is just a stub**. Calling `new Felt(…)` or `AccountId.fromHex(…)` before WASM is ready throws `TypeError: Cannot read properties of undefined`. + +The contract is: + +```typescript +import { MidenClient, AccountId, Felt } from "@miden-sdk/miden-sdk/lazy"; + +// Stubs — DO NOT touch wasm-bindgen types here: +// const id = AccountId.fromHex("0x…"); // ❌ throws + +// Initialize WASM exactly once (idempotent + concurrency-safe): +await MidenClient.ready(); + +// Now everything is real and synchronous: +const id = AccountId.fromHex("0x…"); // ✓ +const felt = new Felt(42n); // ✓ +``` + +`MidenClient.ready()` is idempotent: concurrent callers share the same in-flight promise, and post-init callers resolve immediately from cache. Call it from `MidenProvider`, route loaders, button handlers — wherever the first WASM use is guarded. + +You only need to call it explicitly when you're constructing wasm-bindgen types yourself. **Async SDK methods** (`client.accounts.create()`, `client.transactions.send()`, `MidenClient.createTestnet()`, etc.) await initialization internally, so importing them and calling them is enough — the first call transparently triggers WASM load. The same split applies to `@miden-sdk/react`. The choice cascades: if you use `@miden-sdk/react/lazy`, it pulls `@miden-sdk/miden-sdk/lazy` automatically; the eager variant pulls eager. +### React: gating on `isReady` from `useMiden()` + +The React SDK hides the `MidenClient.ready()` plumbing behind `MidenProvider` — you don't call `ready()` yourself. Instead, the provider initializes WASM (lazily on the `/lazy` entry, eagerly on the default), and exposes the readiness state through `useMiden()`: + +```tsx +import { MidenProvider, useMiden } from "@miden-sdk/react/lazy"; + +function App() { + return ( + + + + ); +} + +function Wallet() { + const { isReady, isInitializing, error } = useMiden(); + if (error) return
Failed to load: {error.message}
; + if (!isReady) return
Loading wallet…
; + // SDK is initialized — safe to call hooks that touch WASM + return ; +} +``` + +`useMiden()` returns: + +| Field | Type | Meaning | +| ---------------- | ----------------- | ---------------------------------------------------------------------- | +| `isInitializing` | `boolean` | WASM and client are being loaded. Show a loading UI. | +| `isReady` | `boolean` | Client is ready. SDK hooks (`useAccount`, `useSend`, …) are safe to use. | +| `error` | `Error \| null` | Initialization failed (network, WASM load, etc.). Show an error UI. | +| `client` | `WebClient \| null` | The underlying client, populated once `isReady === true`. | + +For zero-glue UI, pass `loadingComponent` and `errorComponent` (or `(err) => ReactNode`) props to `MidenProvider` — the provider renders them in place of children until the SDK is ready, and you can drop the `isReady` check in consumer hooks. + +The other SDK hooks (`useCreateWallet`, `useSend`, `useNotes`, etc.) all gate on `isReady` internally and return their own `isLoading` / `error` states, so you don't need to chain readiness checks through every component once you've gated at the top. + --- ## Architecture diff --git a/crates/idxdb-store/src/package.json b/crates/idxdb-store/src/package.json index 47a173e..3d8ca61 100644 --- a/crates/idxdb-store/src/package.json +++ b/crates/idxdb-store/src/package.json @@ -15,7 +15,6 @@ }, "devDependencies": { "@eslint/js": "^9.33.0", - "@typescript-eslint/eslint-plugin": "^8.39.1", "@types/semver": "^7.5.8", "@vitest/coverage-v8": "^3.0.0", "eslint": "^9.33.0", diff --git a/crates/idxdb-store/src/ts/accounts.test.ts b/crates/idxdb-store/src/ts/accounts.test.ts index de644ba..f865551 100644 --- a/crates/idxdb-store/src/ts/accounts.test.ts +++ b/crates/idxdb-store/src/ts/accounts.test.ts @@ -1,129 +1,1341 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, afterEach } from "vitest"; import { openDatabase, getDatabase } from "./schema.js"; -import { applyTransactionDelta, undoAccountStates } from "./accounts.js"; +import { + getAccountIds, + getAllAccountHeaders, + getAccountHeader, + getAccountHeaderByCommitment, + getAccountCode, + getAccountStorage, + getAccountStorageMaps, + getAccountVaultAssets, + getAccountAddresses, + upsertAccountCode, + upsertAccountStorage, + upsertStorageMapEntries, + upsertVaultAssets, + applyTransactionDelta, + applyFullAccountState, + upsertAccountRecord, + insertAccountAddress, + removeAccountAddress, + upsertForeignAccountCode, + getForeignAccountCode, + lockAccount, + pruneAccountHistory, + undoAccountStates, +} from "./accounts.js"; import { uniqueDbName } from "./test-utils.js"; -describe("Account delta and undo operations", () => { - // Use a consistent version so ensureClientVersion doesn't nuke the DB. +// Track opened DB IDs for per-test cleanup. +const openDbIds: string[] = []; + +afterEach(async () => { + for (const dbId of openDbIds) { + const db = getDatabase(dbId); + db.dexie.close(); + await db.dexie.delete(); + } + openDbIds.length = 0; +}); + +async function openTestDb(version = "0.1.0"): Promise { + const name = uniqueDbName(); + await openDatabase(name, version); + openDbIds.push(name); + return name; +} + +// ============================================================ +// Test data helpers +// ============================================================ +const ACC = "0xacc1"; +const CODE_ROOT = "0xcode1"; +const STORAGE_ROOT = "0xsroot1"; +const VAULT_ROOT = "0xvroot1"; +const COMMITMENT = "0xcommit1"; +const NONCE = "1"; + +async function seedAccount( + dbId: string, + opts: { + accountId?: string; + codeRoot?: string; + storageRoot?: string; + vaultRoot?: string; + nonce?: string; + committed?: boolean; + commitment?: string; + seed?: Uint8Array; + } = {} +) { + const id = opts.accountId ?? ACC; + await upsertAccountRecord( + dbId, + id, + opts.codeRoot ?? CODE_ROOT, + opts.storageRoot ?? STORAGE_ROOT, + opts.vaultRoot ?? VAULT_ROOT, + opts.nonce ?? NONCE, + opts.committed ?? false, + opts.commitment ?? COMMITMENT, + opts.seed + ); + return id; +} + +// ============================================================ +// getAccountIds +// ============================================================ +describe("getAccountIds", () => { + it("returns empty array when no accounts exist", async () => { + const dbId = await openTestDb(); + const ids = await getAccountIds(dbId); + expect(ids).toEqual([]); + }); + + it("returns all account ids", async () => { + const dbId = await openTestDb(); + await seedAccount(dbId, { accountId: "0xacc1" }); + await seedAccount(dbId, { accountId: "0xacc2", commitment: "0xcommit2" }); + const ids = await getAccountIds(dbId); + expect(ids).toHaveLength(2); + expect(ids).toContain("0xacc1"); + expect(ids).toContain("0xacc2"); + }); +}); + +// ============================================================ +// getAllAccountHeaders +// ============================================================ +describe("getAllAccountHeaders", () => { + it("returns empty array when no accounts", async () => { + const dbId = await openTestDb(); + const headers = await getAllAccountHeaders(dbId); + expect(headers).toEqual([]); + }); + + it("returns mapped headers including optional fields", async () => { + const dbId = await openTestDb(); + await seedAccount(dbId, { + seed: new Uint8Array([1, 2, 3]), + committed: true, + }); + const headers = await getAllAccountHeaders(dbId); + expect(headers).toHaveLength(1); + const h = headers![0]; + expect(h.id).toBe(ACC); + expect(h.codeRoot).toBe(CODE_ROOT); + expect(h.storageRoot).toBe(STORAGE_ROOT); + expect(h.vaultRoot).toBe(VAULT_ROOT); + expect(h.nonce).toBe(NONCE); + expect(h.committed).toBe(true); + // seed was provided — should be base64 encoded + expect(typeof h.accountSeed).toBe("string"); + expect(h.locked).toBe(false); + }); + + it("handles undefined accountSeed gracefully", async () => { + const dbId = await openTestDb(); + await seedAccount(dbId); // no seed + const headers = await getAllAccountHeaders(dbId); + expect(headers![0].accountSeed).toBeUndefined(); + }); +}); + +// ============================================================ +// getAccountHeader +// ============================================================ +describe("getAccountHeader", () => { + it("returns null when account does not exist", async () => { + const dbId = await openTestDb(); + const result = await getAccountHeader(dbId, "nonexistent"); + expect(result).toBeNull(); + }); + + it("returns the correct account header", async () => { + const dbId = await openTestDb(); + await seedAccount(dbId); + const result = await getAccountHeader(dbId, ACC); + expect(result).not.toBeNull(); + expect(result!.id).toBe(ACC); + expect(result!.codeRoot).toBe(CODE_ROOT); + expect(result!.storageRoot).toBe(STORAGE_ROOT); + expect(result!.vaultRoot).toBe(VAULT_ROOT); + expect(result!.nonce).toBe(NONCE); + expect(result!.locked).toBe(false); + }); +}); + +// ============================================================ +// getAccountHeaderByCommitment +// ============================================================ +describe("getAccountHeaderByCommitment", () => { + it("returns undefined when no matching commitment", async () => { + const dbId = await openTestDb(); + const result = await getAccountHeaderByCommitment(dbId, "nonexistent"); + expect(result).toBeUndefined(); + }); + + it("returns historical header by commitment", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + // Seed a historical record directly + await db.historicalAccountHeaders.put({ + id: ACC, + replacedAtNonce: "1", + codeRoot: CODE_ROOT, + storageRoot: STORAGE_ROOT, + vaultRoot: VAULT_ROOT, + nonce: "0", + committed: false, + accountSeed: undefined, + accountCommitment: "0xoldcommit", + locked: false, + }); + const result = await getAccountHeaderByCommitment(dbId, "0xoldcommit"); + expect(result).toBeDefined(); + expect(result!.id).toBe(ACC); + expect(result!.nonce).toBe("0"); + }); +}); + +// ============================================================ +// getAccountCode +// ============================================================ +describe("getAccountCode", () => { + it("returns null when no code found", async () => { + const dbId = await openTestDb(); + const result = await getAccountCode(dbId, "nonexistent"); + expect(result).toBeNull(); + }); + + it("returns base64-encoded code", async () => { + const dbId = await openTestDb(); + const code = new Uint8Array([10, 20, 30]); + await upsertAccountCode(dbId, CODE_ROOT, code); + const result = await getAccountCode(dbId, CODE_ROOT); + expect(result).not.toBeNull(); + expect(result!.root).toBe(CODE_ROOT); + // Verify it's base64-encoded + expect(typeof result!.code).toBe("string"); + const decoded = Uint8Array.from(atob(result!.code), (c) => c.charCodeAt(0)); + expect(decoded).toEqual(code); + }); +}); + +// ============================================================ +// upsertAccountCode +// ============================================================ +describe("upsertAccountCode", () => { + it("inserts code and overwrites on re-insert", async () => { + const dbId = await openTestDb(); + await upsertAccountCode(dbId, CODE_ROOT, new Uint8Array([1, 2, 3])); + await upsertAccountCode(dbId, CODE_ROOT, new Uint8Array([4, 5, 6])); + const db = getDatabase(dbId); + const record = await db.accountCodes.get(CODE_ROOT); + expect(record!.code).toEqual(new Uint8Array([4, 5, 6])); + }); +}); + +// ============================================================ +// getAccountStorage / upsertAccountStorage +// ============================================================ +describe("getAccountStorage", () => { + it("returns empty array when no storage", async () => { + const dbId = await openTestDb(); + const result = await getAccountStorage(dbId, ACC, []); + expect(result).toEqual([]); + }); + + it("returns all storage slots when no filter", async () => { + const dbId = await openTestDb(); + await upsertAccountStorage(dbId, ACC, [ + { slotName: "slot1", slotValue: "0xval1", slotType: 0 }, + { slotName: "slot2", slotValue: "0xval2", slotType: 1 }, + ]); + const result = await getAccountStorage(dbId, ACC, []); + expect(result).toHaveLength(2); + }); + + it("filters by slotNames when provided", async () => { + const dbId = await openTestDb(); + await upsertAccountStorage(dbId, ACC, [ + { slotName: "slot1", slotValue: "0xval1", slotType: 0 }, + { slotName: "slot2", slotValue: "0xval2", slotType: 1 }, + { slotName: "slot3", slotValue: "0xval3", slotType: 0 }, + ]); + const result = await getAccountStorage(dbId, ACC, ["slot1", "slot3"]); + expect(result).toHaveLength(2); + const names = result!.map((r) => r.slotName); + expect(names).toContain("slot1"); + expect(names).toContain("slot3"); + }); + + it("replaces existing slots on re-upsert", async () => { + const dbId = await openTestDb(); + await upsertAccountStorage(dbId, ACC, [ + { slotName: "slot1", slotValue: "0xold", slotType: 0 }, + ]); + await upsertAccountStorage(dbId, ACC, [ + { slotName: "slot1", slotValue: "0xnew", slotType: 0 }, + ]); + const result = await getAccountStorage(dbId, ACC, []); + expect(result![0].slotValue).toBe("0xnew"); + }); + + it("handles empty newSlots (clears storage)", async () => { + const dbId = await openTestDb(); + await upsertAccountStorage(dbId, ACC, [ + { slotName: "slot1", slotValue: "0xval", slotType: 0 }, + ]); + await upsertAccountStorage(dbId, ACC, []); + const result = await getAccountStorage(dbId, ACC, []); + expect(result).toHaveLength(0); + }); +}); + +// ============================================================ +// getAccountStorageMaps / upsertStorageMapEntries +// ============================================================ +describe("getAccountStorageMaps", () => { + it("returns empty when no map entries", async () => { + const dbId = await openTestDb(); + const result = await getAccountStorageMaps(dbId, ACC); + expect(result).toEqual([]); + }); + + it("returns all map entries for account", async () => { + const dbId = await openTestDb(); + await upsertStorageMapEntries(dbId, ACC, [ + { slotName: "map1", key: "k1", value: "v1" }, + { slotName: "map1", key: "k2", value: "v2" }, + ]); + const result = await getAccountStorageMaps(dbId, ACC); + expect(result).toHaveLength(2); + }); + + it("replaces entries on re-upsert", async () => { + const dbId = await openTestDb(); + await upsertStorageMapEntries(dbId, ACC, [ + { slotName: "map1", key: "k1", value: "v1" }, + ]); + await upsertStorageMapEntries(dbId, ACC, [ + { slotName: "map1", key: "k1", value: "v2" }, + ]); + const result = await getAccountStorageMaps(dbId, ACC); + expect(result![0].value).toBe("v2"); + }); + + it("handles empty entries (clears maps)", async () => { + const dbId = await openTestDb(); + await upsertStorageMapEntries(dbId, ACC, [ + { slotName: "map1", key: "k1", value: "v1" }, + ]); + await upsertStorageMapEntries(dbId, ACC, []); + const result = await getAccountStorageMaps(dbId, ACC); + expect(result).toHaveLength(0); + }); +}); + +// ============================================================ +// getAccountVaultAssets / upsertVaultAssets +// ============================================================ +describe("getAccountVaultAssets", () => { + it("returns empty when no assets", async () => { + const dbId = await openTestDb(); + const result = await getAccountVaultAssets(dbId, ACC, []); + expect(result).toEqual([]); + }); + + it("returns all assets when no filter", async () => { + const dbId = await openTestDb(); + await upsertVaultAssets(dbId, ACC, [ + { vaultKey: "vk1", asset: "0xasset1" }, + { vaultKey: "vk2", asset: "0xasset2" }, + ]); + const result = await getAccountVaultAssets(dbId, ACC, []); + expect(result).toHaveLength(2); + }); + + it("filters by vaultKeys when provided", async () => { + const dbId = await openTestDb(); + await upsertVaultAssets(dbId, ACC, [ + { vaultKey: "vk1", asset: "0xasset1" }, + { vaultKey: "vk2", asset: "0xasset2" }, + { vaultKey: "vk3", asset: "0xasset3" }, + ]); + const result = await getAccountVaultAssets(dbId, ACC, ["vk1", "vk3"]); + expect(result).toHaveLength(2); + const keys = result!.map((r) => r.vaultKey); + expect(keys).toContain("vk1"); + expect(keys).toContain("vk3"); + }); + + it("handles empty assets (clears vault)", async () => { + const dbId = await openTestDb(); + await upsertVaultAssets(dbId, ACC, [ + { vaultKey: "vk1", asset: "0xasset1" }, + ]); + await upsertVaultAssets(dbId, ACC, []); + const result = await getAccountVaultAssets(dbId, ACC, []); + expect(result).toHaveLength(0); + }); +}); + +// ============================================================ +// getAccountAddresses / insertAccountAddress / removeAccountAddress +// ============================================================ +describe("addresses", () => { + it("returns empty array when no addresses", async () => { + const dbId = await openTestDb(); + const result = await getAccountAddresses(dbId, ACC); + expect(result).toEqual([]); + }); + + it("inserts and retrieves an address", async () => { + const dbId = await openTestDb(); + const addr = new Uint8Array([0xaa, 0xbb, 0xcc]); + await insertAccountAddress(dbId, ACC, addr); + const result = await getAccountAddresses(dbId, ACC); + expect(result).toHaveLength(1); + }); + + it("removes an address", async () => { + const dbId = await openTestDb(); + const addr = new Uint8Array([0xaa, 0xbb]); + await insertAccountAddress(dbId, ACC, addr); + await removeAccountAddress(dbId, addr); + const result = await getAccountAddresses(dbId, ACC); + expect(result).toEqual([]); + }); +}); + +// ============================================================ +// upsertAccountRecord +// ============================================================ +describe("upsertAccountRecord", () => { + it("inserts account and can be retrieved via getAccountHeader", async () => { + const dbId = await openTestDb(); + await upsertAccountRecord( + dbId, + ACC, + CODE_ROOT, + STORAGE_ROOT, + VAULT_ROOT, + NONCE, + false, + COMMITMENT, + undefined + ); + const header = await getAccountHeader(dbId, ACC); + expect(header).not.toBeNull(); + expect(header!.id).toBe(ACC); + }); + + it("overwrites existing account on re-upsert", async () => { + const dbId = await openTestDb(); + await seedAccount(dbId); + await upsertAccountRecord( + dbId, + ACC, + CODE_ROOT, + "0xnewsroot", + VAULT_ROOT, + "2", + true, + "0xnewcommit", + undefined + ); + const header = await getAccountHeader(dbId, ACC); + expect(header!.nonce).toBe("2"); + expect(header!.storageRoot).toBe("0xnewsroot"); + }); +}); + +// ============================================================ +// applyTransactionDelta +// ============================================================ +describe("applyTransactionDelta", () => { const CLIENT_VERSION = "0.0.1"; - // The JS layer doesn't validate data formats — all validation happens in the - // Rust layer before values reach IndexedDB. So we use short readable strings - // here instead of real-length hex values. - const ACCOUNT_ID = "0xacc1"; - const CODE_ROOT = "0xcode1"; - - // Nonce "1" state - const STORAGE_ROOT_N1 = "0xsroot1"; - const VAULT_ROOT_N1 = "0xvroot1"; - const COMMITMENT_N1 = "0xcommit1"; - const SLOT_VALUE_N1 = "0xbal100"; - const MAP_VALUE_N1 = "0xmval1"; - const ASSET_N1 = "0xasset1"; - - // Nonce "2" state - const STORAGE_ROOT_N2 = "0xsroot2"; - const VAULT_ROOT_N2 = "0xvroot2"; - const COMMITMENT_N2 = "0xcommit2"; - const SLOT_VALUE_N2 = "0xbal200"; - const MAP_VALUE_N2 = "0xmval2"; - const ASSET_N2 = "0xasset2"; - - // Shared keys - const SLOT_NAME = "balance"; - const MAP_SLOT_NAME = "metadata"; - const MAP_KEY = "0xmkey1"; - const VAULT_KEY = "0xvk1"; + it("creates initial account state when no prior state exists", async () => { + const dbId = await openTestDb(CLIENT_VERSION); + const db = getDatabase(dbId); - it("undo restores previous account state", async () => { - const dbId = uniqueDbName(); - await openDatabase(dbId, CLIENT_VERSION); + await applyTransactionDelta( + dbId, + ACC, + "1", + [{ slotName: "slot1", slotValue: "0xval1", slotType: 0 }], + [{ slotName: "map1", key: "k1", value: "v1" }], + [{ vaultKey: "vk1", asset: "0xasset1" }], + CODE_ROOT, + STORAGE_ROOT, + VAULT_ROOT, + false, + COMMITMENT + ); + + const header = await db.latestAccountHeaders + .where("id") + .equals(ACC) + .first(); + expect(header?.nonce).toBe("1"); + expect(header?.storageRoot).toBe(STORAGE_ROOT); + + const slots = await db.latestAccountStorages + .where("accountId") + .equals(ACC) + .toArray(); + expect(slots).toHaveLength(1); + expect(slots[0].slotValue).toBe("0xval1"); + + const maps = await db.latestStorageMapEntries + .where("accountId") + .equals(ACC) + .toArray(); + expect(maps).toHaveLength(1); + expect(maps[0].value).toBe("v1"); + + const assets = await db.latestAccountAssets + .where("accountId") + .equals(ACC) + .toArray(); + expect(assets).toHaveLength(1); + expect(assets[0].asset).toBe("0xasset1"); + }); + + it("archives old state and updates to new state", async () => { + const dbId = await openTestDb(CLIENT_VERSION); const db = getDatabase(dbId); - // Apply nonce "1" — initial account state + // First delta: initial state await applyTransactionDelta( dbId, - ACCOUNT_ID, // accountId - "1", // nonce - [{ slotName: SLOT_NAME, slotValue: SLOT_VALUE_N1, slotType: 0 }], // updatedSlots - [{ slotName: MAP_SLOT_NAME, key: MAP_KEY, value: MAP_VALUE_N1 }], // changedMapEntries - [{ vaultKey: VAULT_KEY, asset: ASSET_N1 }], // changedAssets - CODE_ROOT, // codeRoot - STORAGE_ROOT_N1, // storageRoot - VAULT_ROOT_N1, // vaultRoot - false, // committed - COMMITMENT_N1 // commitment + ACC, + "1", + [{ slotName: "slot1", slotValue: "0xval1", slotType: 0 }], + [{ slotName: "map1", key: "k1", value: "v1" }], + [{ vaultKey: "vk1", asset: "0xasset1" }], + CODE_ROOT, + STORAGE_ROOT, + VAULT_ROOT, + false, + COMMITMENT ); - // Apply nonce "2" — updated account state with changed values + // Second delta: update await applyTransactionDelta( dbId, - ACCOUNT_ID, // accountId - "2", // nonce - [{ slotName: SLOT_NAME, slotValue: SLOT_VALUE_N2, slotType: 0 }], // updatedSlots - [{ slotName: MAP_SLOT_NAME, key: MAP_KEY, value: MAP_VALUE_N2 }], // changedMapEntries - [{ vaultKey: VAULT_KEY, asset: ASSET_N2 }], // changedAssets - CODE_ROOT, // codeRoot - STORAGE_ROOT_N2, // storageRoot - VAULT_ROOT_N2, // vaultRoot - false, // committed - COMMITMENT_N2 // commitment + ACC, + "2", + [{ slotName: "slot1", slotValue: "0xval2", slotType: 0 }], + [{ slotName: "map1", key: "k1", value: "" }], // empty string = removal + [{ vaultKey: "vk1", asset: "" }], // empty string = removal + CODE_ROOT, + "0xsroot2", + "0xvroot2", + false, + "0xcommit2" ); - // Verify latest shows nonce "2" state - const beforeUndo = await db.latestAccountHeaders + // Latest should reflect nonce 2 + const header = await db.latestAccountHeaders .where("id") - .equals(ACCOUNT_ID) + .equals(ACC) .first(); - expect(beforeUndo?.nonce).toBe("2"); - expect(beforeUndo?.storageRoot).toBe(STORAGE_ROOT_N2); + expect(header?.nonce).toBe("2"); + + // Storage updated + const slots = await db.latestAccountStorages + .where("accountId") + .equals(ACC) + .toArray(); + expect(slots[0].slotValue).toBe("0xval2"); - // Undo nonce "2" — should restore nonce "1" as the latest state - await undoAccountStates(dbId, [COMMITMENT_N2]); + // Map entry removed (empty string = deletion) + const maps = await db.latestStorageMapEntries + .where("accountId") + .equals(ACC) + .toArray(); + expect(maps).toHaveLength(0); - // Validation: Check that latest state now shows the initial account state + // Asset removed + const assets = await db.latestAccountAssets + .where("accountId") + .equals(ACC) + .toArray(); + expect(assets).toHaveLength(0); - const afterUndo = await db.latestAccountHeaders + // Historical should have the old state + const histHeaders = await db.historicalAccountHeaders .where("id") - .equals(ACCOUNT_ID) + .equals(ACC) + .toArray(); + expect(histHeaders.length).toBeGreaterThan(0); + }); +}); + +// ============================================================ +// applyFullAccountState +// ============================================================ +describe("applyFullAccountState", () => { + it("replaces full account state and archives prior", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + + // Seed initial state + await seedAccount(dbId); + await upsertAccountStorage(dbId, ACC, [ + { slotName: "slot1", slotValue: "0xold", slotType: 0 }, + ]); + await upsertStorageMapEntries(dbId, ACC, [ + { slotName: "map1", key: "k1", value: "vold" }, + ]); + await upsertVaultAssets(dbId, ACC, [ + { vaultKey: "vk1", asset: "0xoldasset" }, + ]); + + // Apply full state + await applyFullAccountState(dbId, { + accountId: ACC, + nonce: "2", + storageSlots: [{ slotName: "slot1", slotValue: "0xnew", slotType: 0 }], + storageMapEntries: [{ slotName: "map1", key: "k1", value: "vnew" }], + assets: [{ vaultKey: "vk1", asset: "0xnewasset" }], + codeRoot: CODE_ROOT, + storageRoot: "0xsroot2", + vaultRoot: "0xvroot2", + committed: true, + accountCommitment: "0xnewcommit", + accountSeed: undefined, + }); + + const header = await db.latestAccountHeaders + .where("id") + .equals(ACC) .first(); - expect(afterUndo).toBeDefined(); - expect(afterUndo?.nonce).toBe("1"); - expect(afterUndo?.storageRoot).toBe(STORAGE_ROOT_N1); - expect(afterUndo?.vaultRoot).toBe(VAULT_ROOT_N1); + expect(header?.nonce).toBe("2"); + expect(header?.committed).toBe(true); - // Storage - const latestStorage = await db.latestAccountStorages + const slots = await db.latestAccountStorages .where("accountId") - .equals(ACCOUNT_ID) + .equals(ACC) .toArray(); - expect(latestStorage).toHaveLength(1); - expect(latestStorage[0].slotValue).toBe(SLOT_VALUE_N1); + expect(slots[0].slotValue).toBe("0xnew"); - // Map entries - const latestMaps = await db.latestStorageMapEntries + const maps = await db.latestStorageMapEntries .where("accountId") - .equals(ACCOUNT_ID) + .equals(ACC) .toArray(); - expect(latestMaps).toHaveLength(1); - expect(latestMaps[0].value).toBe(MAP_VALUE_N1); + expect(maps[0].value).toBe("vnew"); - // Assets - const latestAssets = await db.latestAccountAssets + const assets = await db.latestAccountAssets .where("accountId") - .equals(ACCOUNT_ID) + .equals(ACC) .toArray(); - expect(latestAssets).toHaveLength(1); - expect(latestAssets[0].asset).toBe(ASSET_N1); + expect(assets[0].asset).toBe("0xnewasset"); + }); + + it("applies full state when no existing header (no-history branch)", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + + // No prior state for account + await applyFullAccountState(dbId, { + accountId: "0xbrand-new", + nonce: "1", + storageSlots: [], + storageMapEntries: [], + assets: [], + codeRoot: "0xcodeNew", + storageRoot: "0xsrootNew", + vaultRoot: "0xvrootNew", + committed: false, + accountCommitment: "0xcommitNew", + accountSeed: new Uint8Array([5, 6, 7]), + }); - // Historical headers should be empty: undoAccountStates consumes the - // nonce-1 historical entry when restoring it back to the latest table. - // (Pre-next behavior was to retain the historical row alongside the - // restored latest; the next-branch logic moves rather than copies.) - const historicalHeaders = await db.historicalAccountHeaders + const header = await db.latestAccountHeaders .where("id") - .equals(ACCOUNT_ID) + .equals("0xbrand-new") + .first(); + expect(header?.nonce).toBe("1"); + }); + + it("archives new slots as null-old-value when new slot has no old counterpart", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + + // Start with an existing account but NO storage + await seedAccount(dbId); + + await applyFullAccountState(dbId, { + accountId: ACC, + nonce: "2", + storageSlots: [ + { slotName: "brand-new-slot", slotValue: "0xv", slotType: 0 }, + ], + storageMapEntries: [{ slotName: "brand-new-map", key: "k", value: "v" }], + assets: [{ vaultKey: "brand-new-key", asset: "0xa" }], + codeRoot: CODE_ROOT, + storageRoot: STORAGE_ROOT, + vaultRoot: VAULT_ROOT, + committed: false, + accountCommitment: "0xcommit2", + accountSeed: undefined, + }); + + // Historical should have null old values for all brand-new entries + const histSlots = await db.historicalAccountStorages + .where("[accountId+replacedAtNonce]") + .equals([ACC, "2"]) + .toArray(); + expect(histSlots.length).toBeGreaterThan(0); + expect(histSlots[0].oldSlotValue).toBeNull(); + + const histMaps = await db.historicalStorageMapEntries + .where("[accountId+replacedAtNonce]") + .equals([ACC, "2"]) + .toArray(); + expect(histMaps.length).toBeGreaterThan(0); + expect(histMaps[0].oldValue).toBeNull(); + + const histAssets = await db.historicalAccountAssets + .where("[accountId+replacedAtNonce]") + .equals([ACC, "2"]) + .toArray(); + expect(histAssets.length).toBeGreaterThan(0); + expect(histAssets[0].oldAsset).toBeNull(); + }); +}); + +// ============================================================ +// upsertForeignAccountCode / getForeignAccountCode +// ============================================================ +describe("getForeignAccountCode", () => { + it("returns null when no records found", async () => { + const dbId = await openTestDb(); + const result = await getForeignAccountCode(dbId, ["0xacc-foreign"]); + expect(result).toBeNull(); + }); + + it("returns code for foreign accounts", async () => { + const dbId = await openTestDb(); + const code = new Uint8Array([11, 22, 33]); + await upsertForeignAccountCode(dbId, "0xforeign1", code, "0xfcoderoot"); + const result = await getForeignAccountCode(dbId, ["0xforeign1"]); + expect(result).not.toBeNull(); + expect(result).toHaveLength(1); + expect(result![0].accountId).toBe("0xforeign1"); + expect(typeof result![0].code).toBe("string"); // base64 + }); + + it("handles missing code record gracefully (undefined filtered out)", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + // Insert foreign account reference without actual code record + await db.foreignAccountCode.put({ + accountId: "0xbroken", + codeRoot: "0xmissingcode", + }); + const result = await getForeignAccountCode(dbId, ["0xbroken"]); + // Should return empty array (undefined entries filtered) + expect(result).toBeDefined(); + expect((result as unknown[]).length).toBe(0); + }); +}); + +// ============================================================ +// lockAccount +// ============================================================ +describe("lockAccount", () => { + it("locks the latest account header", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + await seedAccount(dbId); + + const before = await db.latestAccountHeaders + .where("id") + .equals(ACC) + .first(); + expect(before?.locked).toBe(false); + + await lockAccount(dbId, ACC); + + const after = await db.latestAccountHeaders.where("id").equals(ACC).first(); + expect(after?.locked).toBe(true); + }); + + it("locks historical account headers for the same account", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + + await seedAccount(dbId); + // Create a historical record + await db.historicalAccountHeaders.put({ + id: ACC, + replacedAtNonce: "1", + codeRoot: CODE_ROOT, + storageRoot: STORAGE_ROOT, + vaultRoot: VAULT_ROOT, + nonce: "0", + committed: false, + accountSeed: undefined, + accountCommitment: "0xoldcommit", + locked: false, + }); + + await lockAccount(dbId, ACC); + + const histHeaders = await db.historicalAccountHeaders + .where("id") + .equals(ACC) + .toArray(); + expect(histHeaders.every((h) => h.locked === true)).toBe(true); + }); +}); + +// ============================================================ +// pruneAccountHistory +// ============================================================ +describe("pruneAccountHistory", () => { + it("returns 0 when there is no history to prune", async () => { + const dbId = await openTestDb(); + await seedAccount(dbId); + const deleted = await pruneAccountHistory(dbId, ACC, "10"); + expect(deleted).toBe(0); + }); + + it("prunes historical records at or below the given nonce", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + + // Build up history via applyTransactionDelta (nonce 1 → 2 → 3) + await applyTransactionDelta( + dbId, + ACC, + "1", + [{ slotName: "s1", slotValue: "v1", slotType: 0 }], + [], + [], + CODE_ROOT, + STORAGE_ROOT, + VAULT_ROOT, + false, + "0xc1" + ); + await applyTransactionDelta( + dbId, + ACC, + "2", + [{ slotName: "s1", slotValue: "v2", slotType: 0 }], + [], + [], + CODE_ROOT, + "0xsr2", + VAULT_ROOT, + false, + "0xc2" + ); + await applyTransactionDelta( + dbId, + ACC, + "3", + [{ slotName: "s1", slotValue: "v3", slotType: 0 }], + [], + [], + CODE_ROOT, + "0xsr3", + VAULT_ROOT, + false, + "0xc3" + ); + + // Prune up to and including nonce 2 + const deleted = await pruneAccountHistory(dbId, ACC, "2"); + expect(deleted).toBeGreaterThan(0); + + // Historical headers at nonce <= 2 should be gone + const remaining = await db.historicalAccountHeaders + .where("id") + .equals(ACC) + .toArray(); + const remainingNonces = remaining.map((h) => Number(h.replacedAtNonce)); + expect(remainingNonces.every((n) => n > 2)).toBe(true); + }); + + it("also prunes orphaned account code", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + + const OLD_CODE = "0xoldcode"; + const NEW_CODE = "0xnewcode"; + await upsertAccountCode(dbId, OLD_CODE, new Uint8Array([1])); + await upsertAccountCode(dbId, NEW_CODE, new Uint8Array([2])); + + // Manually build a historical header with replacedAtNonce = "1" and OLD_CODE. + // This simulates a state archived when nonce "1" replaced the prior nonce. + // The latest header uses NEW_CODE so OLD_CODE has no remaining references. + await db.historicalAccountHeaders.put({ + id: ACC, + replacedAtNonce: "1", + codeRoot: OLD_CODE, + storageRoot: STORAGE_ROOT, + vaultRoot: VAULT_ROOT, + nonce: "0", + committed: false, + accountSeed: undefined, + accountCommitment: "0xc0", + locked: false, + }); + + // Latest account uses NEW_CODE + await upsertAccountRecord( + dbId, + ACC, + NEW_CODE, + STORAGE_ROOT, + VAULT_ROOT, + "2", + false, + "0xc2", + undefined + ); + + // Prune up to nonce "1" — removes the historical header (replacedAtNonce=1), + // leaving OLD_CODE unreferenced → should delete it from accountCodes. + await pruneAccountHistory(dbId, ACC, "1"); + + const oldCodeRecord = await db.accountCodes.get(OLD_CODE); + expect(oldCodeRecord).toBeUndefined(); + + // NEW_CODE should still be there (referenced by latest header) + const newCodeRecord = await db.accountCodes.get(NEW_CODE); + expect(newCodeRecord).toBeDefined(); + }); +}); + +// ============================================================ +// undoAccountStates +// ============================================================ +describe("undoAccountStates", () => { + const CV = "0.0.1"; + + it("undo restores previous account state", async () => { + const dbId = await openTestDb(CV); + const db = getDatabase(dbId); + + await applyTransactionDelta( + dbId, + ACC, + "1", + [{ slotName: "slot1", slotValue: "0xval1", slotType: 0 }], + [{ slotName: "map1", key: "k1", value: "v1" }], + [{ vaultKey: "vk1", asset: "0xasset1" }], + CODE_ROOT, + STORAGE_ROOT, + VAULT_ROOT, + false, + COMMITMENT + ); + + await applyTransactionDelta( + dbId, + ACC, + "2", + [{ slotName: "slot1", slotValue: "0xval2", slotType: 0 }], + [{ slotName: "map1", key: "k1", value: "v2" }], + [{ vaultKey: "vk1", asset: "0xasset2" }], + CODE_ROOT, + "0xsroot2", + "0xvroot2", + false, + "0xcommit2" + ); + + await undoAccountStates(dbId, ["0xcommit2"]); + + const header = await db.latestAccountHeaders + .where("id") + .equals(ACC) + .first(); + expect(header?.nonce).toBe("1"); + expect(header?.storageRoot).toBe(STORAGE_ROOT); + + const slots = await db.latestAccountStorages + .where("accountId") + .equals(ACC) + .toArray(); + expect(slots[0].slotValue).toBe("0xval1"); + + const maps = await db.latestStorageMapEntries + .where("accountId") + .equals(ACC) + .toArray(); + expect(maps[0].value).toBe("v1"); + + const assets = await db.latestAccountAssets + .where("accountId") + .equals(ACC) + .toArray(); + expect(assets[0].asset).toBe("0xasset1"); + }); + + it("deletes the account entirely when no previous header exists", async () => { + const dbId = await openTestDb(CV); + const db = getDatabase(dbId); + + // Insert account directly (no prior history) + await upsertAccountRecord( + dbId, + "0xnewaccount", + CODE_ROOT, + STORAGE_ROOT, + VAULT_ROOT, + "1", + false, + "0xcommitNew", + undefined + ); + await upsertAccountStorage(dbId, "0xnewaccount", [ + { slotName: "slot1", slotValue: "0xval1", slotType: 0 }, + ]); + + // Undo the commitment that corresponds to this account's current state + await undoAccountStates(dbId, ["0xcommitNew"]); + + // Account should be deleted from latest (commitment found in latest header) + const header = await db.latestAccountHeaders + .where("id") + .equals("0xnewaccount") + .first(); + expect(header).toBeUndefined(); + }); + + it("resolves commitment from historical headers when not in latest", async () => { + const dbId = await openTestDb(CV); + const db = getDatabase(dbId); + + await applyTransactionDelta( + dbId, + ACC, + "1", + [], + [], + [], + CODE_ROOT, + STORAGE_ROOT, + VAULT_ROOT, + false, + "0xc1" + ); + await applyTransactionDelta( + dbId, + ACC, + "2", + [], + [], + [], + CODE_ROOT, + "0xsr2", + VAULT_ROOT, + false, + "0xc2" + ); + + // "0xc1" is now in historical (archived when nonce 2 applied) + // undoAccountStates("0xc1") should find it in historical and restore + await undoAccountStates(dbId, ["0xc1"]); + + // Latest header should now be at nonce "0" (before nonce "1" was applied) + // — no prior historical means account deleted + const header = await db.latestAccountHeaders + .where("id") + .equals(ACC) + .first(); + expect(header).toBeUndefined(); + }); + + it("no-ops when commitment does not exist anywhere", async () => { + const dbId = await openTestDb(CV); + const db = getDatabase(dbId); + await seedAccount(dbId); + + // Should not throw + await expect( + undoAccountStates(dbId, ["0xnonexistent"]) + ).resolves.not.toThrow(); + + // Account should still be there + const header = await db.latestAccountHeaders + .where("id") + .equals(ACC) + .first(); + expect(header).toBeDefined(); + }); + + it("restores null old values by deleting from latest (slot null branch)", async () => { + const dbId = await openTestDb(CV); + const db = getDatabase(dbId); + + // Apply nonce "1" adding a brand-new slot/map/asset (no prior state) + await applyTransactionDelta( + dbId, + ACC, + "1", + [{ slotName: "newslot", slotValue: "0xv", slotType: 0 }], + [{ slotName: "newmap", key: "k", value: "v" }], + [{ vaultKey: "newkey", asset: "0xa" }], + CODE_ROOT, + STORAGE_ROOT, + VAULT_ROOT, + false, + COMMITMENT + ); + + // Historical entries for nonce "1" have null old values (brand-new) + const histSlots = await db.historicalAccountStorages + .where("[accountId+replacedAtNonce]") + .equals([ACC, "1"]) + .toArray(); + expect(histSlots[0].oldSlotValue).toBeNull(); + + // Undo nonce "1" — null old values should cause deletion from latest + await undoAccountStates(dbId, [COMMITMENT]); + + const slots = await db.latestAccountStorages + .where("accountId") + .equals(ACC) + .toArray(); + expect(slots).toHaveLength(0); + + const maps = await db.latestStorageMapEntries + .where("accountId") + .equals(ACC) + .toArray(); + expect(maps).toHaveLength(0); + + const assets = await db.latestAccountAssets + .where("accountId") + .equals(ACC) + .toArray(); + expect(assets).toHaveLength(0); + }); +}); + +// ============================================================ +// Error-path coverage: catch blocks call logWebStoreError (re-throws) +// Passing an unregistered dbId causes getDatabase() to throw, which +// exercises the catch body in every function. +// ============================================================ +const BAD_DB = "does-not-exist-db"; + +describe("error paths: unregistered dbId re-throws", () => { + it("getAccountIds rejects on bad dbId", async () => { + await expect(getAccountIds(BAD_DB)).rejects.toThrow(); + }); + + it("getAllAccountHeaders rejects on bad dbId", async () => { + await expect(getAllAccountHeaders(BAD_DB)).rejects.toThrow(); + }); + + it("getAccountHeader rejects on bad dbId", async () => { + await expect(getAccountHeader(BAD_DB, "0xacc")).rejects.toThrow(); + }); + + it("getAccountHeaderByCommitment rejects on bad dbId", async () => { + await expect( + getAccountHeaderByCommitment(BAD_DB, "0xcommit") + ).rejects.toThrow(); + }); + + it("getAccountCode rejects on bad dbId", async () => { + await expect(getAccountCode(BAD_DB, "0xroot")).rejects.toThrow(); + }); + + it("getAccountStorage rejects on bad dbId", async () => { + await expect(getAccountStorage(BAD_DB, "0xacc", [])).rejects.toThrow(); + }); + + it("getAccountStorageMaps rejects on bad dbId", async () => { + await expect(getAccountStorageMaps(BAD_DB, "0xacc")).rejects.toThrow(); + }); + + it("getAccountVaultAssets rejects on bad dbId", async () => { + await expect(getAccountVaultAssets(BAD_DB, "0xacc", [])).rejects.toThrow(); + }); + + it("getAccountAddresses rejects on bad dbId", async () => { + await expect(getAccountAddresses(BAD_DB, "0xacc")).rejects.toThrow(); + }); + + it("upsertAccountCode rejects on bad dbId", async () => { + await expect( + upsertAccountCode(BAD_DB, "0xroot", new Uint8Array([1])) + ).rejects.toThrow(); + }); + + it("upsertAccountStorage rejects on bad dbId", async () => { + await expect(upsertAccountStorage(BAD_DB, "0xacc", [])).rejects.toThrow(); + }); + + it("upsertStorageMapEntries rejects on bad dbId", async () => { + await expect( + upsertStorageMapEntries(BAD_DB, "0xacc", []) + ).rejects.toThrow(); + }); + + it("upsertVaultAssets rejects on bad dbId", async () => { + await expect(upsertVaultAssets(BAD_DB, "0xacc", [])).rejects.toThrow(); + }); + + it("upsertAccountRecord rejects on bad dbId", async () => { + await expect( + upsertAccountRecord( + BAD_DB, + "0xacc", + "0xcode", + "0xsroot", + "0xvroot", + "1", + false, + "0xcommit", + undefined + ) + ).rejects.toThrow(); + }); + + it("insertAccountAddress rejects on bad dbId", async () => { + await expect( + insertAccountAddress(BAD_DB, "0xacc", new Uint8Array([1])) + ).rejects.toThrow(); + }); + + it("removeAccountAddress rejects on bad dbId", async () => { + await expect( + removeAccountAddress(BAD_DB, new Uint8Array([1])) + ).rejects.toThrow(); + }); + + it("upsertForeignAccountCode rejects on bad dbId", async () => { + await expect( + upsertForeignAccountCode(BAD_DB, "0xacc", new Uint8Array([1]), "0xroot") + ).rejects.toThrow(); + }); + + it("getForeignAccountCode rejects on bad dbId", async () => { + await expect(getForeignAccountCode(BAD_DB, ["0xacc"])).rejects.toThrow(); + }); + + it("lockAccount rejects on bad dbId", async () => { + await expect(lockAccount(BAD_DB, "0xacc")).rejects.toThrow(); + }); + + it("applyTransactionDelta rejects on bad dbId", async () => { + await expect( + applyTransactionDelta( + BAD_DB, + "0xacc", + "1", + [], + [], + [], + "0xcode", + "0xsr", + "0xvr", + false, + "0xcommit" + ) + ).rejects.toThrow(); + }); + + it("applyFullAccountState rejects on bad dbId", async () => { + await expect( + applyFullAccountState(BAD_DB, { + accountId: "0xacc", + nonce: "1", + storageSlots: [], + storageMapEntries: [], + assets: [], + codeRoot: "0xcode", + storageRoot: "0xsr", + vaultRoot: "0xvr", + committed: false, + accountCommitment: "0xcommit", + accountSeed: undefined, + }) + ).rejects.toThrow(); + }); + + it("undoAccountStates rejects on bad dbId", async () => { + await expect(undoAccountStates(BAD_DB, ["0xcommit"])).rejects.toThrow(); + }); + + it("pruneAccountHistory rejects on bad dbId", async () => { + await expect(pruneAccountHistory(BAD_DB, "0xacc", "10")).rejects.toThrow(); + }); +}); + +// ============================================================ +// Additional coverage: line 1119 — sort comparator (multiple nonces same account) +// ============================================================ +describe("undoAccountStates: multiple nonces for same account (sort comparator)", () => { + it("undoes multiple nonces for the same account in descending order", async () => { + const dbId = await openTestDb("0.0.1"); + const db = getDatabase(dbId); + + // Build 3 deltas for the same account to exercise the sort comparator at 1119 + await applyTransactionDelta( + dbId, + ACC, + "1", + [{ slotName: "slot1", slotValue: "0xv1", slotType: 0 }], + [], + [], + CODE_ROOT, + STORAGE_ROOT, + VAULT_ROOT, + false, + "0xc1" + ); + await applyTransactionDelta( + dbId, + ACC, + "2", + [{ slotName: "slot1", slotValue: "0xv2", slotType: 0 }], + [], + [], + CODE_ROOT, + "0xsr2", + VAULT_ROOT, + false, + "0xc2" + ); + await applyTransactionDelta( + dbId, + ACC, + "3", + [{ slotName: "slot1", slotValue: "0xv3", slotType: 0 }], + [], + [], + CODE_ROOT, + "0xsr3", + VAULT_ROOT, + false, + "0xc3" + ); + + // Undo both nonce 2 and 3 at once — they have the same accountId, + // so accountNonces will have one entry with {2, 3}, triggering the sort. + await undoAccountStates(dbId, ["0xc2", "0xc3"]); + + // After undoing nonces 2 and 3, the slot value should be back to nonce "1" state + const slots = await db.latestAccountStorages + .where("accountId") + .equals(ACC) .toArray(); - expect(historicalHeaders).toHaveLength(0); + expect(slots[0].slotValue).toBe("0xv1"); }); }); diff --git a/crates/idxdb-store/src/ts/auth.test.ts b/crates/idxdb-store/src/ts/auth.test.ts new file mode 100644 index 0000000..abbf06c --- /dev/null +++ b/crates/idxdb-store/src/ts/auth.test.ts @@ -0,0 +1,228 @@ +import { describe, it, expect, afterEach, vi, beforeEach } from "vitest"; +import { openDatabase, getDatabase } from "./schema.js"; +import { + insertAccountAuth, + getAccountAuthByPubKeyCommitment, + removeAccountAuth, + insertAccountKeyMapping, + getKeyCommitmentsByAccountId, + removeAllMappingsForKey, + getAccountIdByKeyCommitment, +} from "./auth.js"; + +let dbCounter = 0; +function uniqueDbName(): string { + return `test-auth-${++dbCounter}-${Date.now()}`; +} + +const openDbIds: string[] = []; + +afterEach(async () => { + for (const dbId of openDbIds) { + const db = getDatabase(dbId); + db.dexie.close(); + await db.dexie.delete(); + } + openDbIds.length = 0; +}); + +async function openTestDb(): Promise { + const name = uniqueDbName(); + await openDatabase(name, "0.1.0"); + openDbIds.push(name); + return name; +} + +describe("auth", () => { + let errorSpy: any; + let logSpy: any; + + beforeEach(() => { + errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + }); + + afterEach(() => { + errorSpy.mockRestore(); + logSpy.mockRestore(); + }); + + // --------------------------------------------------------------------------- + // insertAccountAuth / getAccountAuthByPubKeyCommitment + // --------------------------------------------------------------------------- + + it("inserts an account auth and retrieves it by pubkey commitment", async () => { + const dbId = await openTestDb(); + await insertAccountAuth(dbId, "pubkey-abc", "secretkey-xyz"); + const result = await getAccountAuthByPubKeyCommitment(dbId, "pubkey-abc"); + expect(result).toEqual({ secretKey: "secretkey-xyz" }); + }); + + it("stores multiple account auths independently", async () => { + const dbId = await openTestDb(); + await insertAccountAuth(dbId, "pubkey-1", "secret-1"); + await insertAccountAuth(dbId, "pubkey-2", "secret-2"); + const r1 = await getAccountAuthByPubKeyCommitment(dbId, "pubkey-1"); + const r2 = await getAccountAuthByPubKeyCommitment(dbId, "pubkey-2"); + expect(r1).toEqual({ secretKey: "secret-1" }); + expect(r2).toEqual({ secretKey: "secret-2" }); + }); + + it("getAccountAuthByPubKeyCommitment throws when record does not exist", async () => { + const dbId = await openTestDb(); + await expect( + getAccountAuthByPubKeyCommitment(dbId, "nonexistent-key") + ).rejects.toThrow("Account auth not found in cache."); + }); + + it("insertAccountAuth throws (via logWebStoreError rethrow) when db is not opened", async () => { + await expect( + insertAccountAuth("never-opened", "pubkey-abc", "secretkey-xyz") + ).rejects.toThrow(); + }); + + it("getAccountAuthByPubKeyCommitment throws when db is not opened", async () => { + // No try/catch in getAccountAuthByPubKeyCommitment — getDatabase throws propagate + await expect( + getAccountAuthByPubKeyCommitment("never-opened", "pubkey-abc") + ).rejects.toThrow(); + }); + + // --------------------------------------------------------------------------- + // removeAccountAuth + // --------------------------------------------------------------------------- + + it("removes an account auth", async () => { + const dbId = await openTestDb(); + await insertAccountAuth(dbId, "pubkey-del", "secret-del"); + await removeAccountAuth(dbId, "pubkey-del"); + await expect( + getAccountAuthByPubKeyCommitment(dbId, "pubkey-del") + ).rejects.toThrow("Account auth not found in cache."); + }); + + it("removeAccountAuth on a missing key is a no-op", async () => { + const dbId = await openTestDb(); + // Should not throw + await removeAccountAuth(dbId, "nonexistent-key"); + }); + + it("removeAccountAuth throws (via logWebStoreError rethrow) when db is not opened", async () => { + await expect( + removeAccountAuth("never-opened", "pubkey-abc") + ).rejects.toThrow(); + }); + + // --------------------------------------------------------------------------- + // insertAccountKeyMapping / getKeyCommitmentsByAccountId + // --------------------------------------------------------------------------- + + it("inserts a key mapping and retrieves commitments by account id", async () => { + const dbId = await openTestDb(); + await insertAccountKeyMapping(dbId, "account-1", "pubkey-commitment-1"); + const commitments = await getKeyCommitmentsByAccountId(dbId, "account-1"); + expect(commitments).toEqual(["pubkey-commitment-1"]); + }); + + it("inserts multiple mappings for the same account and retrieves all commitments", async () => { + const dbId = await openTestDb(); + await insertAccountKeyMapping(dbId, "account-multi", "commitment-a"); + await insertAccountKeyMapping(dbId, "account-multi", "commitment-b"); + const commitments = await getKeyCommitmentsByAccountId( + dbId, + "account-multi" + ); + expect(commitments).toHaveLength(2); + expect(commitments).toEqual( + expect.arrayContaining(["commitment-a", "commitment-b"]) + ); + }); + + it("insertAccountKeyMapping is idempotent (put semantics) for the same pair", async () => { + const dbId = await openTestDb(); + await insertAccountKeyMapping(dbId, "account-idem", "commitment-idem"); + await insertAccountKeyMapping(dbId, "account-idem", "commitment-idem"); + const commitments = await getKeyCommitmentsByAccountId( + dbId, + "account-idem" + ); + // put semantics: the second call replaces the first — still one entry + expect(commitments).toHaveLength(1); + }); + + it("getKeyCommitmentsByAccountId returns empty array when no mappings exist", async () => { + const dbId = await openTestDb(); + const commitments = await getKeyCommitmentsByAccountId(dbId, "no-account"); + expect(commitments).toEqual([]); + }); + + it("insertAccountKeyMapping throws (via logWebStoreError rethrow) when db is not opened", async () => { + await expect( + insertAccountKeyMapping("never-opened", "account-1", "commitment-1") + ).rejects.toThrow(); + }); + + it("getKeyCommitmentsByAccountId throws (via logWebStoreError rethrow) when db is not opened", async () => { + await expect( + getKeyCommitmentsByAccountId("never-opened", "account-1") + ).rejects.toThrow(); + }); + + // --------------------------------------------------------------------------- + // removeAllMappingsForKey + // --------------------------------------------------------------------------- + + it("removes all account key mappings for a given key commitment", async () => { + const dbId = await openTestDb(); + await insertAccountKeyMapping(dbId, "account-a", "shared-commitment"); + await insertAccountKeyMapping(dbId, "account-b", "shared-commitment"); + await removeAllMappingsForKey(dbId, "shared-commitment"); + // Both accounts should now have no mappings for shared-commitment + const idResult = await getAccountIdByKeyCommitment( + dbId, + "shared-commitment" + ); + expect(idResult).toBeNull(); + }); + + it("removeAllMappingsForKey on a missing key is a no-op", async () => { + const dbId = await openTestDb(); + await removeAllMappingsForKey(dbId, "nonexistent-commitment"); + // No throw means success + }); + + it("removeAllMappingsForKey throws (via logWebStoreError rethrow) when db is not opened", async () => { + await expect( + removeAllMappingsForKey("never-opened", "commitment-x") + ).rejects.toThrow(); + }); + + // --------------------------------------------------------------------------- + // getAccountIdByKeyCommitment + // --------------------------------------------------------------------------- + + it("retrieves account id by key commitment", async () => { + const dbId = await openTestDb(); + await insertAccountKeyMapping(dbId, "account-lookup", "commitment-lookup"); + const accountId = await getAccountIdByKeyCommitment( + dbId, + "commitment-lookup" + ); + expect(accountId).toBe("account-lookup"); + }); + + it("getAccountIdByKeyCommitment returns null when commitment is not found", async () => { + const dbId = await openTestDb(); + const accountId = await getAccountIdByKeyCommitment( + dbId, + "nonexistent-commitment" + ); + expect(accountId).toBeNull(); + }); + + it("getAccountIdByKeyCommitment throws (via logWebStoreError rethrow) when db is not opened", async () => { + await expect( + getAccountIdByKeyCommitment("never-opened", "commitment-x") + ).rejects.toThrow(); + }); +}); diff --git a/crates/idxdb-store/src/ts/chainData.test.ts b/crates/idxdb-store/src/ts/chainData.test.ts index 32a428f..858d245 100644 --- a/crates/idxdb-store/src/ts/chainData.test.ts +++ b/crates/idxdb-store/src/ts/chainData.test.ts @@ -3,6 +3,14 @@ import { afterEach, describe, expect, it } from "vitest"; import { getPartialBlockchainPeaksByBlockNum, insertBlockHeader, + insertPartialBlockchainNodes, + getBlockHeaders, + getTrackedBlockHeaders, + getTrackedBlockHeaderNumbers, + getPartialBlockchainNodesAll, + getPartialBlockchainNodes, + getPartialBlockchainNodesUpToInOrderIndex, + pruneIrrelevantBlocks, } from "./chainData.js"; import { getDatabase, openDatabase } from "./schema.js"; import { uniqueDbName } from "./test-utils.js"; @@ -131,3 +139,366 @@ describe("insertBlockHeader: add-if-not-exists semantics", () => { expect(stored!.hasClientNotes).toBe("true"); }); }); + +// ============================================================ +// insertPartialBlockchainNodes +// ============================================================ +describe("insertPartialBlockchainNodes", () => { + it("inserts nodes and retrieves them", async () => { + const dbId = await openTestDb(); + await insertPartialBlockchainNodes( + dbId, + ["1", "2", "3"], + ["0xnode1", "0xnode2", "0xnode3"] + ); + const db = getDatabase(dbId); + const all = await db.partialBlockchainNodes.toArray(); + expect(all).toHaveLength(3); + }); + + it("no-ops when ids array is empty", async () => { + const dbId = await openTestDb(); + await insertPartialBlockchainNodes(dbId, [], []); + const db = getDatabase(dbId); + const all = await db.partialBlockchainNodes.toArray(); + expect(all).toHaveLength(0); + }); + + it("rejects when ids and nodes arrays have different lengths", async () => { + const dbId = await openTestDb(); + // The error is thrown, caught by the catch block, then re-thrown by + // logWebStoreError — so the outer promise rejects. + await expect( + insertPartialBlockchainNodes(dbId, ["1", "2"], ["0xnode1"]) + ).rejects.toThrow("ids and nodes arrays must be of the same length"); + }); + + it("overwrites existing nodes on re-insert (bulkPut semantics)", async () => { + const dbId = await openTestDb(); + await insertPartialBlockchainNodes(dbId, ["1"], ["0xold"]); + await insertPartialBlockchainNodes(dbId, ["1"], ["0xnew"]); + const db = getDatabase(dbId); + const node = await db.partialBlockchainNodes.get(1); + expect(node!.node).toBe("0xnew"); + }); +}); + +// ============================================================ +// getBlockHeaders +// ============================================================ +describe("getBlockHeaders", () => { + it("returns null entries for missing block numbers", async () => { + const dbId = await openTestDb(); + const results = await getBlockHeaders(dbId, [999]); + expect(results).toHaveLength(1); + expect(results![0]).toBeNull(); + }); + + it("returns base64-encoded headers for existing blocks", async () => { + const dbId = await openTestDb(); + await insertBlockHeader(dbId, 1, HEADER_V1, PEAKS_FROM_SYNC, false); + await insertBlockHeader(dbId, 2, HEADER_V2, PEAKS_FROM_BACKFILL, true); + + const results = await getBlockHeaders(dbId, [1, 2]); + expect(results).toHaveLength(2); + expect(results![0]).not.toBeNull(); + expect(results![1]).not.toBeNull(); + expect(results![0]!.blockNum).toBe(1); + expect(results![1]!.blockNum).toBe(2); + // Both should be base64 strings + expect(typeof results![0]!.header).toBe("string"); + expect(results![0]!.hasClientNotes).toBe(false); + expect(results![1]!.hasClientNotes).toBe(true); + }); + + it("returns empty array for empty block list", async () => { + const dbId = await openTestDb(); + const results = await getBlockHeaders(dbId, []); + expect(results).toEqual([]); + }); +}); + +// ============================================================ +// getTrackedBlockHeaders +// ============================================================ +describe("getTrackedBlockHeaders", () => { + it("returns only blocks with hasClientNotes=true", async () => { + const dbId = await openTestDb(); + await insertBlockHeader(dbId, 10, HEADER_V1, PEAKS_FROM_SYNC, false); + await insertBlockHeader(dbId, 20, HEADER_V2, PEAKS_FROM_BACKFILL, true); + + const results = await getTrackedBlockHeaders(dbId); + expect(results).toHaveLength(1); + expect(results![0].blockNum).toBe(20); + expect(results![0].hasClientNotes).toBe(true); + expect(typeof results![0].header).toBe("string"); + expect(typeof results![0].partialBlockchainPeaks).toBe("string"); + }); + + it("returns empty array when no tracked blocks", async () => { + const dbId = await openTestDb(); + await insertBlockHeader(dbId, 10, HEADER_V1, PEAKS_FROM_SYNC, false); + const results = await getTrackedBlockHeaders(dbId); + expect(results).toEqual([]); + }); +}); + +// ============================================================ +// getTrackedBlockHeaderNumbers +// ============================================================ +describe("getTrackedBlockHeaderNumbers", () => { + it("returns primary keys of tracked blocks only", async () => { + const dbId = await openTestDb(); + await insertBlockHeader(dbId, 5, HEADER_V1, PEAKS_FROM_SYNC, true); + await insertBlockHeader(dbId, 6, HEADER_V2, PEAKS_FROM_BACKFILL, false); + await insertBlockHeader(dbId, 7, HEADER_V1, PEAKS_FROM_SYNC, true); + + const nums = await getTrackedBlockHeaderNumbers(dbId); + expect(nums).toHaveLength(2); + expect(nums).toContain(5); + expect(nums).toContain(7); + }); + + it("returns empty when no tracked blocks", async () => { + const dbId = await openTestDb(); + const nums = await getTrackedBlockHeaderNumbers(dbId); + expect(nums).toEqual([]); + }); +}); + +// ============================================================ +// getPartialBlockchainPeaksByBlockNum +// ============================================================ +describe("getPartialBlockchainPeaksByBlockNum", () => { + it("returns {peaks: undefined} for non-existent block", async () => { + const dbId = await openTestDb(); + const result = await getPartialBlockchainPeaksByBlockNum(dbId, 999); + expect(result).toBeDefined(); + expect(result!.peaks).toBeUndefined(); + }); + + it("returns base64-encoded peaks for existing block", async () => { + const dbId = await openTestDb(); + await insertBlockHeader(dbId, 50, HEADER_V1, PEAKS_FROM_SYNC, false); + const result = await getPartialBlockchainPeaksByBlockNum(dbId, 50); + expect(result!.peaks).toBeDefined(); + const decoded = Uint8Array.from(atob(result!.peaks!), (c) => + c.charCodeAt(0) + ); + expect(decoded).toEqual(PEAKS_FROM_SYNC); + }); +}); + +// ============================================================ +// getPartialBlockchainNodesAll +// ============================================================ +describe("getPartialBlockchainNodesAll", () => { + it("returns empty array when no nodes", async () => { + const dbId = await openTestDb(); + const result = await getPartialBlockchainNodesAll(dbId); + expect(result).toEqual([]); + }); + + it("returns all inserted nodes", async () => { + const dbId = await openTestDb(); + await insertPartialBlockchainNodes(dbId, ["10", "20"], ["0xa", "0xb"]); + const result = await getPartialBlockchainNodesAll(dbId); + expect(result).toHaveLength(2); + }); +}); + +// ============================================================ +// getPartialBlockchainNodes +// ============================================================ +describe("getPartialBlockchainNodes", () => { + it("returns nodes for the given ids, filtering undefined for missing", async () => { + const dbId = await openTestDb(); + await insertPartialBlockchainNodes( + dbId, + ["1", "3"], + ["0xnode1", "0xnode3"] + ); + const result = await getPartialBlockchainNodes(dbId, ["1", "2", "3"]); + // id 2 is missing → filtered out + expect(result).toHaveLength(2); + const ids = result!.map((n) => n!.id); + expect(ids).toContain(1); + expect(ids).toContain(3); + }); + + it("returns empty array when none of the requested ids exist", async () => { + const dbId = await openTestDb(); + const result = await getPartialBlockchainNodes(dbId, ["99", "100"]); + expect(result).toEqual([]); + }); +}); + +// ============================================================ +// getPartialBlockchainNodesUpToInOrderIndex +// ============================================================ +describe("getPartialBlockchainNodesUpToInOrderIndex", () => { + it("returns nodes with id <= maxIndex", async () => { + const dbId = await openTestDb(); + await insertPartialBlockchainNodes( + dbId, + ["1", "2", "3", "4", "5"], + ["0xa", "0xb", "0xc", "0xd", "0xe"] + ); + const result = await getPartialBlockchainNodesUpToInOrderIndex(dbId, "3"); + expect(result).toHaveLength(3); + const ids = result!.map((n) => n.id); + expect(ids).toContain(1); + expect(ids).toContain(2); + expect(ids).toContain(3); + expect(ids).not.toContain(4); + }); + + it("returns empty when no nodes exist below threshold", async () => { + const dbId = await openTestDb(); + await insertPartialBlockchainNodes(dbId, ["10", "20"], ["0xa", "0xb"]); + const result = await getPartialBlockchainNodesUpToInOrderIndex(dbId, "5"); + expect(result).toEqual([]); + }); +}); + +// ============================================================ +// pruneIrrelevantBlocks +// ============================================================ +describe("pruneIrrelevantBlocks", () => { + it("deletes non-tracked non-sync-height non-genesis blocks", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + + // Insert sync height = 10 (default populate gives block 0) + await db.stateSync.put({ id: 1, blockNum: 10 }); + + // Block 0 (genesis), block 5 (irrelevant), block 10 (sync height), block 20 (tracked) + await insertBlockHeader(dbId, 0, HEADER_V1, PEAKS_FROM_SYNC, false); + await insertBlockHeader(dbId, 5, HEADER_V1, PEAKS_FROM_SYNC, false); // should be pruned + await insertBlockHeader(dbId, 10, HEADER_V1, PEAKS_FROM_SYNC, false); // sync height, keep + await insertBlockHeader(dbId, 20, HEADER_V2, PEAKS_FROM_BACKFILL, true); // tracked, keep + + await pruneIrrelevantBlocks(dbId, [], []); + + const remaining = await db.blockHeaders.toArray(); + const blockNums = remaining.map((r) => r.blockNum); + expect(blockNums).not.toContain(5); + expect(blockNums).toContain(0); + expect(blockNums).toContain(10); + expect(blockNums).toContain(20); + }); + + it("untracks listed blocks then prunes them", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + + await db.stateSync.put({ id: 1, blockNum: 10 }); + await insertBlockHeader(dbId, 0, HEADER_V1, PEAKS_FROM_SYNC, false); + await insertBlockHeader(dbId, 7, HEADER_V1, PEAKS_FROM_SYNC, true); // tracked, will untrack + await insertBlockHeader(dbId, 10, HEADER_V1, PEAKS_FROM_SYNC, false); + await insertBlockHeader(dbId, 20, HEADER_V2, PEAKS_FROM_BACKFILL, true); + + await pruneIrrelevantBlocks(dbId, [7], []); + + const remaining = await db.blockHeaders.toArray(); + const blockNums = remaining.map((r) => r.blockNum); + expect(blockNums).not.toContain(7); // untracked then pruned + expect(blockNums).toContain(20); // still tracked + }); + + it("removes listed MMR authentication nodes", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + + await db.stateSync.put({ id: 1, blockNum: 10 }); + await insertPartialBlockchainNodes( + dbId, + ["1", "2", "3"], + ["0xa", "0xb", "0xc"] + ); + + await pruneIrrelevantBlocks(dbId, [], ["1", "3"]); + + const nodes = await db.partialBlockchainNodes.toArray(); + const ids = nodes.map((n) => Number(n.id)); + expect(ids).not.toContain(1); + expect(ids).toContain(2); + expect(ids).not.toContain(3); + }); + + it("rejects when stateSync is undefined", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + + // Delete the default stateSync entry that was populated by the 'populate' hook + await db.stateSync.clear(); + + // logWebStoreError re-throws, so the promise rejects + await expect(pruneIrrelevantBlocks(dbId, [], [])).rejects.toThrow( + "SyncHeight is undefined" + ); + }); +}); + +// ============================================================ +// Error-path coverage: catch blocks call logWebStoreError (re-throws) +// Passing an unregistered dbId exercises the catch body in each function. +// ============================================================ +const BAD_DB = "does-not-exist-chaindata"; + +describe("error paths: unregistered dbId re-throws", () => { + it("insertBlockHeader rejects on bad dbId", async () => { + await expect( + insertBlockHeader( + BAD_DB, + 1, + new Uint8Array([1]), + new Uint8Array([2]), + false + ) + ).rejects.toThrow(); + }); + + it("insertPartialBlockchainNodes rejects on bad dbId (empty ids are a no-op before db access)", async () => { + // Non-empty ids will hit getDatabase, which throws + await expect( + insertPartialBlockchainNodes(BAD_DB, ["1"], ["0xnode"]) + ).rejects.toThrow(); + }); + + it("getBlockHeaders rejects on bad dbId", async () => { + await expect(getBlockHeaders(BAD_DB, [1])).rejects.toThrow(); + }); + + it("getTrackedBlockHeaders rejects on bad dbId", async () => { + await expect(getTrackedBlockHeaders(BAD_DB)).rejects.toThrow(); + }); + + it("getTrackedBlockHeaderNumbers rejects on bad dbId", async () => { + await expect(getTrackedBlockHeaderNumbers(BAD_DB)).rejects.toThrow(); + }); + + it("getPartialBlockchainPeaksByBlockNum rejects on bad dbId", async () => { + await expect( + getPartialBlockchainPeaksByBlockNum(BAD_DB, 1) + ).rejects.toThrow(); + }); + + it("getPartialBlockchainNodesAll rejects on bad dbId", async () => { + await expect(getPartialBlockchainNodesAll(BAD_DB)).rejects.toThrow(); + }); + + it("getPartialBlockchainNodes rejects on bad dbId", async () => { + await expect(getPartialBlockchainNodes(BAD_DB, ["1"])).rejects.toThrow(); + }); + + it("getPartialBlockchainNodesUpToInOrderIndex rejects on bad dbId", async () => { + await expect( + getPartialBlockchainNodesUpToInOrderIndex(BAD_DB, "5") + ).rejects.toThrow(); + }); + + it("pruneIrrelevantBlocks rejects on bad dbId", async () => { + await expect(pruneIrrelevantBlocks(BAD_DB, [], [])).rejects.toThrow(); + }); +}); diff --git a/crates/idxdb-store/src/ts/export.test.ts b/crates/idxdb-store/src/ts/export.test.ts new file mode 100644 index 0000000..7ecf3ab --- /dev/null +++ b/crates/idxdb-store/src/ts/export.test.ts @@ -0,0 +1,231 @@ +import { describe, it, expect, afterEach, vi, beforeEach } from "vitest"; +import { openDatabase, getDatabase } from "./schema.js"; +import { exportStore, transformForExport } from "./export.js"; +import { uint8ArrayToBase64 } from "./utils.js"; + +let dbCounter = 0; +function uniqueDbName(): string { + return `test-export-${++dbCounter}-${Date.now()}`; +} + +const openDbIds: string[] = []; + +afterEach(async () => { + for (const dbId of openDbIds) { + const db = getDatabase(dbId); + db.dexie.close(); + await db.dexie.delete(); + } + openDbIds.length = 0; +}); + +async function openTestDb(): Promise { + const name = uniqueDbName(); + await openDatabase(name, "0.1.0"); + openDbIds.push(name); + return name; +} + +// ================================================================================================ +// transformForExport unit tests +// ================================================================================================ + +describe("transformForExport", () => { + let logSpy: any; + + beforeEach(() => { + logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + }); + + afterEach(() => { + logSpy.mockRestore(); + }); + + it("converts a Uint8Array to a tagged base64 object", async () => { + const bytes = new Uint8Array([1, 2, 3]); + const result = await transformForExport(bytes); + expect(result).toEqual({ + __type: "Uint8Array", + data: uint8ArrayToBase64(bytes), + }); + }); + + it("converts a Blob to a tagged base64 object", async () => { + const bytes = new Uint8Array([4, 5, 6]); + const blob = new Blob([bytes]); + const result = await transformForExport(blob); + expect(result).toEqual({ + __type: "Blob", + data: uint8ArrayToBase64(bytes), + }); + }); + + it("transforms an array recursively", async () => { + const input = [new Uint8Array([1]), "hello", 42]; + const result = await transformForExport(input); + expect(result).toEqual([ + { __type: "Uint8Array", data: uint8ArrayToBase64(new Uint8Array([1])) }, + "hello", + 42, + ]); + }); + + it("transforms a nested record recursively", async () => { + const bytes = new Uint8Array([7, 8]); + const input = { key: bytes, count: 5, label: "abc" }; + const result = await transformForExport(input); + expect(result).toEqual({ + key: { __type: "Uint8Array", data: uint8ArrayToBase64(bytes) }, + count: 5, + label: "abc", + }); + }); + + it("returns primitives unchanged", async () => { + expect(await transformForExport(42)).toBe(42); + expect(await transformForExport("hello")).toBe("hello"); + expect(await transformForExport(null)).toBeNull(); + expect(await transformForExport(true)).toBe(true); + }); + + it("handles deeply nested structures", async () => { + const bytes = new Uint8Array([99]); + const input = { outer: { inner: [bytes] } }; + const result = await transformForExport(input); + expect(result).toEqual({ + outer: { + inner: [{ __type: "Uint8Array", data: uint8ArrayToBase64(bytes) }], + }, + }); + }); +}); + +// ================================================================================================ +// exportStore tests +// ================================================================================================ + +describe("exportStore", () => { + let logSpy: any; + + beforeEach(() => { + logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + }); + + afterEach(() => { + logSpy.mockRestore(); + }); + + it("exports an empty DB as a JSON object with all table keys present", async () => { + const dbId = await openTestDb(); + const jsonStr = await exportStore(dbId); + const parsed = JSON.parse(jsonStr); + + // All tables in the schema should be present as keys + const db = getDatabase(dbId); + const tableNames = db.dexie.tables.map((t) => t.name); + for (const name of tableNames) { + expect(parsed).toHaveProperty(name); + expect(Array.isArray(parsed[name])).toBe(true); + } + }); + + it("empty DB tables are empty arrays", async () => { + const dbId = await openTestDb(); + const jsonStr = await exportStore(dbId); + const parsed = JSON.parse(jsonStr); + + // stateSync gets one row on populate and settings gets the clientVersion row. + // Everything else should be empty. + const db = getDatabase(dbId); + const tableNames = db.dexie.tables.map((t) => t.name); + const nonEmptyTables = tableNames.filter((name) => parsed[name].length > 0); + expect(nonEmptyTables).toEqual( + expect.arrayContaining(["stateSync", "settings"]) + ); + // tables other than these two must be empty + const otherNonEmpty = nonEmptyTables.filter( + (n) => n !== "stateSync" && n !== "settings" + ); + expect(otherNonEmpty).toHaveLength(0); + }); + + it("exports inputNotes rows and serializes Uint8Array fields as tagged base64", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + + const assetBytes = new Uint8Array([10, 20, 30]); + const serialBytes = new Uint8Array([1, 2, 3, 4]); + const inputsBytes = new Uint8Array([5, 6]); + const stateBytes = new Uint8Array([7, 8, 9]); + + await db.inputNotes.put({ + noteId: "note-abc", + stateDiscriminant: 0, + assets: assetBytes, + serialNumber: serialBytes, + inputs: inputsBytes, + scriptRoot: "script-root-x", + nullifier: "nullifier-abc", + serializedCreatedAt: "2024-01-01", + state: stateBytes, + }); + + const jsonStr = await exportStore(dbId); + const parsed = JSON.parse(jsonStr); + + expect(parsed.inputNotes).toHaveLength(1); + const note = parsed.inputNotes[0]; + + // Uint8Array fields should be serialized as tagged base64 + expect(note.assets).toEqual({ + __type: "Uint8Array", + data: uint8ArrayToBase64(assetBytes), + }); + expect(note.serialNumber).toEqual({ + __type: "Uint8Array", + data: uint8ArrayToBase64(serialBytes), + }); + expect(note.state).toEqual({ + __type: "Uint8Array", + data: uint8ArrayToBase64(stateBytes), + }); + + // Primitive fields stay as-is + expect(note.noteId).toBe("note-abc"); + expect(note.scriptRoot).toBe("script-root-x"); + expect(note.nullifier).toBe("nullifier-abc"); + }); + + it("exports multiple tables with data", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + + await db.accountCodes.put({ + root: "root-1", + code: new Uint8Array([1, 2]), + }); + + await db.settings.put({ + key: "test-key", + value: new Uint8Array([3, 4]), + }); + + const jsonStr = await exportStore(dbId); + const parsed = JSON.parse(jsonStr); + + expect(parsed.accountCode).toHaveLength(1); + expect(parsed.accountCode[0].root).toBe("root-1"); + expect(parsed.accountCode[0].code).toEqual({ + __type: "Uint8Array", + data: uint8ArrayToBase64(new Uint8Array([1, 2])), + }); + + // settings has the initial clientVersion row + our test-key + const settingsKeys = parsed.settings.map((s: any) => s.key); + expect(settingsKeys).toContain("test-key"); + }); + + it("throws for a db that was never opened", async () => { + await expect(exportStore("never-opened")).rejects.toThrow(); + }); +}); diff --git a/crates/idxdb-store/src/ts/import.test.ts b/crates/idxdb-store/src/ts/import.test.ts new file mode 100644 index 0000000..80bc27c --- /dev/null +++ b/crates/idxdb-store/src/ts/import.test.ts @@ -0,0 +1,294 @@ +import { describe, it, expect, afterEach, vi, beforeEach } from "vitest"; +import { openDatabase, getDatabase } from "./schema.js"; +import { exportStore } from "./export.js"; +import { forceImportStore, transformForImport } from "./import.js"; +import { uint8ArrayToBase64 } from "./utils.js"; + +let dbCounter = 0; +function uniqueDbName(): string { + return `test-import-${++dbCounter}-${Date.now()}`; +} + +const openDbIds: string[] = []; + +afterEach(async () => { + for (const dbId of openDbIds) { + const db = getDatabase(dbId); + db.dexie.close(); + await db.dexie.delete(); + } + openDbIds.length = 0; +}); + +async function openTestDb(): Promise { + const name = uniqueDbName(); + await openDatabase(name, "0.1.0"); + openDbIds.push(name); + return name; +} + +// ================================================================================================ +// transformForImport unit tests +// ================================================================================================ + +describe("transformForImport", () => { + let logSpy: any; + + beforeEach(() => { + logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + }); + + afterEach(() => { + logSpy.mockRestore(); + }); + + it("converts a tagged Uint8Array object back to Uint8Array", async () => { + const original = new Uint8Array([1, 2, 3]); + const encoded = { + __type: "Uint8Array", + data: uint8ArrayToBase64(original), + }; + const result = await transformForImport(encoded); + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toEqual(original); + }); + + it("converts a tagged Blob object back to Blob", async () => { + const original = new Uint8Array([4, 5, 6]); + const encoded = { __type: "Blob", data: uint8ArrayToBase64(original) }; + const result = await transformForImport(encoded); + expect(result).toBeInstanceOf(Blob); + const buf = await result.arrayBuffer(); + expect(new Uint8Array(buf)).toEqual(original); + }); + + it("transforms an array recursively", async () => { + const original = new Uint8Array([7]); + const encoded = [ + { __type: "Uint8Array", data: uint8ArrayToBase64(original) }, + 42, + "hello", + ]; + const result = await transformForImport(encoded); + expect(result[0]).toEqual(original); + expect(result[1]).toBe(42); + expect(result[2]).toBe("hello"); + }); + + it("transforms a nested record recursively", async () => { + const bytes = new Uint8Array([8, 9]); + const encoded = { + data: { __type: "Uint8Array", data: uint8ArrayToBase64(bytes) }, + count: 5, + }; + const result = await transformForImport(encoded); + expect(result.data).toEqual(bytes); + expect(result.count).toBe(5); + }); + + it("returns primitives unchanged", async () => { + expect(await transformForImport(42)).toBe(42); + expect(await transformForImport("hello")).toBe("hello"); + expect(await transformForImport(null)).toBeNull(); + expect(await transformForImport(true)).toBe(true); + }); + + it("round-trips through export transformForExport", async () => { + const original = new Uint8Array([10, 20, 30]); + const { transformForExport } = await import("./export.js"); + const exported = await transformForExport(original); + const reimported = await transformForImport(exported); + expect(reimported).toEqual(original); + }); +}); + +// ================================================================================================ +// forceImportStore tests +// ================================================================================================ + +describe("forceImportStore", () => { + let logSpy: any; + let errorSpy: any; + let warnSpy: any; + + beforeEach(() => { + logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + }); + + afterEach(() => { + logSpy.mockRestore(); + errorSpy.mockRestore(); + warnSpy.mockRestore(); + }); + + it("round-trip: export DB-A, import into DB-B, rows match", async () => { + const dbIdA = await openTestDb(); + const dbA = getDatabase(dbIdA); + + const assetBytes = new Uint8Array([11, 22, 33]); + const serialBytes = new Uint8Array([44, 55, 66, 77]); + const inputsBytes = new Uint8Array([1]); + const stateBytes = new Uint8Array([2, 3]); + + // Insert a row into DB-A + await dbA.inputNotes.put({ + noteId: "round-trip-note", + stateDiscriminant: 0, + assets: assetBytes, + serialNumber: serialBytes, + inputs: inputsBytes, + scriptRoot: "sr-round-trip", + nullifier: "nullifier-round-trip", + serializedCreatedAt: "2024-06-01", + state: stateBytes, + }); + + // Export DB-A + const jsonStr = await exportStore(dbIdA); + + // Open DB-B and import + const dbIdB = await openTestDb(); + await forceImportStore(dbIdB, jsonStr); + + // Verify DB-B has the same inputNotes row + const dbB = getDatabase(dbIdB); + const notesB = await dbB.inputNotes.toArray(); + const imported = notesB.find((n) => n.noteId === "round-trip-note"); + expect(imported).toBeDefined(); + expect(imported!.assets).toEqual(assetBytes); + expect(imported!.serialNumber).toEqual(serialBytes); + expect(imported!.inputs).toEqual(inputsBytes); + expect(imported!.state).toEqual(stateBytes); + expect(imported!.scriptRoot).toBe("sr-round-trip"); + }); + + it("round-trip preserves all populated tables", async () => { + const dbIdA = await openTestDb(); + const dbA = getDatabase(dbIdA); + + await dbA.accountCodes.put({ + root: "root-rt", + code: new Uint8Array([1, 2, 3]), + }); + + await dbA.tags.put({ tag: "tag-rt" }); + + const jsonStr = await exportStore(dbIdA); + + const dbIdB = await openTestDb(); + await forceImportStore(dbIdB, jsonStr); + + const dbB = getDatabase(dbIdB); + const codesB = await dbB.accountCodes.toArray(); + expect(codesB).toHaveLength(1); + expect(codesB[0].root).toBe("root-rt"); + expect(codesB[0].code).toEqual(new Uint8Array([1, 2, 3])); + + const tagsB = await dbB.tags.toArray(); + const tagFound = tagsB.find((t) => t.tag === "tag-rt"); + expect(tagFound).toBeDefined(); + }); + + it("import clears existing rows in target DB before importing", async () => { + const dbIdA = await openTestDb(); + const dbA = getDatabase(dbIdA); + + await dbA.accountCodes.put({ root: "root-a1", code: new Uint8Array([1]) }); + + const jsonStr = await exportStore(dbIdA); + + const dbIdB = await openTestDb(); + const dbB = getDatabase(dbIdB); + + // Pre-populate DB-B with a row that should be wiped + await dbB.accountCodes.put({ + root: "root-b-old", + code: new Uint8Array([9]), + }); + expect(await dbB.accountCodes.count()).toBe(1); + + await forceImportStore(dbIdB, jsonStr); + + const codesAfter = await dbB.accountCodes.toArray(); + // Only the row from DB-A should remain + expect(codesAfter.map((c) => c.root)).toContain("root-a1"); + expect(codesAfter.map((c) => c.root)).not.toContain("root-b-old"); + }); + + it("handles double-serialized JSON (string payload)", async () => { + const dbIdA = await openTestDb(); + const jsonStr = await exportStore(dbIdA); + // Double-encode: JSON.stringify the string again + const doubleEncoded = JSON.stringify(jsonStr); + + const dbIdB = await openTestDb(); + // Should not throw — import.ts handles double-encoded payloads + await forceImportStore(dbIdB, doubleEncoded); + }); + + it("throws when payload has no tables (empty JSON object {})", async () => { + const dbId = await openTestDb(); + // {} parses to an object with zero keys — triggers "No tables found" error + await expect(forceImportStore(dbId, "{}")).rejects.toThrow( + "No tables found" + ); + }); + + it("throws when the payload contains only unknown table names", async () => { + // Dexie.table() throws InvalidTableError before the warn+skip guard in import.ts + // can fire, because the table name is not in the transaction scope. This verifies + // the real (observed) behavior of the source rather than its intent comment. + const dbId = await openTestDb(); + const payload = JSON.stringify({ unknownTable: [{ id: 1, value: "x" }] }); + await expect(forceImportStore(dbId, payload)).rejects.toThrow(); + }); + + it("warn+skip guard: covers lines 86-90 by mocking dexie.table not to throw for unknown names", async () => { + // The warn+skip guard in import.ts (lines 85-90) is normally dead code because + // db.dexie.table() throws InvalidTableError for unknown tables before the guard is reached. + // We mock the table accessor to make it return a fake table object for unknown names, + // allowing execution to reach the guard and exercise the console.warn + continue path. + const dbId = await openTestDb(); + const db = getDatabase(dbId); + + const origTable = db.dexie.table.bind(db.dexie); + const fakeBulkPut = vi.fn().mockResolvedValue(undefined); + const fakeTableStub = { bulkPut: fakeBulkPut }; + + vi.spyOn(db.dexie, "table").mockImplementation((name: string) => { + // For unknown table names, return a stub instead of throwing. + // For known tables, delegate to the real implementation. + try { + return origTable(name); + } catch { + return fakeTableStub as any; + } + }); + + const payload = JSON.stringify({ unknownTable: [{ id: 1 }] }); + + // Should resolve without error (unknown table is warned and skipped) + await forceImportStore(dbId, payload); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("unknownTable") + ); + // The stub's bulkPut should NOT have been called (we skipped it) + expect(fakeBulkPut).not.toHaveBeenCalled(); + + vi.restoreAllMocks(); + }); + + it("throws for a db that was never opened", async () => { + await expect( + forceImportStore("never-opened", JSON.stringify({ someTable: [] })) + ).rejects.toThrow(); + }); + + it("throws on malformed JSON payload", async () => { + const dbId = await openTestDb(); + await expect(forceImportStore(dbId, "not-valid-json{{")).rejects.toThrow(); + }); +}); diff --git a/crates/idxdb-store/src/ts/notes.test.ts b/crates/idxdb-store/src/ts/notes.test.ts index 43b57d2..0686a7f 100644 --- a/crates/idxdb-store/src/ts/notes.test.ts +++ b/crates/idxdb-store/src/ts/notes.test.ts @@ -1,6 +1,19 @@ import { describe, it, expect, afterEach } from "vitest"; import { openDatabase, getDatabase } from "./schema.js"; -import { upsertInputNote, getInputNoteByOffset } from "./notes.js"; +import { + upsertInputNote, + upsertOutputNote, + upsertNoteScript, + getInputNoteByOffset, + getInputNotes, + getInputNotesFromIds, + getInputNotesFromNullifiers, + getOutputNotes, + getOutputNotesFromIds, + getOutputNotesFromNullifiers, + getUnspentInputNoteNullifiers, + getNoteScript, +} from "./notes.js"; // Unique DB names to avoid collisions between tests. let dbCounter = 0; @@ -39,6 +52,11 @@ const CONSUMED_STATES = new Uint8Array([ STATE_CONSUMED_EXTERNAL, ]); +// Unspent state discriminants (stateDiscriminant 2, 4, 5) +const STATE_COMMITTED = 2; +const STATE_PROCESSING_AUTHENTICATED = 4; +const STATE_PROCESSING_UNAUTHENTICATED = 5; + const DUMMY_BYTES = new Uint8Array([1, 2, 3]); const DUMMY_SCRIPT_ROOT = "script-root-1"; @@ -55,6 +73,8 @@ async function insertNote( consumedBlockHeight?: number; consumedTxOrder?: number; consumerAccountId?: string; + scriptRoot?: string; + nullifier?: string; } = {} ) { await upsertInputNote( @@ -63,9 +83,9 @@ async function insertNote( DUMMY_BYTES, DUMMY_BYTES, DUMMY_BYTES, - DUMMY_SCRIPT_ROOT, + opts.scriptRoot ?? DUMMY_SCRIPT_ROOT, DUMMY_BYTES, - `nullifier-${noteId}`, + opts.nullifier ?? `nullifier-${noteId}`, noteId, // store noteId as createdAt so we can read it back from processed output opts.stateDiscriminant ?? STATE_CONSUMED_EXTERNAL, DUMMY_BYTES, @@ -296,6 +316,74 @@ describe("getInputNoteByOffset block range filtering", () => { }); }); +describe("getInputNoteByOffset unordered-filter branches", () => { + it("excludes unordered notes with null consumedBlockHeight when blockStart is set", async () => { + // Exercises the `consumedBlockHeight == null` branch in the unordered filter + // (line 218 of notes.ts: blockStart != null && (consumedBlockHeight == null || ...)) + const dbId = await openTestDb(); + + await insertNote(dbId, "note-ordered-b5", { + consumedBlockHeight: 5, + consumedTxOrder: 0, + }); + // Unordered note with null consumedBlockHeight — should be excluded by blockStart filter + await insertNote(dbId, "note-unordered-no-height", { + // no consumedBlockHeight — null + }); + + const ids = await collectAllNoteIds( + dbId, + CONSUMED_STATES, + undefined, + 3, + undefined + ); + expect(ids).toContain("note-ordered-b5"); + expect(ids).not.toContain("note-unordered-no-height"); + }); + + it("excludes unordered notes with null consumedBlockHeight when blockEnd is set", async () => { + // Exercises the `consumedBlockHeight == null` branch in the unordered filter + // (line 220 of notes.ts: blockEnd != null && (consumedBlockHeight == null || ...)) + const dbId = await openTestDb(); + + await insertNote(dbId, "note-ordered-b5", { + consumedBlockHeight: 5, + consumedTxOrder: 0, + }); + await insertNote(dbId, "note-unordered-no-height-2", { + // no consumedBlockHeight — null + }); + + const ids = await collectAllNoteIds( + dbId, + CONSUMED_STATES, + undefined, + undefined, + 10 + ); + expect(ids).toContain("note-ordered-b5"); + expect(ids).not.toContain("note-unordered-no-height-2"); + }); + + it("excludes unordered notes with consumerAccountId != undefined (line 217 branch)", async () => { + // In the unordered path, consumerAccountId filter is undefined. If a note has + // a non-undefined consumerAccountId, it should be excluded via line 217. + const dbId = await openTestDb(); + + await insertNote(dbId, "note-no-tx-with-consumer", { + consumerAccountId: "0xsomeconsumer", + consumedBlockHeight: 5, + // no consumedTxOrder — so not in compound index + }); + + // Query with no consumer (undefined) — the unordered filter line 217 excludes + // notes with a different consumerAccountId + const ids = await collectAllNoteIds(dbId, CONSUMED_STATES); + expect(ids).not.toContain("note-no-tx-with-consumer"); + }); +}); + // STATE FILTER TESTS // ================================================================================================ @@ -330,3 +418,556 @@ describe("getInputNoteByOffset state filtering", () => { expect(result).toEqual([]); }); }); + +// ================================================================================================ +// getInputNotes +// ================================================================================================ + +describe("getInputNotes", () => { + it("returns all notes when states is empty", async () => { + const dbId = await openTestDb(); + await insertNote(dbId, "n1", { + stateDiscriminant: STATE_CONSUMED_EXTERNAL, + consumedBlockHeight: 1, + }); + await insertNote(dbId, "n2", { stateDiscriminant: STATE_EXPECTED }); + const result = await getInputNotes(dbId, new Uint8Array([])); + expect(result).toHaveLength(2); + }); + + it("filters by state discriminants when non-empty", async () => { + const dbId = await openTestDb(); + await insertNote(dbId, "n-consumed", { + stateDiscriminant: STATE_CONSUMED_EXTERNAL, + consumedBlockHeight: 1, + }); + await insertNote(dbId, "n-expected", { stateDiscriminant: STATE_EXPECTED }); + const result = await getInputNotes( + dbId, + new Uint8Array([STATE_CONSUMED_EXTERNAL]) + ); + expect(result).toHaveLength(1); + // createdAt holds the noteId + expect(result![0].createdAt).toBe("n-consumed"); + }); + + it("returns empty array when no notes exist", async () => { + const dbId = await openTestDb(); + const result = await getInputNotes(dbId, new Uint8Array([])); + expect(result).toEqual([]); + }); + + it("includes note script in processed result when available", async () => { + const dbId = await openTestDb(); + const SCRIPT_ROOT = "my-script-root"; + await insertNote(dbId, "note-with-script", { + stateDiscriminant: STATE_CONSUMED_EXTERNAL, + consumedBlockHeight: 1, + scriptRoot: SCRIPT_ROOT, + }); + const result = await getInputNotes( + dbId, + new Uint8Array([STATE_CONSUMED_EXTERNAL]) + ); + expect(result).toHaveLength(1); + // Script was inserted via upsertInputNote → notesScripts table + expect(result![0].serializedNoteScript).toBeDefined(); + expect(typeof result![0].serializedNoteScript).toBe("string"); + }); + + it("returns undefined for serializedNoteScript when script root is empty", async () => { + const dbId = await openTestDb(); + // Insert with empty scriptRoot + await upsertInputNote( + dbId, + "note-no-script", + DUMMY_BYTES, + DUMMY_BYTES, + DUMMY_BYTES, + "", // empty script root + DUMMY_BYTES, + "null-nullifier", + "note-no-script", + STATE_CONSUMED_EXTERNAL, + DUMMY_BYTES, + 1, + 0, + undefined + ); + const result = await getInputNotes( + dbId, + new Uint8Array([STATE_CONSUMED_EXTERNAL]) + ); + expect(result).toHaveLength(1); + expect(result![0].serializedNoteScript).toBeUndefined(); + }); +}); + +// ================================================================================================ +// getInputNotesFromIds +// ================================================================================================ + +describe("getInputNotesFromIds", () => { + it("returns notes matching the given IDs", async () => { + const dbId = await openTestDb(); + await insertNote(dbId, "id-note-1", { + stateDiscriminant: STATE_CONSUMED_EXTERNAL, + consumedBlockHeight: 1, + }); + await insertNote(dbId, "id-note-2", { + stateDiscriminant: STATE_CONSUMED_EXTERNAL, + consumedBlockHeight: 2, + }); + await insertNote(dbId, "id-note-3", { stateDiscriminant: STATE_EXPECTED }); + + const result = await getInputNotesFromIds(dbId, ["id-note-1", "id-note-2"]); + expect(result).toHaveLength(2); + }); + + it("returns empty array for unmatched IDs", async () => { + const dbId = await openTestDb(); + const result = await getInputNotesFromIds(dbId, ["nonexistent"]); + expect(result).toEqual([]); + }); +}); + +// ================================================================================================ +// getInputNotesFromNullifiers +// ================================================================================================ + +describe("getInputNotesFromNullifiers", () => { + it("returns notes matching the given nullifiers", async () => { + const dbId = await openTestDb(); + await insertNote(dbId, "null-note-1", { + stateDiscriminant: STATE_CONSUMED_EXTERNAL, + consumedBlockHeight: 1, + nullifier: "0xnullifier1", + }); + await insertNote(dbId, "null-note-2", { + stateDiscriminant: STATE_CONSUMED_EXTERNAL, + consumedBlockHeight: 2, + nullifier: "0xnullifier2", + }); + + const result = await getInputNotesFromNullifiers(dbId, ["0xnullifier1"]); + expect(result).toHaveLength(1); + expect(result![0].createdAt).toBe("null-note-1"); + }); + + it("returns empty array for unknown nullifiers", async () => { + const dbId = await openTestDb(); + const result = await getInputNotesFromNullifiers(dbId, ["0xunknown"]); + expect(result).toEqual([]); + }); +}); + +// ================================================================================================ +// getUnspentInputNoteNullifiers +// ================================================================================================ + +describe("getUnspentInputNoteNullifiers", () => { + it("returns nullifiers for notes with discriminant 2, 4, or 5", async () => { + const dbId = await openTestDb(); + await insertNote(dbId, "note-committed", { + stateDiscriminant: STATE_COMMITTED, + nullifier: "0xnull-committed", + }); + await insertNote(dbId, "note-proc-auth", { + stateDiscriminant: STATE_PROCESSING_AUTHENTICATED, + nullifier: "0xnull-proc-auth", + }); + await insertNote(dbId, "note-proc-unauth", { + stateDiscriminant: STATE_PROCESSING_UNAUTHENTICATED, + nullifier: "0xnull-proc-unauth", + }); + await insertNote(dbId, "note-expected", { + stateDiscriminant: STATE_EXPECTED, + nullifier: "0xnull-expected", + }); + + const nullifiers = await getUnspentInputNoteNullifiers(dbId); + expect(nullifiers).toHaveLength(3); + expect(nullifiers).toContain("0xnull-committed"); + expect(nullifiers).toContain("0xnull-proc-auth"); + expect(nullifiers).toContain("0xnull-proc-unauth"); + expect(nullifiers).not.toContain("0xnull-expected"); + }); + + it("returns empty array when no unspent notes", async () => { + const dbId = await openTestDb(); + const nullifiers = await getUnspentInputNoteNullifiers(dbId); + expect(nullifiers).toEqual([]); + }); +}); + +// ================================================================================================ +// getNoteScript +// ================================================================================================ + +describe("getNoteScript", () => { + it("returns undefined when script not found", async () => { + const dbId = await openTestDb(); + const result = await getNoteScript(dbId, "nonexistent-root"); + expect(result).toBeUndefined(); + }); + + it("returns the script record when found", async () => { + const dbId = await openTestDb(); + const scriptRoot = "my-script"; + const scriptBytes = new Uint8Array([7, 8, 9]); + await upsertNoteScript(dbId, scriptRoot, scriptBytes); + const result = await getNoteScript(dbId, scriptRoot); + expect(result).toBeDefined(); + expect(result!.scriptRoot).toBe(scriptRoot); + expect(result!.serializedNoteScript).toEqual(scriptBytes); + }); +}); + +// ================================================================================================ +// upsertNoteScript +// ================================================================================================ + +describe("upsertNoteScript", () => { + it("inserts and overwrites a note script", async () => { + const dbId = await openTestDb(); + const scriptRoot = "root-1"; + await upsertNoteScript(dbId, scriptRoot, new Uint8Array([1, 2, 3])); + await upsertNoteScript(dbId, scriptRoot, new Uint8Array([4, 5, 6])); + const result = await getNoteScript(dbId, scriptRoot); + expect(result!.serializedNoteScript).toEqual(new Uint8Array([4, 5, 6])); + }); +}); + +// ================================================================================================ +// getOutputNotes +// ================================================================================================ + +describe("getOutputNotes", () => { + it("returns all output notes when states is empty", async () => { + const dbId = await openTestDb(); + await upsertOutputNote( + dbId, + "out-1", + DUMMY_BYTES, + "recipient1", + DUMMY_BYTES, + "0xnull1", + 100, + 3, + DUMMY_BYTES + ); + await upsertOutputNote( + dbId, + "out-2", + DUMMY_BYTES, + "recipient2", + DUMMY_BYTES, + undefined, + 200, + 4, + DUMMY_BYTES + ); + const result = await getOutputNotes(dbId, new Uint8Array([])); + expect(result).toHaveLength(2); + }); + + it("filters output notes by state discriminant", async () => { + const dbId = await openTestDb(); + await upsertOutputNote( + dbId, + "out-state3", + DUMMY_BYTES, + "r1", + DUMMY_BYTES, + "0xn1", + 100, + 3, + DUMMY_BYTES + ); + await upsertOutputNote( + dbId, + "out-state4", + DUMMY_BYTES, + "r2", + DUMMY_BYTES, + "0xn2", + 200, + 4, + DUMMY_BYTES + ); + + const result = await getOutputNotes(dbId, new Uint8Array([3])); + expect(result).toHaveLength(1); + }); + + it("returns processed output note with base64 fields", async () => { + const dbId = await openTestDb(); + await upsertOutputNote( + dbId, + "out-processed", + DUMMY_BYTES, + "recipient-x", + DUMMY_BYTES, + "0xnull-x", + 50, + 3, + DUMMY_BYTES + ); + const result = await getOutputNotes(dbId, new Uint8Array([])); + expect(result).toHaveLength(1); + const note = result![0]; + expect(typeof note.assets).toBe("string"); // base64 + expect(typeof note.metadata).toBe("string"); // base64 + expect(note.recipientDigest).toBe("recipient-x"); + expect(note.expectedHeight).toBe(50); + }); + + it("returns empty array when no output notes", async () => { + const dbId = await openTestDb(); + const result = await getOutputNotes(dbId, new Uint8Array([])); + expect(result).toEqual([]); + }); +}); + +// ================================================================================================ +// getOutputNotesFromIds +// ================================================================================================ + +describe("getOutputNotesFromIds", () => { + it("returns output notes matching the given IDs", async () => { + const dbId = await openTestDb(); + await upsertOutputNote( + dbId, + "out-id-1", + DUMMY_BYTES, + "r1", + DUMMY_BYTES, + "0xn1", + 100, + 3, + DUMMY_BYTES + ); + await upsertOutputNote( + dbId, + "out-id-2", + DUMMY_BYTES, + "r2", + DUMMY_BYTES, + "0xn2", + 200, + 4, + DUMMY_BYTES + ); + + const result = await getOutputNotesFromIds(dbId, ["out-id-1"]); + expect(result).toHaveLength(1); + expect(result![0].recipientDigest).toBe("r1"); + }); + + it("returns empty array for unmatched IDs", async () => { + const dbId = await openTestDb(); + const result = await getOutputNotesFromIds(dbId, ["does-not-exist"]); + expect(result).toEqual([]); + }); +}); + +// ================================================================================================ +// getOutputNotesFromNullifiers +// ================================================================================================ + +describe("getOutputNotesFromNullifiers", () => { + it("returns output notes matching the given nullifiers", async () => { + const dbId = await openTestDb(); + await upsertOutputNote( + dbId, + "out-null-1", + DUMMY_BYTES, + "r1", + DUMMY_BYTES, + "0xoutnull1", + 100, + 3, + DUMMY_BYTES + ); + await upsertOutputNote( + dbId, + "out-null-2", + DUMMY_BYTES, + "r2", + DUMMY_BYTES, + "0xoutnull2", + 200, + 4, + DUMMY_BYTES + ); + + const result = await getOutputNotesFromNullifiers(dbId, ["0xoutnull1"]); + expect(result).toHaveLength(1); + expect(result![0].recipientDigest).toBe("r1"); + }); + + it("returns empty when nullifier not found", async () => { + const dbId = await openTestDb(); + const result = await getOutputNotesFromNullifiers(dbId, ["0xunknown"]); + expect(result).toEqual([]); + }); +}); + +// ================================================================================================ +// upsertInputNote with provided transaction +// ================================================================================================ + +describe("upsertInputNote with external transaction", () => { + it("uses an external transaction when provided", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + + // Pass a transaction object to upsertInputNote (the `tx` code path) + await db.dexie.transaction( + "rw", + db.inputNotes, + db.notesScripts, + async (tx) => { + await upsertInputNote( + dbId, + "tx-note-1", + DUMMY_BYTES, + DUMMY_BYTES, + DUMMY_BYTES, + "tx-script-root", + DUMMY_BYTES, + "tx-nullifier", + "tx-note-1", + STATE_CONSUMED_EXTERNAL, + DUMMY_BYTES, + 10, + 0, + undefined, + tx + ); + } + ); + + const result = await getInputNotesFromIds(dbId, ["tx-note-1"]); + expect(result).toHaveLength(1); + }); +}); + +// ================================================================================================ +// upsertOutputNote with external transaction +// ================================================================================================ + +describe("upsertOutputNote with external transaction", () => { + it("uses an external transaction when provided", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + + await db.dexie.transaction( + "rw", + db.outputNotes, + db.notesScripts, + async (tx) => { + await upsertOutputNote( + dbId, + "out-tx-1", + DUMMY_BYTES, + "recipient-tx", + DUMMY_BYTES, + "0xtxnull", + 999, + 3, + DUMMY_BYTES, + tx + ); + } + ); + + const result = await getOutputNotesFromIds(dbId, ["out-tx-1"]); + expect(result).toHaveLength(1); + expect(result![0].recipientDigest).toBe("recipient-tx"); + }); +}); + +// ================================================================================================ +// Error-path coverage: catch blocks call logWebStoreError (re-throws) +// Passing an unregistered dbId exercises the catch body in each function. +// ================================================================================================ +const BAD_DB = "does-not-exist-notes"; + +describe("error paths: unregistered dbId re-throws", () => { + it("getOutputNotes rejects on bad dbId", async () => { + await expect(getOutputNotes(BAD_DB, new Uint8Array([]))).rejects.toThrow(); + }); + + it("getInputNotes rejects on bad dbId", async () => { + await expect(getInputNotes(BAD_DB, new Uint8Array([]))).rejects.toThrow(); + }); + + it("getInputNotesFromIds rejects on bad dbId", async () => { + await expect(getInputNotesFromIds(BAD_DB, ["id1"])).rejects.toThrow(); + }); + + it("getInputNotesFromNullifiers rejects on bad dbId", async () => { + await expect( + getInputNotesFromNullifiers(BAD_DB, ["null1"]) + ).rejects.toThrow(); + }); + + it("getOutputNotesFromNullifiers rejects on bad dbId", async () => { + await expect( + getOutputNotesFromNullifiers(BAD_DB, ["null1"]) + ).rejects.toThrow(); + }); + + it("getOutputNotesFromIds rejects on bad dbId", async () => { + await expect(getOutputNotesFromIds(BAD_DB, ["id1"])).rejects.toThrow(); + }); + + it("getUnspentInputNoteNullifiers rejects on bad dbId", async () => { + await expect(getUnspentInputNoteNullifiers(BAD_DB)).rejects.toThrow(); + }); + + it("getNoteScript rejects on bad dbId", async () => { + await expect(getNoteScript(BAD_DB, "root1")).rejects.toThrow(); + }); + + it("getInputNoteByOffset rejects on bad dbId", async () => { + await expect( + getInputNoteByOffset( + BAD_DB, + new Uint8Array([]), + undefined, + undefined, + undefined, + 0 + ) + ).rejects.toThrow(); + }); + + it("upsertInputNote rejects on bad dbId (no tx, bad db)", async () => { + await expect( + upsertInputNote( + BAD_DB, + "note-1", + DUMMY_BYTES, + DUMMY_BYTES, + DUMMY_BYTES, + "root", + DUMMY_BYTES, + "null-1", + "note-1", + 0, + DUMMY_BYTES, + undefined, + undefined, + undefined + ) + ).rejects.toThrow(); + }); + + it("upsertNoteScript rejects on bad dbId", async () => { + await expect( + upsertNoteScript(BAD_DB, "root", new Uint8Array([1])) + ).rejects.toThrow(); + }); +}); diff --git a/crates/idxdb-store/src/ts/schema.test.ts b/crates/idxdb-store/src/ts/schema.test.ts index 6a3ebbf..79e3441 100644 --- a/crates/idxdb-store/src/ts/schema.test.ts +++ b/crates/idxdb-store/src/ts/schema.test.ts @@ -1,5 +1,11 @@ import { describe, it, expect, afterEach } from "vitest"; import Dexie from "dexie"; +import { + openDatabase, + getDatabase, + MidenDatabase, + CLIENT_VERSION_SETTING_KEY, +} from "./schema.js"; import { uniqueDbName } from "./test-utils.js"; const encoder = new TextEncoder(); @@ -21,6 +27,22 @@ function trackDb(db: Dexie): Dexie { return db; } +// Track MidenDatabase instances separately (they wrap a Dexie under .dexie) +const openMidenDbs: MidenDatabase[] = []; + +afterEach(async () => { + for (const mdb of openMidenDbs) { + mdb.dexie.close(); + await mdb.dexie.delete(); + } + openMidenDbs.length = 0; +}); + +function trackMidenDb(mdb: MidenDatabase): MidenDatabase { + openMidenDbs.push(mdb); + return mdb; +} + describe("MidenDatabase migrations", () => { // Placeholder for the actual v1→v2 migration test. When the first real // migration is introduced, replace the dummy schema and upgrade logic below @@ -85,3 +107,188 @@ describe("MidenDatabase migrations", () => { expect(decoder.decode(setting.value)).toBe("blue"); }); }); + +// ============================================================ +// openDatabase +// ============================================================ +describe("openDatabase", () => { + it("opens a fresh database and registers it in the registry", async () => { + const name = uniqueDbName(); + const dbId = await openDatabase(name, "1.0.0"); + openMidenDbs.push(getDatabase(dbId)); + expect(dbId).toBe(name); + const db = getDatabase(dbId); + expect(db).toBeDefined(); + }); + + it("persists the client version on first open", async () => { + const name = uniqueDbName(); + await openDatabase(name, "1.0.0"); + const db = getDatabase(name); + openMidenDbs.push(db); + const record = await db.settings.get(CLIENT_VERSION_SETTING_KEY); + expect(record).toBeDefined(); + expect(new TextDecoder().decode(record!.value)).toBe("1.0.0"); + }); +}); + +// ============================================================ +// ensureClientVersion — same version (no-op) +// ============================================================ +describe("ensureClientVersion: same version already stored", () => { + it("re-opening with the same version is a no-op", async () => { + const name = uniqueDbName(); + // First open + await openDatabase(name, "2.3.4"); + const db1 = getDatabase(name); + openMidenDbs.push(db1); + + // Insert a sentinel row that should survive if the DB is NOT nuked + await db1.settings.put({ + key: "sentinel", + value: new TextEncoder().encode("alive"), + }); + + // Close and re-open with the same version + db1.dexie.close(); + + const mdb2 = trackMidenDb(new MidenDatabase(name)); + const success = await mdb2.open("2.3.4"); + expect(success).toBe(true); + + // Sentinel must still be there + const sentinel = await mdb2.settings.get("sentinel"); + expect(sentinel).toBeDefined(); + expect(new TextDecoder().decode(sentinel!.value)).toBe("alive"); + }); +}); + +// ============================================================ +// ensureClientVersion — same major.minor, patch bump (update only) +// ============================================================ +describe("ensureClientVersion: same major.minor, new patch", () => { + it("updates persisted version without nuking the store", async () => { + const name = uniqueDbName(); + await openDatabase(name, "1.2.0"); + const db1 = getDatabase(name); + openMidenDbs.push(db1); + await db1.settings.put({ + key: "sentinel", + value: new TextEncoder().encode("safe"), + }); + db1.dexie.close(); + + // Patch bump: 1.2.0 → 1.2.5 + const mdb2 = trackMidenDb(new MidenDatabase(name)); + const success = await mdb2.open("1.2.5"); + expect(success).toBe(true); + + // Sentinel must survive (no nuke) + const sentinel = await mdb2.settings.get("sentinel"); + expect(sentinel).toBeDefined(); + + // Version must be updated + const versionRecord = await mdb2.settings.get(CLIENT_VERSION_SETTING_KEY); + expect(new TextDecoder().decode(versionRecord!.value)).toBe("1.2.5"); + }); +}); + +// ============================================================ +// ensureClientVersion — stored version is newer than requested (downgrade path) +// ============================================================ +describe("ensureClientVersion: stored version is newer (downgrade path)", () => { + it("does not nuke on downgrade — updates persisted version only", async () => { + const name = uniqueDbName(); + await openDatabase(name, "2.0.0"); + const db1 = getDatabase(name); + openMidenDbs.push(db1); + await db1.settings.put({ + key: "sentinel", + value: new TextEncoder().encode("present"), + }); + db1.dexie.close(); + + // Open with an older version (1.9.0 < 2.0.0) + const mdb2 = trackMidenDb(new MidenDatabase(name)); + await mdb2.open("1.9.0"); + + // The non-gt branch just persists the new version without nuking + const sentinel = await mdb2.settings.get("sentinel"); + expect(sentinel).toBeDefined(); + }); +}); + +// ============================================================ +// ensureClientVersion — major version bump (nuke path) +// ============================================================ +describe("ensureClientVersion: major version bump triggers nuke", () => { + it("nukes the database and persists the new version", async () => { + const name = uniqueDbName(); + await openDatabase(name, "1.0.0"); + const db1 = getDatabase(name); + openMidenDbs.push(db1); + // Insert a sentinel row that should be GONE after nuke + await db1.settings.put({ + key: "sentinel", + value: new TextEncoder().encode("gone-after-nuke"), + }); + db1.dexie.close(); + + // Open with a new major version (2.0.0 > 1.0.0, different minor) + const mdb2 = trackMidenDb(new MidenDatabase(name)); + const success = await mdb2.open("2.0.0"); + expect(success).toBe(true); + + // Sentinel should be gone (DB was nuked) + const sentinel = await mdb2.settings.get("sentinel"); + expect(sentinel).toBeUndefined(); + + // New version should be persisted + const versionRecord = await mdb2.settings.get(CLIENT_VERSION_SETTING_KEY); + expect(new TextDecoder().decode(versionRecord!.value)).toBe("2.0.0"); + }); +}); + +// ============================================================ +// ensureClientVersion — invalid semver strings (warn + nuke path) +// ============================================================ +describe("ensureClientVersion: invalid semver strings", () => { + it("falls through to nuke when stored version is not valid semver", async () => { + const name = uniqueDbName(); + // First open with a non-semver string + await openDatabase(name, "not-a-version"); + const db1 = getDatabase(name); + openMidenDbs.push(db1); + await db1.settings.put({ + key: "sentinel", + value: new TextEncoder().encode("will-be-nuked"), + }); + db1.dexie.close(); + + // Re-open with a different non-semver string — triggers the else branch + const mdb2 = trackMidenDb(new MidenDatabase(name)); + const success = await mdb2.open("also-not-a-version"); + expect(success).toBe(true); + + // After the nuke the sentinel is gone + const sentinel = await mdb2.settings.get("sentinel"); + expect(sentinel).toBeUndefined(); + }); +}); + +// ============================================================ +// ensureClientVersion — empty clientVersion (warn + skip) +// ============================================================ +describe("ensureClientVersion: empty clientVersion", () => { + it("skips version enforcement when clientVersion is empty string", async () => { + const name = uniqueDbName(); + const mdb = trackMidenDb(new MidenDatabase(name)); + // Pass empty string — should open successfully and skip enforcement + const success = await mdb.open(""); + expect(success).toBe(true); + + // No version record should be stored + const versionRecord = await mdb.settings.get(CLIENT_VERSION_SETTING_KEY); + expect(versionRecord).toBeUndefined(); + }); +}); diff --git a/crates/idxdb-store/src/ts/settings.test.ts b/crates/idxdb-store/src/ts/settings.test.ts new file mode 100644 index 0000000..0bcdc74 --- /dev/null +++ b/crates/idxdb-store/src/ts/settings.test.ts @@ -0,0 +1,119 @@ +import { describe, it, expect, afterEach, vi, beforeEach } from "vitest"; +import { + openDatabase, + getDatabase, + CLIENT_VERSION_SETTING_KEY, +} from "./schema.js"; +import { + getSetting, + insertSetting, + removeSetting, + listSettingKeys, +} from "./settings.js"; + +let dbCounter = 0; +function uniqueDbName(): string { + return `test-settings-${++dbCounter}-${Date.now()}`; +} + +const openDbIds: string[] = []; + +afterEach(async () => { + for (const dbId of openDbIds) { + const db = getDatabase(dbId); + db.dexie.close(); + await db.dexie.delete(); + } + openDbIds.length = 0; +}); + +async function openTestDb(): Promise { + const name = uniqueDbName(); + await openDatabase(name, "0.1.0"); + openDbIds.push(name); + return name; +} + +describe("settings", () => { + let errorSpy: any; + let logSpy: any; + + beforeEach(() => { + errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + }); + + afterEach(() => { + errorSpy.mockRestore(); + logSpy.mockRestore(); + }); + + it("returns null when key is missing", async () => { + const dbId = await openTestDb(); + const result = await getSetting(dbId, "nope"); + expect(result).toBeNull(); + }); + + it("inserts and retrieves a setting", async () => { + const dbId = await openTestDb(); + const value = new Uint8Array([1, 2, 3]); + await insertSetting(dbId, "k1", value); + const got = await getSetting(dbId, "k1"); + expect(got).toEqual({ key: "k1", value: "AQID" }); + }); + + it("upserts on duplicate key", async () => { + const dbId = await openTestDb(); + await insertSetting(dbId, "k1", new Uint8Array([1])); + await insertSetting(dbId, "k1", new Uint8Array([2])); + const got = await getSetting(dbId, "k1"); + expect(got!.value).toBe("Ag=="); + }); + + it("removes a setting", async () => { + const dbId = await openTestDb(); + await insertSetting(dbId, "k1", new Uint8Array([1])); + await removeSetting(dbId, "k1"); + expect(await getSetting(dbId, "k1")).toBeNull(); + }); + + it("removeSetting on a missing key is a no-op", async () => { + const dbId = await openTestDb(); + await removeSetting(dbId, "nope"); + // No throw means success. + }); + + it("listSettingKeys excludes internal keys", async () => { + const dbId = await openTestDb(); + await insertSetting(dbId, "user-a", new Uint8Array([1])); + await insertSetting(dbId, "user-b", new Uint8Array([2])); + await insertSetting(dbId, CLIENT_VERSION_SETTING_KEY, new Uint8Array([3])); + const keys = await listSettingKeys(dbId); + expect(keys).toEqual(expect.arrayContaining(["user-a", "user-b"])); + expect(keys).not.toContain(CLIENT_VERSION_SETTING_KEY); + }); + + it("listSettingKeys returns empty list when no user keys are present", async () => { + const dbId = await openTestDb(); + const keys = await listSettingKeys(dbId); + expect(keys).toEqual([]); + }); + + it("getSetting throws on Dexie error (e.g., db not opened)", async () => { + await expect(getSetting("never-opened", "k")).rejects.toThrow(); + }); + + it("insertSetting throws on Dexie error", async () => { + await expect( + insertSetting("never-opened", "k", new Uint8Array([1])) + ).rejects.toThrow(); + }); + + it("removeSetting throws on Dexie error", async () => { + await expect(removeSetting("never-opened", "k")).rejects.toThrow(); + }); + + it("listSettingKeys throws on Dexie error", async () => { + await expect(listSettingKeys("never-opened")).rejects.toThrow(); + }); +}); diff --git a/crates/idxdb-store/src/ts/sync.test.ts b/crates/idxdb-store/src/ts/sync.test.ts new file mode 100644 index 0000000..39ca48c --- /dev/null +++ b/crates/idxdb-store/src/ts/sync.test.ts @@ -0,0 +1,931 @@ +import { describe, it, expect, afterEach, vi, beforeEach } from "vitest"; +import { openDatabase, getDatabase } from "./schema.js"; +import { + getNoteTags, + getSyncHeight, + addNoteTag, + removeNoteTag, + applyStateSync, + discardTransactions, +} from "./sync.js"; + +// --------------------------------------------------------------------------- +// Test DB helpers +// --------------------------------------------------------------------------- + +let dbCounter = 0; +function uniqueDbName(): string { + return `test-sync-${++dbCounter}-${Date.now()}`; +} + +const openDbIds: string[] = []; + +afterEach(async () => { + for (const dbId of openDbIds) { + const db = getDatabase(dbId); + db.dexie.close(); + await db.dexie.delete(); + } + openDbIds.length = 0; +}); + +async function openTestDb(): Promise { + const name = uniqueDbName(); + await openDatabase(name, "0.1.0"); + openDbIds.push(name); + return name; +} + +// Helper: uint8Array -> base64 (mirrors the source util) +function toBase64(bytes: Uint8Array): string { + const binary = bytes.reduce((acc, b) => acc + String.fromCharCode(b), ""); + return btoa(binary); +} + +// --------------------------------------------------------------------------- +// Minimal applyStateSync builder +// --------------------------------------------------------------------------- + +/** A FlattenedU8Vec-compatible object with zero entries. */ +function emptyFlattenedVec() { + return { + data: () => new Uint8Array(0), + lengths: () => [] as number[], + }; +} + +/** A FlattenedU8Vec holding a single Uint8Array chunk. */ +function singleFlattenedVec(chunk: Uint8Array) { + return { + data: () => chunk, + lengths: () => [chunk.length], + }; +} + +/** Build a minimal JsStateSyncUpdate that performs only what the test needs. */ +function minimalStateUpdate( + overrides: Partial[1]> = {} +): Parameters[1] { + return { + blockNum: 5, + flattenedNewBlockHeaders: emptyFlattenedVec(), + flattenedPartialBlockChainPeaks: emptyFlattenedVec(), + newBlockNums: [], + blockHasRelevantNotes: new Uint8Array(0), + serializedNodeIds: [], + serializedNodes: [], + committedNoteIds: [], + serializedInputNotes: [], + serializedOutputNotes: [], + accountUpdates: [], + transactionUpdates: [], + ...overrides, + }; +} + +// --------------------------------------------------------------------------- +// describe blocks +// --------------------------------------------------------------------------- + +describe("sync", () => { + let errorSpy: ReturnType; + let logSpy: ReturnType; + let warnSpy: ReturnType; + + beforeEach(() => { + errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + }); + + afterEach(() => { + errorSpy.mockRestore(); + logSpy.mockRestore(); + warnSpy.mockRestore(); + }); + + // ------------------------------------------------------------------------- + // getSyncHeight + // ------------------------------------------------------------------------- + + describe("getSyncHeight", () => { + it("returns blockNum 0 when DB was just created (populate hook seeds record)", async () => { + const dbId = await openTestDb(); + const result = await getSyncHeight(dbId); + expect(result).toEqual({ blockNum: 0 }); + }); + + it("returns the persisted blockNum after updating stateSync", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + // Manually bump blockNum to verify getSyncHeight reads it back + await db.stateSync.update(1, { blockNum: 42 }); + const result = await getSyncHeight(dbId); + expect(result).toEqual({ blockNum: 42 }); + }); + + it("returns null when no stateSync record exists (deleted)", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + await db.stateSync.delete(1); + const result = await getSyncHeight(dbId); + expect(result).toBeNull(); + }); + + it("rejects when db is not opened (logWebStoreError re-throws)", async () => { + await expect(getSyncHeight("never-opened-sync")).rejects.toThrow(); + expect(errorSpy).toHaveBeenCalled(); + }); + }); + + // ------------------------------------------------------------------------- + // getNoteTags + // ------------------------------------------------------------------------- + + describe("getNoteTags", () => { + it("returns an empty array when no tags exist", async () => { + const dbId = await openTestDb(); + const result = await getNoteTags(dbId); + expect(result).toEqual([]); + }); + + it("returns tags with sourceNoteId/sourceAccountId populated correctly", async () => { + const dbId = await openTestDb(); + await addNoteTag(dbId, new Uint8Array([0x01, 0x02]), "note-1", "acct-1"); + const tags = await getNoteTags(dbId); + expect(tags).toHaveLength(1); + expect(tags![0].sourceNoteId).toBe("note-1"); + expect(tags![0].sourceAccountId).toBe("acct-1"); + }); + + it("converts empty string sourceNoteId to undefined", async () => { + const dbId = await openTestDb(); + // addNoteTag stores "" when sourceNoteId is falsy; getNoteTags should normalise it back + const db = getDatabase(dbId); + await db.tags.add({ + tag: toBase64(new Uint8Array([0x0a])), + sourceNoteId: "", + sourceAccountId: "", + }); + const tags = await getNoteTags(dbId); + expect(tags).toHaveLength(1); + expect(tags![0].sourceNoteId).toBeUndefined(); + expect(tags![0].sourceAccountId).toBeUndefined(); + }); + + it("returns multiple tags in insertion order", async () => { + const dbId = await openTestDb(); + await addNoteTag(dbId, new Uint8Array([0x01]), "note-a", "acct-a"); + await addNoteTag(dbId, new Uint8Array([0x02]), "note-b", "acct-b"); + const tags = await getNoteTags(dbId); + expect(tags).toHaveLength(2); + }); + + it("rejects when db is not opened (logWebStoreError re-throws)", async () => { + await expect(getNoteTags("never-opened-sync")).rejects.toThrow(); + expect(errorSpy).toHaveBeenCalled(); + }); + }); + + // ------------------------------------------------------------------------- + // addNoteTag + // ------------------------------------------------------------------------- + + describe("addNoteTag", () => { + it("adds a tag with both sourceNoteId and sourceAccountId", async () => { + const dbId = await openTestDb(); + const tagBytes = new Uint8Array([0xde, 0xad]); + await addNoteTag(dbId, tagBytes, "note-1", "acct-1"); + + const db = getDatabase(dbId); + const stored = await db.tags.toArray(); + expect(stored).toHaveLength(1); + expect(stored[0].tag).toBe(toBase64(tagBytes)); + expect(stored[0].sourceNoteId).toBe("note-1"); + expect(stored[0].sourceAccountId).toBe("acct-1"); + }); + + it("stores empty string when sourceNoteId is falsy", async () => { + const dbId = await openTestDb(); + await addNoteTag(dbId, new Uint8Array([0x01]), "", ""); + + const db = getDatabase(dbId); + const stored = await db.tags.toArray(); + expect(stored[0].sourceNoteId).toBe(""); + expect(stored[0].sourceAccountId).toBe(""); + }); + + it("rejects when db is not opened (logWebStoreError re-throws)", async () => { + await expect( + addNoteTag("never-opened-sync", new Uint8Array([1]), "n", "a") + ).rejects.toThrow(); + expect(errorSpy).toHaveBeenCalled(); + }); + }); + + // ------------------------------------------------------------------------- + // removeNoteTag + // ------------------------------------------------------------------------- + + describe("removeNoteTag", () => { + it("removes the matching tag and returns delete count 1", async () => { + const dbId = await openTestDb(); + const tagBytes = new Uint8Array([0xab]); + await addNoteTag(dbId, tagBytes, "note-x", "acct-x"); + + const deleted = await removeNoteTag(dbId, tagBytes, "note-x", "acct-x"); + expect(deleted).toBe(1); + + const db = getDatabase(dbId); + expect(await db.tags.count()).toBe(0); + }); + + it("returns 0 when no matching tag exists", async () => { + const dbId = await openTestDb(); + const deleted = await removeNoteTag( + dbId, + new Uint8Array([0xff]), + "no-such-note" + ); + expect(deleted).toBe(0); + }); + + it("only removes the matching tag, leaving others intact", async () => { + const dbId = await openTestDb(); + await addNoteTag(dbId, new Uint8Array([0x01]), "note-1", "acct-1"); + await addNoteTag(dbId, new Uint8Array([0x02]), "note-2", "acct-2"); + + await removeNoteTag(dbId, new Uint8Array([0x01]), "note-1", "acct-1"); + + const db = getDatabase(dbId); + const remaining = await db.tags.toArray(); + expect(remaining).toHaveLength(1); + expect(remaining[0].sourceNoteId).toBe("note-2"); + }); + + it("uses empty string for sourceNoteId/sourceAccountId when undefined is passed", async () => { + const dbId = await openTestDb(); + // Add tag with empty sourceNoteId/sourceAccountId + await addNoteTag(dbId, new Uint8Array([0x05]), "", ""); + // Remove using undefined — internally converts to "" + const deleted = await removeNoteTag( + dbId, + new Uint8Array([0x05]), + undefined, + undefined + ); + expect(deleted).toBe(1); + }); + + it("rejects when db is not opened (logWebStoreError re-throws)", async () => { + await expect( + removeNoteTag("never-opened-sync", new Uint8Array([1])) + ).rejects.toThrow(); + expect(errorSpy).toHaveBeenCalled(); + }); + }); + + // ------------------------------------------------------------------------- + // discardTransactions + // ------------------------------------------------------------------------- + + describe("discardTransactions", () => { + it("removes transactions matching the provided ids", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + const status = new Uint8Array([0]); + await db.transactions.put({ + id: "tx-1", + details: new Uint8Array([1]), + blockNum: 1, + statusVariant: 0, + status, + }); + await db.transactions.put({ + id: "tx-2", + details: new Uint8Array([2]), + blockNum: 2, + statusVariant: 0, + status, + }); + await db.transactions.put({ + id: "tx-3", + details: new Uint8Array([3]), + blockNum: 3, + statusVariant: 0, + status, + }); + + await discardTransactions(dbId, ["tx-1", "tx-3"]); + + const remaining = await db.transactions.toArray(); + expect(remaining).toHaveLength(1); + expect(remaining[0].id).toBe("tx-2"); + }); + + it("is a no-op when the ids array is empty", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + const status = new Uint8Array([0]); + await db.transactions.put({ + id: "tx-keep", + details: new Uint8Array([1]), + blockNum: 1, + statusVariant: 0, + status, + }); + + await discardTransactions(dbId, []); + + const remaining = await db.transactions.toArray(); + expect(remaining).toHaveLength(1); + }); + + it("is a no-op when none of the ids exist", async () => { + const dbId = await openTestDb(); + const db = getDatabase(dbId); + await db.transactions.put({ + id: "tx-1", + details: new Uint8Array([1]), + blockNum: 1, + statusVariant: 0, + status: new Uint8Array([0]), + }); + + await discardTransactions(dbId, ["nonexistent"]); + const remaining = await db.transactions.toArray(); + expect(remaining).toHaveLength(1); + }); + + it("rejects when db is not opened (logWebStoreError re-throws)", async () => { + await expect( + discardTransactions("never-opened-sync", ["tx-1"]) + ).rejects.toThrow(); + expect(errorSpy).toHaveBeenCalled(); + }); + }); + + // ------------------------------------------------------------------------- + // applyStateSync — sync height update + // ------------------------------------------------------------------------- + + describe("applyStateSync — sync height", () => { + it("updates sync height to the given blockNum", async () => { + const dbId = await openTestDb(); + await applyStateSync(dbId, minimalStateUpdate({ blockNum: 10 })); + const result = await getSyncHeight(dbId); + expect(result).toEqual({ blockNum: 10 }); + }); + + it("does not regress sync height when a lower blockNum is applied", async () => { + const dbId = await openTestDb(); + // First advance to 20 + await applyStateSync(dbId, minimalStateUpdate({ blockNum: 20 })); + // Then apply a lower blockNum — should not overwrite + await applyStateSync(dbId, minimalStateUpdate({ blockNum: 5 })); + const result = await getSyncHeight(dbId); + expect(result).toEqual({ blockNum: 20 }); + }); + + it("advances sync height when a higher blockNum is applied", async () => { + const dbId = await openTestDb(); + await applyStateSync(dbId, minimalStateUpdate({ blockNum: 10 })); + await applyStateSync(dbId, minimalStateUpdate({ blockNum: 30 })); + const result = await getSyncHeight(dbId); + expect(result).toEqual({ blockNum: 30 }); + }); + }); + + // ------------------------------------------------------------------------- + // applyStateSync — block headers + // ------------------------------------------------------------------------- + + describe("applyStateSync — block headers", () => { + it("inserts a new block header during sync", async () => { + const dbId = await openTestDb(); + const headerBytes = new Uint8Array([0x10, 0x20]); + const peaksBytes = new Uint8Array([0x30, 0x40]); + + await applyStateSync( + dbId, + minimalStateUpdate({ + blockNum: 7, + newBlockNums: [7], + blockHasRelevantNotes: new Uint8Array([0]), + flattenedNewBlockHeaders: singleFlattenedVec(headerBytes), + flattenedPartialBlockChainPeaks: singleFlattenedVec(peaksBytes), + }) + ); + + const db = getDatabase(dbId); + const header = await db.blockHeaders.get(7); + expect(header).toBeDefined(); + expect(header!.blockNum).toBe(7); + expect(header!.hasClientNotes).toBe("false"); + }); + + it("marks block header hasClientNotes=true when blockHasRelevantNotes[i] === 1", async () => { + const dbId = await openTestDb(); + const headerBytes = new Uint8Array([0xaa]); + const peaksBytes = new Uint8Array([0xbb]); + + await applyStateSync( + dbId, + minimalStateUpdate({ + blockNum: 15, + newBlockNums: [15], + blockHasRelevantNotes: new Uint8Array([1]), + flattenedNewBlockHeaders: singleFlattenedVec(headerBytes), + flattenedPartialBlockChainPeaks: singleFlattenedVec(peaksBytes), + }) + ); + + const db = getDatabase(dbId); + const header = await db.blockHeaders.get(15); + expect(header!.hasClientNotes).toBe("true"); + }); + + it("does not overwrite an existing block header", async () => { + const dbId = await openTestDb(); + const original = new Uint8Array([0x01]); + const replacement = new Uint8Array([0xff]); + const peaks = new Uint8Array([0x00]); + + // Insert block header 5 first time + await applyStateSync( + dbId, + minimalStateUpdate({ + blockNum: 5, + newBlockNums: [5], + blockHasRelevantNotes: new Uint8Array([0]), + flattenedNewBlockHeaders: singleFlattenedVec(original), + flattenedPartialBlockChainPeaks: singleFlattenedVec(peaks), + }) + ); + + // Try to insert same block num with different data — should be skipped + await applyStateSync( + dbId, + minimalStateUpdate({ + blockNum: 6, + newBlockNums: [5], + blockHasRelevantNotes: new Uint8Array([0]), + flattenedNewBlockHeaders: singleFlattenedVec(replacement), + flattenedPartialBlockChainPeaks: singleFlattenedVec(peaks), + }) + ); + + const db = getDatabase(dbId); + const header = await db.blockHeaders.get(5); + expect(header!.header).toEqual(original); + }); + + it("handles zero block headers (empty newBlockNums)", async () => { + const dbId = await openTestDb(); + // No block headers — should complete without error + await applyStateSync(dbId, minimalStateUpdate({ blockNum: 3 })); + const result = await getSyncHeight(dbId); + expect(result).toEqual({ blockNum: 3 }); + }); + }); + + // ------------------------------------------------------------------------- + // applyStateSync — partial blockchain nodes + // ------------------------------------------------------------------------- + + describe("applyStateSync — partial blockchain nodes", () => { + it("inserts partial blockchain nodes", async () => { + const dbId = await openTestDb(); + await applyStateSync( + dbId, + minimalStateUpdate({ + blockNum: 1, + serializedNodeIds: ["42"], + serializedNodes: ["node-data-42"], + }) + ); + + const db = getDatabase(dbId); + const node = await db.partialBlockchainNodes.get(42); + expect(node).toBeDefined(); + expect(node!.node).toBe("node-data-42"); + }); + + it("overwrites an existing partial blockchain node (bulkPut)", async () => { + const dbId = await openTestDb(); + + await applyStateSync( + dbId, + minimalStateUpdate({ + blockNum: 1, + serializedNodeIds: ["10"], + serializedNodes: ["first-data"], + }) + ); + await applyStateSync( + dbId, + minimalStateUpdate({ + blockNum: 2, + serializedNodeIds: ["10"], + serializedNodes: ["second-data"], + }) + ); + + const db = getDatabase(dbId); + const node = await db.partialBlockchainNodes.get(10); + expect(node!.node).toBe("second-data"); + }); + + it("is a no-op when serializedNodeIds is empty", async () => { + const dbId = await openTestDb(); + // Should complete without error + await applyStateSync( + dbId, + minimalStateUpdate({ + blockNum: 1, + serializedNodeIds: [], + serializedNodes: [], + }) + ); + const db = getDatabase(dbId); + expect(await db.partialBlockchainNodes.count()).toBe(0); + }); + + it("rejects when nodeIndexes and nodes arrays have different lengths", async () => { + const dbId = await openTestDb(); + // Mismatched arrays — error thrown inside Dexie transaction, aborts tx and rejects + await expect( + applyStateSync( + dbId, + minimalStateUpdate({ + blockNum: 1, + serializedNodeIds: ["1", "2"], + serializedNodes: ["only-one"], + }) + ) + ).rejects.toThrow( + "nodeIndexes and nodes arrays must be of the same length" + ); + }); + }); + + // ------------------------------------------------------------------------- + // applyStateSync — committed note tags + // ------------------------------------------------------------------------- + + describe("applyStateSync — committed note tags (updateCommittedNoteTags)", () => { + it("removes tags whose sourceNoteId matches a committedNoteId", async () => { + const dbId = await openTestDb(); + // Add a tag that is associated with note-A + await addNoteTag(dbId, new Uint8Array([0x01]), "note-A", "acct-1"); + // Add a tag associated with note-B (should survive) + await addNoteTag(dbId, new Uint8Array([0x02]), "note-B", "acct-2"); + + await applyStateSync( + dbId, + minimalStateUpdate({ + blockNum: 1, + committedNoteIds: ["note-A"], + }) + ); + + const tags = await getNoteTags(dbId); + expect(tags).toHaveLength(1); + expect(tags![0].sourceNoteId).toBe("note-B"); + }); + + it("is a no-op when committedNoteIds is empty", async () => { + const dbId = await openTestDb(); + await addNoteTag(dbId, new Uint8Array([0x01]), "note-A", "acct-1"); + + await applyStateSync( + dbId, + minimalStateUpdate({ + blockNum: 1, + committedNoteIds: [], + }) + ); + + const tags = await getNoteTags(dbId); + expect(tags).toHaveLength(1); + }); + + it("removes all tags for multiple committedNoteIds", async () => { + const dbId = await openTestDb(); + await addNoteTag(dbId, new Uint8Array([0x01]), "note-A", "acct-1"); + await addNoteTag(dbId, new Uint8Array([0x02]), "note-B", "acct-2"); + await addNoteTag(dbId, new Uint8Array([0x03]), "note-C", "acct-3"); + + await applyStateSync( + dbId, + minimalStateUpdate({ + blockNum: 1, + committedNoteIds: ["note-A", "note-B"], + }) + ); + + const tags = await getNoteTags(dbId); + expect(tags).toHaveLength(1); + expect(tags![0].sourceNoteId).toBe("note-C"); + }); + }); + + // ------------------------------------------------------------------------- + // applyStateSync — transaction updates + // ------------------------------------------------------------------------- + + describe("applyStateSync — transaction updates", () => { + it("upserts a transaction record without a script", async () => { + const dbId = await openTestDb(); + await applyStateSync( + dbId, + minimalStateUpdate({ + blockNum: 5, + transactionUpdates: [ + { + id: "tx-sync-1", + details: new Uint8Array([1, 2, 3]), + blockNum: 5, + statusVariant: 1, + status: new Uint8Array([4, 5, 6]), + scriptRoot: undefined, + txScript: undefined, + }, + ], + }) + ); + + const db = getDatabase(dbId); + const tx = await db.transactions.where("id").equals("tx-sync-1").first(); + expect(tx).toBeDefined(); + expect(tx!.blockNum).toBe(5); + expect(tx!.statusVariant).toBe(1); + }); + + it("upserts a transaction record WITH a script when both scriptRoot and txScript are provided", async () => { + const dbId = await openTestDb(); + const scriptRootBytes = new Uint8Array([0xca, 0xfe]); + const txScriptBytes = new Uint8Array([0xba, 0xbe]); + + await applyStateSync( + dbId, + minimalStateUpdate({ + blockNum: 5, + transactionUpdates: [ + { + id: "tx-with-script", + details: new Uint8Array([1]), + blockNum: 5, + statusVariant: 1, + status: new Uint8Array([0]), + scriptRoot: scriptRootBytes, + txScript: txScriptBytes, + }, + ], + }) + ); + + const db = getDatabase(dbId); + const script = await db.transactionScripts + .where("scriptRoot") + .equals(toBase64(scriptRootBytes)) + .first(); + expect(script).toBeDefined(); + expect(script!.txScript).toEqual(txScriptBytes); + }); + + it("does NOT insert a script when txScript is absent (scriptRoot only)", async () => { + const dbId = await openTestDb(); + const scriptRootBytes = new Uint8Array([0x11]); + + await applyStateSync( + dbId, + minimalStateUpdate({ + blockNum: 5, + transactionUpdates: [ + { + id: "tx-script-root-only", + details: new Uint8Array([1]), + blockNum: 5, + statusVariant: 1, + status: new Uint8Array([0]), + scriptRoot: scriptRootBytes, + txScript: undefined, + }, + ], + }) + ); + + const db = getDatabase(dbId); + // Script should not exist since txScript was absent + const scripts = await db.transactionScripts.toArray(); + expect(scripts).toHaveLength(0); + }); + }); + + // ------------------------------------------------------------------------- + // applyStateSync — output notes + // ------------------------------------------------------------------------- + + describe("applyStateSync — output notes", () => { + it("upserts an output note during sync", async () => { + const dbId = await openTestDb(); + + await applyStateSync( + dbId, + minimalStateUpdate({ + blockNum: 5, + serializedOutputNotes: [ + { + noteId: "out-note-1", + noteAssets: new Uint8Array([0x01, 0x02]), + recipientDigest: "recipient-digest-abc", + metadata: new Uint8Array([0x03, 0x04]), + nullifier: undefined, + expectedHeight: 100, + stateDiscriminant: 1, + state: new Uint8Array([0x05]), + }, + ], + }) + ); + + const db = getDatabase(dbId); + const note = await db.outputNotes + .where("noteId") + .equals("out-note-1") + .first(); + expect(note).toBeDefined(); + expect(note!.recipientDigest).toBe("recipient-digest-abc"); + expect(note!.expectedHeight).toBe(100); + expect(note!.stateDiscriminant).toBe(1); + }); + + it("upserts multiple output notes in one sync call", async () => { + const dbId = await openTestDb(); + + await applyStateSync( + dbId, + minimalStateUpdate({ + blockNum: 5, + serializedOutputNotes: [ + { + noteId: "out-a", + noteAssets: new Uint8Array([0x01]), + recipientDigest: "digest-a", + metadata: new Uint8Array([0x02]), + nullifier: "null-a", + expectedHeight: 10, + stateDiscriminant: 2, + state: new Uint8Array([0x03]), + }, + { + noteId: "out-b", + noteAssets: new Uint8Array([0x04]), + recipientDigest: "digest-b", + metadata: new Uint8Array([0x05]), + nullifier: undefined, + expectedHeight: 20, + stateDiscriminant: 3, + state: new Uint8Array([0x06]), + }, + ], + }) + ); + + const db = getDatabase(dbId); + const notes = await db.outputNotes.toArray(); + expect(notes).toHaveLength(2); + }); + }); + + // ------------------------------------------------------------------------- + // applyStateSync — input notes + // ------------------------------------------------------------------------- + + describe("applyStateSync — input notes", () => { + it("upserts an input note during sync", async () => { + const dbId = await openTestDb(); + + await applyStateSync( + dbId, + minimalStateUpdate({ + blockNum: 5, + serializedInputNotes: [ + { + noteId: "in-note-1", + noteAssets: new Uint8Array([0x0a]), + serialNumber: new Uint8Array([0x0b]), + inputs: new Uint8Array([0x0c]), + noteScriptRoot: "script-root-in", + noteScript: new Uint8Array([0x0d]), + nullifier: "nullifier-in-1", + createdAt: "100", + stateDiscriminant: 2, + state: new Uint8Array([0x0e]), + consumedBlockHeight: undefined, + consumedTxOrder: undefined, + consumerAccountId: undefined, + }, + ], + }) + ); + + const db = getDatabase(dbId); + const note = await db.inputNotes + .where("noteId") + .equals("in-note-1") + .first(); + expect(note).toBeDefined(); + expect(note!.nullifier).toBe("nullifier-in-1"); + expect(note!.stateDiscriminant).toBe(2); + }); + }); + + // ------------------------------------------------------------------------- + // applyStateSync — account updates + // ------------------------------------------------------------------------- + + describe("applyStateSync — account updates", () => { + it("applies a full account state during sync", async () => { + const dbId = await openTestDb(); + + await applyStateSync( + dbId, + minimalStateUpdate({ + blockNum: 5, + accountUpdates: [ + { + accountId: "acct-sync-1", + nonce: "1", + storageRoot: "storage-root-1", + storageSlots: [], + storageMapEntries: [], + vaultRoot: "vault-root-1", + assets: [], + codeRoot: "code-root-1", + committed: true, + accountCommitment: "commitment-1", + accountSeed: undefined, + }, + ], + }) + ); + + const db = getDatabase(dbId); + const account = await db.latestAccountHeaders + .where("id") + .equals("acct-sync-1") + .first(); + expect(account).toBeDefined(); + expect(account!.nonce).toBe("1"); + expect(account!.committed).toBe(true); + expect(account!.codeRoot).toBe("code-root-1"); + }); + + it("applies multiple account updates in one sync call", async () => { + const dbId = await openTestDb(); + + await applyStateSync( + dbId, + minimalStateUpdate({ + blockNum: 5, + accountUpdates: [ + { + accountId: "acct-sync-A", + nonce: "1", + storageRoot: "sr-A", + storageSlots: [], + storageMapEntries: [], + vaultRoot: "vr-A", + assets: [], + codeRoot: "cr-A", + committed: true, + accountCommitment: "com-A", + accountSeed: undefined, + }, + { + accountId: "acct-sync-B", + nonce: "2", + storageRoot: "sr-B", + storageSlots: [], + storageMapEntries: [], + vaultRoot: "vr-B", + assets: [], + codeRoot: "cr-B", + committed: false, + accountCommitment: "com-B", + accountSeed: new Uint8Array([0xca, 0xfe]), + }, + ], + }) + ); + + const db = getDatabase(dbId); + const all = await db.latestAccountHeaders.toArray(); + const ids = all.map((a) => a.id); + expect(ids).toContain("acct-sync-A"); + expect(ids).toContain("acct-sync-B"); + }); + }); +}); diff --git a/crates/idxdb-store/src/ts/transactions.test.ts b/crates/idxdb-store/src/ts/transactions.test.ts new file mode 100644 index 0000000..ed130d1 --- /dev/null +++ b/crates/idxdb-store/src/ts/transactions.test.ts @@ -0,0 +1,464 @@ +import { describe, it, expect, afterEach, vi, beforeEach } from "vitest"; +import { openDatabase, getDatabase } from "./schema.js"; +import { + getTransactions, + insertTransactionScript, + upsertTransactionRecord, +} from "./transactions.js"; + +let dbCounter = 0; +function uniqueDbName(): string { + return `test-transactions-${++dbCounter}-${Date.now()}`; +} + +const openDbIds: string[] = []; + +afterEach(async () => { + for (const dbId of openDbIds) { + const db = getDatabase(dbId); + db.dexie.close(); + await db.dexie.delete(); + } + openDbIds.length = 0; +}); + +async function openTestDb(): Promise { + const name = uniqueDbName(); + await openDatabase(name, "0.1.0"); + openDbIds.push(name); + return name; +} + +// Helper: uint8Array -> base64 (mirrors the source util) +function toBase64(bytes: Uint8Array): string { + const binary = bytes.reduce((acc, b) => acc + String.fromCharCode(b), ""); + return btoa(binary); +} + +describe("transactions", () => { + let errorSpy: any; + let logSpy: any; + + beforeEach(() => { + errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + }); + + afterEach(() => { + errorSpy.mockRestore(); + logSpy.mockRestore(); + }); + + // ------------------------------------------------------------------------- + // upsertTransactionRecord / getTransactions — basic round-trip + // ------------------------------------------------------------------------- + + it("upserts a transaction and retrieves it with the 'all' filter", async () => { + const dbId = await openTestDb(); + const details = new Uint8Array([1, 2, 3]); + const status = new Uint8Array([4, 5, 6]); + + await upsertTransactionRecord(dbId, "tx-1", details, 10, 1, status); + + const results = await getTransactions(dbId, "All"); + expect(results).toHaveLength(1); + const tx = results![0]; + expect(tx.id).toBe("tx-1"); + expect(tx.blockNum).toBe(10); + expect(tx.statusVariant).toBe(1); + expect(tx.details).toBe(toBase64(details)); + expect(tx.status).toBe(toBase64(status)); + expect(tx.scriptRoot).toBeUndefined(); + expect(tx.txScript).toBeUndefined(); + }); + + it("upserts a transaction with a scriptRoot and retrieves it with txScript", async () => { + const dbId = await openTestDb(); + const details = new Uint8Array([10]); + const status = new Uint8Array([20]); + const scriptRootBytes = new Uint8Array([0xaa, 0xbb]); + const txScriptBytes = new Uint8Array([0xcc, 0xdd]); + + // Insert the script first + await insertTransactionScript(dbId, scriptRootBytes, txScriptBytes); + + // Insert transaction referencing that script root + await upsertTransactionRecord( + dbId, + "tx-with-script", + details, + 5, + 1, + status, + scriptRootBytes + ); + + const results = await getTransactions(dbId, "All"); + expect(results).toHaveLength(1); + const tx = results![0]; + expect(tx.id).toBe("tx-with-script"); + expect(tx.scriptRoot).toBe(toBase64(scriptRootBytes)); + expect(tx.txScript).toBe(toBase64(txScriptBytes)); + }); + + it("upserts a transaction with scriptRoot but no matching script (txScript undefined)", async () => { + const dbId = await openTestDb(); + const details = new Uint8Array([1]); + const status = new Uint8Array([2]); + const scriptRootBytes = new Uint8Array([0x01, 0x02]); + + // Do NOT insert a script — scriptRoot points to nothing + await upsertTransactionRecord( + dbId, + "tx-no-script", + details, + 7, + 0, + status, + scriptRootBytes + ); + + const results = await getTransactions(dbId, "All"); + expect(results).toHaveLength(1); + const tx = results![0]; + expect(tx.txScript).toBeUndefined(); + expect(tx.scriptRoot).toBe(toBase64(scriptRootBytes)); + }); + + it("upsert replaces existing record with same id", async () => { + const dbId = await openTestDb(); + const details1 = new Uint8Array([1]); + const details2 = new Uint8Array([99]); + const status = new Uint8Array([0]); + + await upsertTransactionRecord(dbId, "tx-upsert", details1, 1, 0, status); + await upsertTransactionRecord(dbId, "tx-upsert", details2, 2, 1, status); + + const results = await getTransactions(dbId, "All"); + expect(results).toHaveLength(1); + expect(results![0].blockNum).toBe(2); + expect(results![0].details).toBe(toBase64(details2)); + }); + + // ------------------------------------------------------------------------- + // getTransactions — empty result path + // ------------------------------------------------------------------------- + + it("returns empty array when no transactions exist (All filter)", async () => { + const dbId = await openTestDb(); + const results = await getTransactions(dbId, "All"); + expect(results).toEqual([]); + }); + + // ------------------------------------------------------------------------- + // getTransactions — 'Uncommitted' filter (statusVariant === 0) + // ------------------------------------------------------------------------- + + it("Uncommitted filter returns only pending transactions (statusVariant 0)", async () => { + const dbId = await openTestDb(); + const status = new Uint8Array([0]); + + // pending + await upsertTransactionRecord( + dbId, + "tx-pending", + new Uint8Array([1]), + 1, + 0 /* STATUS_PENDING_VARIANT */, + status + ); + // committed + await upsertTransactionRecord( + dbId, + "tx-committed", + new Uint8Array([2]), + 2, + 1 /* STATUS_COMMITTED_VARIANT */, + status + ); + // discarded + await upsertTransactionRecord( + dbId, + "tx-discarded", + new Uint8Array([3]), + 3, + 2 /* STATUS_DISCARDED_VARIANT */, + status + ); + + const results = await getTransactions(dbId, "Uncommitted"); + expect(results).toHaveLength(1); + expect(results![0].id).toBe("tx-pending"); + }); + + it("Uncommitted filter returns empty array when no pending transactions exist", async () => { + const dbId = await openTestDb(); + await upsertTransactionRecord( + dbId, + "tx-committed", + new Uint8Array([1]), + 1, + 1, + new Uint8Array([0]) + ); + + const results = await getTransactions(dbId, "Uncommitted"); + expect(results).toEqual([]); + }); + + // ------------------------------------------------------------------------- + // getTransactions — 'Ids:' filter + // ------------------------------------------------------------------------- + + it("Ids filter returns transactions matching provided ids", async () => { + const dbId = await openTestDb(); + const status = new Uint8Array([0]); + + await upsertTransactionRecord( + dbId, + "tx-a", + new Uint8Array([1]), + 1, + 1, + status + ); + await upsertTransactionRecord( + dbId, + "tx-b", + new Uint8Array([2]), + 2, + 1, + status + ); + await upsertTransactionRecord( + dbId, + "tx-c", + new Uint8Array([3]), + 3, + 1, + status + ); + + const results = await getTransactions(dbId, "Ids:tx-a,tx-c"); + expect(results).toHaveLength(2); + const ids = results!.map((r) => r.id); + expect(ids).toEqual(expect.arrayContaining(["tx-a", "tx-c"])); + expect(ids).not.toContain("tx-b"); + }); + + it("Ids filter with a single id returns that transaction", async () => { + const dbId = await openTestDb(); + await upsertTransactionRecord( + dbId, + "tx-single", + new Uint8Array([9]), + 5, + 1, + new Uint8Array([1]) + ); + + const results = await getTransactions(dbId, "Ids:tx-single"); + expect(results).toHaveLength(1); + expect(results![0].id).toBe("tx-single"); + }); + + it("Ids filter returns empty array when none of the ids exist", async () => { + const dbId = await openTestDb(); + const results = await getTransactions( + dbId, + "Ids:nonexistent-1,nonexistent-2" + ); + expect(results).toEqual([]); + }); + + // ------------------------------------------------------------------------- + // getTransactions — 'ExpiredPending:' filter + // ------------------------------------------------------------------------- + + it("ExpiredPending filter returns pending txs with blockNum below threshold", async () => { + const dbId = await openTestDb(); + const status = new Uint8Array([0]); + + // pending, blockNum 5 — should match ExpiredPending:10 + await upsertTransactionRecord( + dbId, + "tx-expired-pending", + new Uint8Array([1]), + 5, + 0 /* pending */, + status + ); + // pending, blockNum 15 — above threshold, should NOT match + await upsertTransactionRecord( + dbId, + "tx-fresh-pending", + new Uint8Array([2]), + 15, + 0 /* pending */, + status + ); + // committed, blockNum 5 — committed, should NOT match + await upsertTransactionRecord( + dbId, + "tx-committed", + new Uint8Array([3]), + 5, + 1 /* committed */, + status + ); + // discarded, blockNum 5 — discarded, should NOT match + await upsertTransactionRecord( + dbId, + "tx-discarded", + new Uint8Array([4]), + 5, + 2 /* discarded */, + status + ); + + const results = await getTransactions(dbId, "ExpiredPending:10"); + expect(results).toHaveLength(1); + expect(results![0].id).toBe("tx-expired-pending"); + }); + + it("ExpiredPending filter returns empty array when no transactions match", async () => { + const dbId = await openTestDb(); + // Only committed transactions, none pending + await upsertTransactionRecord( + dbId, + "tx-committed", + new Uint8Array([1]), + 5, + 1, + new Uint8Array([0]) + ); + + const results = await getTransactions(dbId, "ExpiredPending:100"); + expect(results).toEqual([]); + }); + + it("ExpiredPending filter boundary: blockNum equal to threshold is excluded", async () => { + const dbId = await openTestDb(); + await upsertTransactionRecord( + dbId, + "tx-boundary", + new Uint8Array([1]), + 10 /* blockNum == threshold */, + 0, + new Uint8Array([0]) + ); + + // filter is strict < blockNum, so blockNum === 10 with threshold 10 is excluded + const results = await getTransactions(dbId, "ExpiredPending:10"); + expect(results).toEqual([]); + }); + + // ------------------------------------------------------------------------- + // insertTransactionScript — round-trip without a transaction context + // ------------------------------------------------------------------------- + + it("insertTransactionScript stores a script retrievable via transactionScripts table", async () => { + const dbId = await openTestDb(); + const scriptRootBytes = new Uint8Array([0x01, 0x02, 0x03]); + const txScriptBytes = new Uint8Array([0xde, 0xad, 0xbe, 0xef]); + + await insertTransactionScript(dbId, scriptRootBytes, txScriptBytes); + + const db = getDatabase(dbId); + const stored = await db.transactionScripts + .where("scriptRoot") + .equals(toBase64(scriptRootBytes)) + .first(); + + expect(stored).toBeDefined(); + expect(stored!.scriptRoot).toBe(toBase64(scriptRootBytes)); + expect(stored!.txScript).toEqual(txScriptBytes); + }); + + it("insertTransactionScript upserts on duplicate scriptRoot", async () => { + const dbId = await openTestDb(); + const scriptRootBytes = new Uint8Array([0xaa]); + const script1 = new Uint8Array([0x01]); + const script2 = new Uint8Array([0x02]); + + await insertTransactionScript(dbId, scriptRootBytes, script1); + await insertTransactionScript(dbId, scriptRootBytes, script2); + + const db = getDatabase(dbId); + const all = await db.transactionScripts.toArray(); + expect(all).toHaveLength(1); + expect(all[0].txScript).toEqual(script2); + }); + + // ------------------------------------------------------------------------- + // Error paths — "never-opened" dbId + // ------------------------------------------------------------------------- + + it("getTransactions throws when db is not opened", async () => { + await expect(getTransactions("never-opened", "All")).rejects.toThrow(); + }); + + it("upsertTransactionRecord throws when db is not opened", async () => { + await expect( + upsertTransactionRecord( + "never-opened", + "tx-err", + new Uint8Array([1]), + 0, + 0, + new Uint8Array([0]) + ) + ).rejects.toThrow(); + }); + + it("insertTransactionScript throws when db is not opened", async () => { + await expect( + insertTransactionScript( + "never-opened", + new Uint8Array([1]), + new Uint8Array([2]) + ) + ).rejects.toThrow(); + }); + + // ------------------------------------------------------------------------- + // Multiple transactions — verify all are returned by All filter + // ------------------------------------------------------------------------- + + it("returns all transactions when multiple are inserted", async () => { + const dbId = await openTestDb(); + const status = new Uint8Array([0]); + + await upsertTransactionRecord( + dbId, + "multi-1", + new Uint8Array([1]), + 1, + 0, + status + ); + await upsertTransactionRecord( + dbId, + "multi-2", + new Uint8Array([2]), + 2, + 1, + status + ); + await upsertTransactionRecord( + dbId, + "multi-3", + new Uint8Array([3]), + 3, + 2, + status + ); + + const results = await getTransactions(dbId, "All"); + expect(results).toHaveLength(3); + const ids = results!.map((r) => r.id); + expect(ids).toEqual( + expect.arrayContaining(["multi-1", "multi-2", "multi-3"]) + ); + }); +}); diff --git a/crates/idxdb-store/src/ts/utils.test.ts b/crates/idxdb-store/src/ts/utils.test.ts new file mode 100644 index 0000000..ac9c322 --- /dev/null +++ b/crates/idxdb-store/src/ts/utils.test.ts @@ -0,0 +1,105 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import Dexie from "dexie"; +import { mapOption, logWebStoreError, uint8ArrayToBase64 } from "./utils.js"; + +describe("mapOption", () => { + it("applies the function when value is defined", () => { + expect(mapOption(5, (n) => n * 2)).toBe(10); + }); + + it("returns undefined when value is null", () => { + expect(mapOption(null, (n) => n * 2)).toBeUndefined(); + }); + + it("returns undefined when value is undefined", () => { + expect(mapOption(undefined, (n) => n * 2)).toBeUndefined(); + }); + + it("treats 0 and empty string as defined", () => { + expect(mapOption(0, (n) => n + 1)).toBe(1); + expect(mapOption("", (s) => s.length)).toBe(0); + }); +}); + +describe("uint8ArrayToBase64", () => { + it("encodes bytes correctly", () => { + expect(uint8ArrayToBase64(new Uint8Array([1, 2, 3]))).toBe("AQID"); + }); + + it("encodes an empty array to an empty string", () => { + expect(uint8ArrayToBase64(new Uint8Array([]))).toBe(""); + }); +}); + +describe("logWebStoreError", () => { + let errorSpy: any; + let traceSpy: any; + + beforeEach(() => { + errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + traceSpy = vi.spyOn(console, "trace").mockImplementation(() => {}); + }); + + afterEach(() => { + errorSpy.mockRestore(); + traceSpy.mockRestore(); + }); + + it("logs and rethrows a Dexie error with context", () => { + const err = new Dexie.DexieError("OpenError", "DB closed"); + expect(() => logWebStoreError(err, "ctx")).toThrow(); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining("ctx: Indexdb error") + ); + }); + + it("logs a Dexie error without context", () => { + const err = new Dexie.DexieError("OpenError", "DB closed"); + expect(() => logWebStoreError(err)).toThrow(); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringMatching(/^Indexdb error:/) + ); + }); + + it("logs a Dexie error's stack when present", () => { + const err = new Dexie.DexieError("OpenError", "DB closed"); + (err as any).stack = "stack-line"; + expect(() => logWebStoreError(err)).toThrow(); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining("Stacktrace") + ); + }); + + it("recurses into Dexie inner exception", () => { + const inner = new Error("inner-cause"); + const err = new Dexie.DexieError("OpenError", "outer"); + (err as any).inner = inner; + expect(() => logWebStoreError(err)).toThrow(); + expect(errorSpy.mock.calls.length).toBeGreaterThan(1); + }); + + it("logs a plain Error with stack", () => { + const err = new Error("boom"); + expect(() => logWebStoreError(err)).toThrow(); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining("Unexpected error") + ); + }); + + it("logs a plain Error without stack", () => { + const err = new Error("boom"); + err.stack = undefined; + expect(() => logWebStoreError(err)).toThrow(); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining("Unexpected error") + ); + }); + + it("logs and rethrows a non-Error value", () => { + expect(() => logWebStoreError({ thrown: "thing" })).toThrow(); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining("non-error value") + ); + expect(traceSpy).toHaveBeenCalled(); + }); +}); diff --git a/crates/idxdb-store/src/vitest.config.ts b/crates/idxdb-store/src/vitest.config.ts index adbb67d..7cbe249 100644 --- a/crates/idxdb-store/src/vitest.config.ts +++ b/crates/idxdb-store/src/vitest.config.ts @@ -4,5 +4,12 @@ export default defineConfig({ test: { environment: "node", setupFiles: ["fake-indexeddb/auto"], + coverage: { + provider: "v8", + reporter: ["text", "json", "json-summary", "html", "lcov"], + include: ["ts/**/*.ts"], + exclude: ["ts/**/*.test.ts", "ts/test-utils.ts"], + thresholds: { lines: 95, branches: 95, functions: 95, statements: 95 }, + }, }, }); diff --git a/crates/web-client/.attw.json b/crates/web-client/.attw.json new file mode 100644 index 0000000..daeca4e --- /dev/null +++ b/crates/web-client/.attw.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/arethetypeswrong/arethetypeswrong.github.io/main/docs/configuration.md", + "profile": "esm-only" +} diff --git a/crates/web-client/.mocharc.json b/crates/web-client/.mocharc.json deleted file mode 100644 index 15f2118..0000000 --- a/crates/web-client/.mocharc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "require": ["ts-node/register", "esm"], - "extension": ["ts"], - "spec": "test/**/*.test.ts", - "timeout": 600000 -} diff --git a/crates/web-client/js/eager.js b/crates/web-client/js/eager.js new file mode 100644 index 0000000..a1047db --- /dev/null +++ b/crates/web-client/js/eager.js @@ -0,0 +1,33 @@ +// Eager entry point for @miden-sdk/miden-sdk (browser builds). +// +// Awaits WASM initialization at module top level, so importing this module +// guarantees that any wasm-bindgen constructor (`new RpcClient(...)`, +// `AccountId.fromHex(...)`, `TransactionProver.newRemoteProver(...)`, etc.) +// is safe to call synchronously on the next line. No explicit +// `await MidenClient.ready()` / `isReady` gate is required. +// +// This is the default entry for browser bundlers (`@miden-sdk/miden-sdk` +// → `./dist/eager.js`). Node.js consumers resolve the `node` exports +// condition instead and get the napi binding via `./js/node-index.js`, +// bypassing this file entirely. +// +// When NOT to use this entry: +// - **Capacitor mobile apps** (Miden Wallet iOS/Android): Capacitor's +// `capacitor://localhost` scheme handler interacts poorly with top-level +// await in the main WKWebView. Verified empirically: TLA in a Capacitor +// host WKWebView hangs module evaluation indefinitely, while the same +// TLA in the dApp-browser WKWebView (vanilla HTTPS) resolves in <100ms. +// - **Next.js / SSR**: TLA blocks server-side module evaluation. +// - **Framework adapters (@miden-sdk/react, etc.)**: they manage readiness +// via their own state machine (e.g. `isReady`) and should not impose +// TLA on consumer bundles. +// +// For those contexts, import from `@miden-sdk/miden-sdk/lazy` — identical +// API surface, no top-level await, callers are responsible for awaiting +// `MidenClient.ready()` (or the equivalent) before touching wasm-bindgen +// types. +import { getWasmOrThrow } from "./index.js"; + +await getWasmOrThrow(); + +export * from "./index.js"; diff --git a/crates/web-client/js/node/napi-compat.js b/crates/web-client/js/node/napi-compat.js index 8c74661..29017b0 100644 --- a/crates/web-client/js/node/napi-compat.js +++ b/crates/web-client/js/node/napi-compat.js @@ -33,7 +33,7 @@ export function normalizeArg(val) { /** * Wraps a napi class so constructor and static method args are normalized. */ -export function wrapClass(Cls) { +function wrapClass(Cls) { if (!Cls) return Cls; const Wrapper = function (...args) { return new Cls(...args.map(normalizeArg)); @@ -126,7 +126,7 @@ export function wrapClient(rawClient, storeName) { * - Converts null -> undefined for Option returns * - Aliases static methods */ -export function patchSdkPrototypes(rawSdk) { +function patchSdkPrototypes(rawSdk) { // snake_case aliases for instance methods /* eslint-disable camelcase */ for (const [cls, aliases] of [ @@ -176,7 +176,7 @@ export function patchSdkPrototypes(rawSdk) { * typed wrappers (NoteAndArgsArray, FeltArray, etc.). These polyfills * let `new sdk.FeltArray([a, b])` work on Node.js by returning a plain array. */ -export function makeArrayPolyfills() { +function makeArrayPolyfills() { function polyfill(items) { const arr = items === undefined || items === null diff --git a/crates/web-client/lazy/package.json b/crates/web-client/lazy/package.json new file mode 100644 index 0000000..45739eb --- /dev/null +++ b/crates/web-client/lazy/package.json @@ -0,0 +1,4 @@ +{ + "main": "../dist/index.js", + "types": "../dist/index.d.ts" +} diff --git a/crates/web-client/package.json b/crates/web-client/package.json index b96ac79..b0c783e 100644 --- a/crates/web-client/package.json +++ b/crates/web-client/package.json @@ -6,18 +6,31 @@ "Miden" ], "type": "module", - "main": "./dist/index.js", - "browser": "./dist/index.js", + "main": "./dist/eager.js", + "browser": "./dist/eager.js", "types": "./dist/index.d.ts", "exports": { ".": { - "browser": "./dist/index.js", - "node": "./js/node-index.js", - "default": "./dist/index.js" - } + "node": { + "types": "./dist/index.d.ts", + "default": "./js/node-index.js" + }, + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/eager.js" + } + }, + "./lazy": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "./package.json": "./package.json" }, "files": [ "dist", + "lazy", "js/node-index.js", "js/node", "js/client.js", @@ -28,7 +41,7 @@ ], "scripts": { "build-rust-client-js": "pnpm --filter web_store run build", - "build": "rimraf dist && pnpm run build-rust-client-js && cross-env RUSTFLAGS=\"--cfg getrandom_backend=\\\"wasm_js\\\"\" rollup -c rollup.config.js && cpr js/types dist && node clean.js", + "build": "rimraf dist && pnpm run build-rust-client-js && cross-env RUSTFLAGS=\"--cfg getrandom_backend=\\\"wasm_js\\\"\" rollup -c rollup.config.js && cpr js/types dist && node clean.js && node ./scripts/post-build.js", "build-dev": "pnpm install && MIDEN_WEB_DEV=true pnpm run build", "check:wasm-types": "node ./scripts/check-bindgen-types.js", "check:method-classification": "node ./scripts/check-method-classification.js", @@ -54,23 +67,16 @@ "@types/node": "^24.9.2", "@wasm-tool/rollup-plugin-rust": "^3.0.3", "binaryen": "^129.0.0", - "chai": "^5.1.1", "cpr": "^3.0.1", "cross-env": "^7.0.3", - "esm": "^3.2.25", - "http-server": "^14.1.1", - "mocha": "^10.7.3", - "puppeteer": "^23.1.0", "rimraf": "^6.0.1", "rollup": "^4.59.0", "rollup-plugin-copy": "^3.5.0", - "ts-node": "^10.9.2", "typedoc": "^0.28.1", "typedoc-plugin-markdown": "^4.8.1", "typescript": "^5.5.4" }, "dependencies": { - "@rollup/plugin-typescript": "^12.3.0", "dexie": "^4.0.1", "glob": "^11.0.0" } diff --git a/crates/web-client/rollup.config.js b/crates/web-client/rollup.config.js index 6f913e6..f38f06b 100644 --- a/crates/web-client/rollup.config.js +++ b/crates/web-client/rollup.config.js @@ -98,7 +98,7 @@ const baseCargoArgs = [ */ export default [ { - input: ["./js/wasm.js", "./js/index.js"], + input: ["./js/wasm.js", "./js/index.js", "./js/eager.js"], output: { dir: `dist`, format: "es", diff --git a/crates/web-client/scripts/post-build.js b/crates/web-client/scripts/post-build.js new file mode 100644 index 0000000..71cc718 --- /dev/null +++ b/crates/web-client/scripts/post-build.js @@ -0,0 +1,66 @@ +#!/usr/bin/env node +// Post-build step that prepares dist/ for `attw` / `publint` compliance. +// +// Rewrites extensionless relative imports in dist/*.d.ts to use explicit +// `.js` extensions. TypeScript's Node16/NodeNext module resolution +// requires explicit extensions on relative specifiers; without this, +// attw reports `InternalResolutionError` for the published types. +// +// The `lazy/package.json` node10 fallback shim is checked into the repo +// at `crates/web-client/lazy/package.json` rather than emitted here; this +// keeps the published artifact set fully visible in source control and +// avoids a script-generated file outside `dist/`. +// +// This file only touches generated output in `dist/`. It does not modify +// any source under `src/` or `js/types/`. + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const distDir = path.resolve(__dirname, "..", "dist"); + +function rewriteDtsImports(dir) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + rewriteDtsImports(full); + continue; + } + if (!entry.name.endsWith(".d.ts")) continue; + + const original = fs.readFileSync(full, "utf8"); + // Match relative specifiers in two forms used by the hand-authored + // declaration files in `js/types/`: + // 1. `from "./foo"` / `from "../foo"` (static import/re-export) + // 2. `import("./foo")` (dynamic import in type position) + // For each, append `.js` so Node16 type resolution finds the sibling + // `.d.ts` (TS resolves `./foo.js` -> `./foo.d.ts` automatically). + // + // Does not handle bare side-effect imports (import "./foo") — none + // currently exist in dist/**.d.ts. + const rewriteSpec = (match, prefix, spec, suffix) => { + if (/\.[a-zA-Z0-9]+$/.test(spec)) return match; // already has extension + if (spec.endsWith("/")) return match; // directory specifier, leave alone + return `${prefix}${spec}.js${suffix}`; + }; + const updated = original + .replace(/(from\s+["'])(\.\.?\/[^"']+?)(["'])/g, rewriteSpec) + .replace(/(import\s*\(\s*["'])(\.\.?\/[^"']+?)(["']\s*\))/g, rewriteSpec); + + if (updated !== original) { + fs.writeFileSync(full, updated); + console.log( + `[post-build] Rewrote relative imports in ${path.relative(distDir, full)}` + ); + } + } +} + +if (!fs.existsSync(distDir)) { + console.error(`[post-build] dist directory not found at ${distDir}`); + process.exit(1); +} + +rewriteDtsImports(distDir); diff --git a/crates/web-client/test/global.test.d.ts b/crates/web-client/test/global.test.d.ts index a5d6559..0b84b0e 100644 --- a/crates/web-client/test/global.test.d.ts +++ b/crates/web-client/test/global.test.d.ts @@ -1,4 +1,4 @@ -import { Page } from "puppeteer"; +import { Page } from "@playwright/test"; import { WebClient as WasmWebClient } from "../dist/crates/miden_client_web"; import { Account, diff --git a/crates/web-client/test/node-adapter.ts b/crates/web-client/test/node-adapter.ts index 64e9c9f..037efd7 100644 --- a/crates/web-client/test/node-adapter.ts +++ b/crates/web-client/test/node-adapter.ts @@ -116,7 +116,7 @@ function initSdk(): any { return rawSdk; } -export const sdk = new Proxy( +const sdk = new Proxy( {}, { get(_target, prop) { @@ -323,7 +323,7 @@ function tmpTestDir(): string { /** * Matches the browser's `window.MockWasmWebClient` interface. */ -export const MockWasmWebClient = { +const MockWasmWebClient = { createClient: async ( seed?: any, serializedMockChain?: any, diff --git a/crates/web-client/test/sync_lock.test.ts b/crates/web-client/test/sync_lock.test.ts index 7a067b4..a837617 100644 --- a/crates/web-client/test/sync_lock.test.ts +++ b/crates/web-client/test/sync_lock.test.ts @@ -600,19 +600,10 @@ test.describe("Sync Lock Timeout Race Condition", () => { // This test verifies that waiters (coalesced callers) are properly // rejected when the sync they're waiting on times out const result = await page.evaluate(async () => { - // Access the sync lock functions directly from the idxdb-store module - const { acquireSyncLock, releaseSyncLock, releaseSyncLockWithError } = - await import("@aspect-build/aspect-rsdoctor/index.js").catch(() => { - // Fallback: the functions may not be directly exported - // In this case, we test via the client API - return { - acquireSyncLock: null, - releaseSyncLock: null, - releaseSyncLockWithError: null, - }; - }); - - // If we can't access the low-level functions, test via client API + // Test via the client API: an in-flight sync that holds the lock + // plus two coalesced waiters. If the lock implementation regresses + // (e.g. waiters aren't rejected on timeout), Promise.all rejects + // and the assertions below fail. const client = window.client; // Start a sync that will hold the lock diff --git a/crates/web-client/test/test-helpers.ts b/crates/web-client/test/test-helpers.ts index 6d13db6..e0a9fb8 100644 --- a/crates/web-client/test/test-helpers.ts +++ b/crates/web-client/test/test-helpers.ts @@ -655,83 +655,3 @@ export async function createIntegrationClient(): Promise<{ return null; } } - -/** - * Mints tokens using integration flow (executeAndApplyTransaction + waitForTransaction). - * Requires a running node. - */ -export async function integrationMint( - client: any, - sdk: any, - targetId: any, - faucetId: any, - opts?: { amount?: number; publicNote?: boolean; sync?: boolean } -): Promise<{ - transactionId: string; - createdNoteId: string; - numOutputNotesCreated: number; -}> { - const amount = opts?.amount ?? 1000; - const noteType = opts?.publicNote - ? sdk.NoteType.Public - : sdk.NoteType.Private; - const shouldSync = opts?.sync !== false; - - await client.syncState(); - - const mintRequest = await client.newMintTransactionRequest( - targetId, - faucetId, - noteType, - sdk.u64(amount) - ); - const result = await executeAndApplyTransaction( - client, - sdk, - faucetId, - mintRequest - ); - - const transactionId = result.executedTransaction().id().toHex(); - const createdNoteId = result.createdNotes().notes()[0].id().toString(); - const numOutputNotesCreated = result.createdNotes().numNotes(); - - if (shouldSync) { - await waitForTransaction(client, sdk, transactionId); - } - - return { transactionId, createdNoteId, numOutputNotesCreated }; -} - -/** - * Consumes a note using integration flow. - */ -export async function integrationConsume( - client: any, - sdk: any, - accountId: any, - faucetId: any, - noteId: string -): Promise<{ transactionId: string; targetAccountBalance: string }> { - await client.syncState(); - - const inputNoteRecord = await client.getInputNote(noteId); - if (!inputNoteRecord) throw new Error(`Note ${noteId} not found`); - - const note = inputNoteRecord.toNote(); - const consumeRequest = client.newConsumeTransactionRequest([note]); - const result = await executeAndApplyTransaction( - client, - sdk, - accountId, - consumeRequest - ); - - const transactionId = result.executedTransaction().id().toHex(); - await waitForTransaction(client, sdk, transactionId); - - const account = await client.getAccount(accountId); - const balance = account.vault().getBalance(faucetId).toString(); - - return { transactionId, targetAccountBalance: balance }; -} diff --git a/crates/web-client/test/test-setup.ts b/crates/web-client/test/test-setup.ts index fec621c..f61713d 100644 --- a/crates/web-client/test/test-setup.ts +++ b/crates/web-client/test/test-setup.ts @@ -81,7 +81,7 @@ export function loadNodeSdk(): any { let _nodeTestCounter = 0; -export async function createNodeMockClient(): Promise<{ +async function createNodeMockClient(): Promise<{ client: any; sdk: any; }> { @@ -333,7 +333,7 @@ function patchNapiPrototypes(rawSdk: any) { } } -export function createNodeSdkWrapper(rawSdk: any): any { +function createNodeSdkWrapper(rawSdk: any): any { patchNapiPrototypes(rawSdk); // Expose the StorageView JS wrapper on `sdk.*` so tests can reach it via the // same namespace on both platforms (browser exposes it on `window.*`). diff --git a/crates/web-client/test/webClientTestUtils.ts b/crates/web-client/test/webClientTestUtils.ts index a171e6e..47b1a8e 100644 --- a/crates/web-client/test/webClientTestUtils.ts +++ b/crates/web-client/test/webClientTestUtils.ts @@ -1,5 +1,5 @@ // @ts-nocheck -import { expect } from "chai"; +import { expect } from "@playwright/test"; import { TransactionProver } from "../dist"; import test from "./playwright.global.setup"; import { Page } from "@playwright/test"; @@ -942,7 +942,7 @@ export const clearStore = async (page: Page) => { // Misc test utils export const isValidAddress = (address: string) => { - expect(address.startsWith("0x")).to.be.true; + expect(address.startsWith("0x")).toBe(true); }; // Constants diff --git a/crates/web-client/tsconfig.json b/crates/web-client/tsconfig.json index a9c6c4b..263dabf 100644 --- a/crates/web-client/tsconfig.json +++ b/crates/web-client/tsconfig.json @@ -22,10 +22,5 @@ "tsconfig.json", "./test/playwright.global.setup.ts" ], - "exclude": ["node_modules", "js"], - "ts-node": { - "esm": true, - "experimentalSpecifierResolution": "node", - "transpileOnly": true - } + "exclude": ["node_modules", "js"] } diff --git a/eslint.config.js b/eslint.config.js index f483ca7..3268e2c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,3 +1,5 @@ +const prettierConfig = require("eslint-config-prettier"); + module.exports = [ { // Ignore patterns @@ -10,6 +12,7 @@ module.exports = [ "crates/idxdb-store/src/**", "packages/react-sdk/**", "packages/vite-plugin/**", + "vitest.config.ts", ], }, { @@ -23,34 +26,6 @@ module.exports = [ }, rules: { camelcase: ["error", { properties: "always" }], - semi: ["error", "always"], - "keyword-spacing": [ - "error", - { - before: true, - after: true, - }, - ], - "comma-dangle": [ - "error", - { - arrays: "always-multiline", - objects: "always-multiline", - imports: "always-multiline", - exports: "always-multiline", - functions: "never", - }, - ], - "eol-last": ["error", "always"], - "space-before-blocks": ["error", "always"], - "no-multiple-empty-lines": [ - "error", - { - max: 1, - maxBOF: 0, - maxEOF: 0, - }, - ], }, }, { @@ -66,34 +41,8 @@ module.exports = [ }, rules: { camelcase: ["error", { properties: "always" }], - semi: ["error", "always"], - "keyword-spacing": [ - "error", - { - before: true, - after: true, - }, - ], - "comma-dangle": [ - "error", - { - arrays: "always-multiline", - objects: "always-multiline", - imports: "always-multiline", - exports: "always-multiline", - functions: "never", - }, - ], - "eol-last": ["error", "always"], - "space-before-blocks": ["error", "always"], - "no-multiple-empty-lines": [ - "error", - { - max: 1, - maxBOF: 0, - maxEOF: 0, - }, - ], }, }, + // Must be last: disables any stylistic rules that conflict with Prettier. + prettierConfig, ]; diff --git a/knip.jsonc b/knip.jsonc new file mode 100644 index 0000000..d2246b2 --- /dev/null +++ b/knip.jsonc @@ -0,0 +1,131 @@ +// Knip — unused-exports / unused-deps / unused-files detector. +// +// Strict mode: package.json#scripts.check:knip runs `knip` with no +// `--no-exit-code` flag, so any finding fails CI. The allowlists below +// each carry a comment naming the consumer that knip cannot statically +// see (browser-resolved imports, makefile targets, GitHub workflows, +// post-build copy, etc.) — touch them only if that consumer changes. +// +// Workspace layout: pnpm 9 monorepo with three published TS packages +// plus a small TS-bundled crate. `entry` patterns are reserved for files +// knip can't infer from a plugin (e.g. rollup configs, tsup configs, +// vitest configs, eslint configs are all picked up automatically). +{ + "$schema": "https://unpkg.com/knip@6/schema.json", + // Top-level binaries that show up in CI workflows / Makefiles but + // aren't pulled in by any source file. + "ignoreBinaries": [ + // Used in .github/workflows/wallet-pages.yml via `pnpm exec vite + // build` from the wallet example workspace (which has its own + // package.json + lockfile, hidden from this monorepo's graph). + "vite", + ], + "ignoreDependencies": [ + // `dexie` is bundled into the web-client test page via rollup at + // test runtime — `page.evaluate(...)` blocks load the rolled-up + // bundle from http://localhost:8080, which transitively imports + // dexie. Knip's static scan can't see across the build boundary. + "dexie", + // `publint` and `@arethetypeswrong/cli` are invoked by the + // `check:publint` / `check:attw` scripts in root package.json via + // `pnpm exec publint` / `pnpm exec attw`. Knip flags them as unused + // because it only follows ESM/CJS imports in source, not script + // strings. + "publint", + "@arethetypeswrong/cli", + ], + "rules": { + // Three intentional dual-export patterns in this repo: + // - packages/vite-plugin/src/index.ts: `export function midenVitePlugin` + // + `export default midenVitePlugin` — both forms are documented + // public API; consumers use either named or default import. + // - crates/web-client/test/playwright.global.setup.ts: `export const + // test` + `export default test` — fixture re-exports used by both + // `import test from ...` and `import { test as base } from ...` + // patterns across ~50 test files. + // - packages/react-sdk/test/test-app/react-jsx-runtime.js: `jsx`, + // `jsxs`, `jsxDEV` are all aliased to the same function because + // the React JSX transform looks for whichever name matches the + // classic/automatic runtime. + "duplicates": "off", + }, + "workspaces": { + ".": { + "entry": ["scripts/**/*.{js,ts}"], + }, + "packages/react-sdk": { + "entry": [ + "src/__tests__/**/*.test.{ts,tsx}", + "test/**/*.test.ts", + "test/serve-tests.cjs", + "test/test-app/**/*.{js,html}", + ], + }, + "packages/vite-plugin": { + "entry": ["src/__tests__/**/*.test.ts"], + }, + "crates/web-client": { + // Hand-rolled JS in `js/` is the published surface (bundled via + // rollup). Files not picked up by the rollup plugin entry trace + // (worker bundles, JS unit tests, helper modules, Playwright TS + // tests, build scripts) are listed explicitly here. + "entry": [ + "js/standalone.js", + "js/client.js", + "js/asyncLock.js", + "js/syncLock.js", + "js/webLock.js", + "js/utils.js", + "js/constants.js", + "js/storageView.js", + // js/node-index.js is auto-detected from the package.json `node` + // exports condition; only the rest of js/node/ needs to be listed + // explicitly here (helpers loaded via dynamic require / wrapper + // factories that knip can't statically follow). + "js/node/**/*.js", + "js/workers/**/*.js", + // The .d.ts files in js/types/ ship as the package's published + // type surface — `pnpm run build` runs `cpr js/types dist` after + // rollup, so dist/index.d.ts (the package's `types` field) is a + // copy of js/types/index.d.ts. Knip can't see the post-build + // copy step, so we register the source files as entry points. + "js/types/index.d.ts", + "js/types/api-types.d.ts", + "js/types/docs-entry.d.ts", + "test/**/*.test.ts", + "test/webClientTestUtils.ts", + "test/playwright.global.setup.ts", + "scripts/**/*.js", + ], + // The `./index.js` strings inside `page.evaluate(...)` blocks are + // dynamic imports executed in the *browser* against + // http://localhost:8080 — they resolve to files in dist/ at test + // runtime, not relative to the Playwright test file. + // `./crates/miden_client_web` is the wasm-bindgen module emitted + // into dist/ by the rollup rust plugin; the .d.ts files in + // js/types/ reference it but it doesn't exist until after build. + // Knip can't follow either of these dynamic targets. + "ignoreUnresolved": ["./index.js", "./crates/miden_client_web"], + }, + "crates/idxdb-store/src": { + // Each `ts/*.ts` is independently tsc-compiled into `js/` and + // consumed by the Rust crate, so each file is its own entry point. + "entry": [ + "ts/accounts.ts", + "ts/auth.ts", + "ts/chainData.ts", + "ts/export.ts", + "ts/import.ts", + "ts/notes.ts", + "ts/schema.ts", + "ts/settings.ts", + "ts/sync.ts", + "ts/transactions.ts", + "ts/utils.ts", + "ts/test-utils.ts", + "ts/**/*.test.ts", + ], + }, + }, + "ignore": ["packages/react-sdk/examples/**", "crates/idxdb-store/src/js/**"], +} diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..57781c1 --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,13 @@ +# Lefthook config — local dev hygiene hooks. +# Docs: https://lefthook.dev/configuration/ +# +# Install: `pnpm install` triggers `prepare` → `lefthook install`. +# Manual: `pnpm exec lefthook install` + +pre-commit: + commands: + lint-staged: + run: pnpm exec lint-staged + stage_fixed: true + +# commit-msg: future commitlint integration goes here diff --git a/package.json b/package.json index a42e5a5..53064cf 100644 --- a/package.json +++ b/package.json @@ -3,20 +3,42 @@ "private": true, "scripts": { "check:sync:react-sdk": "node scripts/check-react-sdk-sync.js", + "check:knip": "knip", "build:web-client": "pnpm --filter @miden-sdk/miden-sdk run build", "build:react-sdk": "pnpm --filter @miden-sdk/react run build", "build:vite-plugin": "pnpm --filter @miden-sdk/vite-plugin run build", + "test": "vitest run", + "test:watch": "vitest", "test:react-sdk": "pnpm --filter @miden-sdk/react run test:unit", - "test:react-sdk:coverage": "pnpm --filter @miden-sdk/react run test:coverage" + "test:react-sdk:coverage": "pnpm --filter @miden-sdk/react run test:coverage", + "prepare": "lefthook install || true", + "check:publint": "pnpm --filter @miden-sdk/miden-sdk --filter @miden-sdk/react --filter @miden-sdk/vite-plugin --workspace-concurrency=1 exec publint", + "check:attw": "pnpm --filter @miden-sdk/miden-sdk --filter @miden-sdk/react --filter @miden-sdk/vite-plugin --workspace-concurrency=1 exec attw --pack .", + "check:publish": "pnpm run build:web-client && pnpm run build:react-sdk && pnpm run build:vite-plugin && pnpm run check:publint && pnpm run check:attw" }, "dependencies": { "prettier": "^3.8.1" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^8.25.0", + "@arethetypeswrong/cli": "^0.18.2", "@typescript-eslint/parser": "^8.25.0", "eslint": "^9.30.1", - "typescript": "^5.5.4" + "eslint-config-prettier": "^10.1.8", + "knip": "^6.7.0", + "lefthook": "^1.13.6", + "lint-staged": "^16.4.0", + "publint": "^0.3.18", + "typescript": "^5.5.4", + "vitest": "^3.0.0" + }, + "lint-staged": { + "*.{ts,tsx,js,jsx}": [ + "eslint --fix", + "prettier --write" + ], + "*.{json,md,yml,yaml,css}": [ + "prettier --write" + ] }, "packageManager": "pnpm@9.15.4", "engines": { diff --git a/packages/react-sdk/.attw.json b/packages/react-sdk/.attw.json new file mode 100644 index 0000000..daeca4e --- /dev/null +++ b/packages/react-sdk/.attw.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/arethetypeswrong/arethetypeswrong.github.io/main/docs/configuration.md", + "profile": "esm-only" +} diff --git a/packages/react-sdk/CLAUDE.md b/packages/react-sdk/CLAUDE.md index a8a1c8c..9dcf1b0 100644 --- a/packages/react-sdk/CLAUDE.md +++ b/packages/react-sdk/CLAUDE.md @@ -45,36 +45,37 @@ function App() { ## Reading Data (Query Hooks) -All query hooks return `{ data, isLoading, error, refetch }`. +Query hooks return `{ ...data, isLoading, error, refetch }` — the data fields are spread directly onto the result object, with hook-specific names (no generic `data` field). ### List Accounts ```tsx -const { data: accounts, isLoading } = useAccounts(); +const { accounts, wallets, faucets, isLoading } = useAccounts(); -// accounts.wallets - regular accounts -// accounts.faucets - token faucets -// accounts.all - everything +// wallets - regular accounts +// faucets - token faucets +// accounts - both, combined ``` ### Get Account Details ```tsx -const { data: account } = useAccount(accountId); +const { account, isLoading } = useAccount(accountId); -// account.id, account.nonce, account.bech32id() -// account.balance(faucetId) - get token balance +// account.id(), account.nonce(), account.bech32id() +// account.vault().getBalance(assetId) - get token balance ``` ### Get Notes ```tsx -const { data: notes } = useNotes(); +const { notes, consumableNotes, noteSummaries, consumableNoteSummaries } = useNotes(); -// notes.input - incoming notes -// notes.consumable - ready to claim +// notes - all input notes for this account +// consumableNotes - subset that's ready to claim +// noteSummaries / consumableNoteSummaries - same lists, projected to UI-friendly summaries ``` ### Check Sync Status ```tsx -const { syncHeight, isSyncing, sync } = useSyncState(); +const { syncHeight, isSyncing, lastSyncTime, sync, error } = useSyncState(); // Manual sync await sync(); @@ -82,19 +83,19 @@ await sync(); ### Get Token Metadata ```tsx -const { data: metadata } = useAssetMetadata(faucetId); +const { metadata, isLoading } = useAssetMetadata(assetId); // metadata.symbol, metadata.decimals ``` ## Writing Data (Mutation Hooks) -All mutation hooks return `{ mutate, data, isLoading, stage, error, reset }`. +Mutation hooks return `{ , result, isLoading, stage, error, reset }` — the action callback is named after the hook (`send`, `consume`, `mint`, ...) and the resolved value is on `result`. **Transaction stages:** `idle` → `executing` → `proving` → `submitting` → `complete` ### Create Wallet ```tsx -const { mutate: createWallet, isLoading } = useCreateWallet(); +const { createWallet, isLoading } = useCreateWallet(); const account = await createWallet({ storageMode: "private", // "private" | "public" | "network" @@ -103,12 +104,12 @@ const account = await createWallet({ ### Send Tokens ```tsx -const { mutate: send, stage } = useSend(); +const { send, stage } = useSend(); await send({ from: senderAccountId, to: recipientAccountId, - faucetId: tokenFaucetId, + assetId: tokenFaucetId, amount: 1000n, noteType: "private", // "private" | "public" }); @@ -116,20 +117,20 @@ await send({ ### Send to Multiple Recipients ```tsx -const { mutate: multiSend } = useMultiSend(); +const { multiSend } = useMultiSend(); await multiSend({ from: senderAccountId, - outputs: [ - { to: recipient1, faucetId, amount: 500n }, - { to: recipient2, faucetId, amount: 300n }, + recipients: [ + { to: recipient1, assetId, amount: 500n }, + { to: recipient2, assetId, amount: 300n }, ], }); ``` ### Claim Notes ```tsx -const { mutate: consume } = useConsume(); +const { consume } = useConsume(); await consume({ accountId: myAccountId, @@ -139,7 +140,7 @@ await consume({ ### Mint Tokens (Faucet Owner) ```tsx -const { mutate: mint } = useMint(); +const { mint } = useMint(); await mint({ faucetId: myFaucetId, @@ -150,7 +151,7 @@ await mint({ ### Create Faucet ```tsx -const { mutate: createFaucet } = useCreateFaucet(); +const { createFaucet } = useCreateFaucet(); const faucet = await createFaucet({ symbol: "TOKEN", @@ -165,11 +166,11 @@ const faucet = await createFaucet({ ### Show Transaction Progress ```tsx function SendButton() { - const { mutate: send, stage, isLoading, error } = useSend(); + const { send, stage, isLoading, error } = useSend(); const handleSend = async () => { try { - await send({ from, to, faucetId, amount }); + await send({ from, to, assetId, amount }); } catch (err) { console.error("Transaction failed:", err); } @@ -207,11 +208,11 @@ const text = formatNoteSummary(summary); // "1.5 TOKEN" ### Wait for Transaction Confirmation ```tsx -const { mutate: waitForCommit } = useWaitForCommit(); +const { waitForCommit } = useWaitForCommit(); // After sending const result = await send({ ... }); -await waitForCommit({ transactionId: result.transactionId }); +await waitForCommit({ txId: result.txId }); ``` ### Access Client Directly @@ -316,7 +317,8 @@ import { SignerContext } from "@miden-sdk/react"; storeName: `mywallet_${userAddress}`, // unique per user for DB isolation isConnected: true, accountConfig: { - publicKey: userPublicKeyCommitment, // Uint8Array + publicKeyCommitment: userPublicKeyCommitment, // Uint8Array + accountType: "RegularAccountUpdatableCode", storageMode: "private", }, signCb: async (pubKey, signingInputs) => { @@ -377,23 +379,40 @@ account.bech32id(); // "miden1qy35..." ## Hook Reference -| Hook | Returns | Purpose | -|------|---------|---------| -| `useAccounts()` | `{ wallets, faucets, all }` | List all accounts | -| `useAccount(id)` | `Account` | Account details + balances | -| `useNotes(filter?)` | `{ input, consumable }` | Available notes | -| `useSyncState()` | `{ syncHeight, sync() }` | Sync status | -| `useAssetMetadata(id)` | `{ symbol, decimals }` | Token info | -| `useCreateWallet()` | `Account` | Create wallet | -| `useCreateFaucet()` | `Account` | Create faucet | -| `useImportAccount()` | `Account` | Import account | -| `useSend()` | `TransactionResult` | Send tokens | -| `useMultiSend()` | `TransactionResult` | Multi-recipient send | -| `useMint()` | `TransactionResult` | Mint tokens | -| `useConsume()` | `TransactionResult` | Claim notes | -| `useSwap()` | `TransactionResult` | Atomic swap | -| `useTransaction()` | `TransactionResult` | Custom transaction | -| `useCompile()` | `{ component, txScript, noteScript }` | Compile MASM into `AccountComponent` / `TransactionScript` / `NoteScript` | +Query hooks return `{ ...data, isLoading, error, refetch }`. Mutation hooks return `{ , result, isLoading, stage, error, reset }`. + +### Query (read) +| Hook | Data fields | Purpose | +|------|-------------|---------| +| `useAccounts()` | `accounts`, `wallets`, `faucets` | List local accounts | +| `useAccount(id)` | `account` | Account details + balances | +| `useNotes(filter?)` | `notes`, `consumableNotes`, `noteSummaries`, `consumableNoteSummaries` | Input notes + UI summaries | +| `useNoteStream(filter?)` | streaming variant of `useNotes` | Auto-updates as notes arrive | +| `useSyncState()` | `syncHeight`, `isSyncing`, `lastSyncTime`, `sync()` | Sync status + manual trigger | +| `useSyncControl()` | `pause()`, `resume()`, `isPaused` | Pause/resume the auto-sync timer | +| `useAssetMetadata(id)` | `metadata: { symbol, decimals }` | Token info | +| `useTransactionHistory(...)` | `transactions` | Local transaction log | +| `useSessionAccount()` | `account` | The signer's connected account | +| `useWaitForNotes(...)` | resolves when matching notes appear | Pull-style note waiting | + +### Mutation (write) +| Hook | Action | Returns on success | +|------|--------|--------------------| +| `useCreateWallet()` | `createWallet({ storageMode })` | `Account` | +| `useCreateFaucet()` | `createFaucet({ symbol, decimals, ... })` | `Account` | +| `useImportAccount()` | `importAccount(...)` | `Account` | +| `useImportNote()` | `importNote(...)` | imported `InputNoteRecord` | +| `useExportNote()` | `exportNote(...)` | serialized note bytes | +| `useImportStore()` / `useExportStore()` | store import/export | bytes / `void` | +| `useSend()` | `send({ from, to, assetId, amount, noteType })` | `SendResult` (with `txId`, `note`) | +| `useMultiSend()` | `multiSend({ from, recipients })` | `TransactionResult` | +| `useMint()` | `mint({ faucetId, to, amount })` | `TransactionResult` | +| `useConsume()` | `consume({ accountId, notes })` | `TransactionResult` | +| `useSwap()` | `swap({ ... })` | `TransactionResult` | +| `useTransaction()` | `transact({ ... })` | `TransactionResult` (custom tx) | +| `useExecuteProgram()` | `execute(...)` | program output | +| `useCompile()` | `compile({ source })` | `{ component, txScript, noteScript }` | +| `useWaitForCommit()` | `waitForCommit({ txId })` | resolves when committed on-chain | ## Type Imports diff --git a/packages/react-sdk/eslint.config.js b/packages/react-sdk/eslint.config.cjs similarity index 83% rename from packages/react-sdk/eslint.config.js rename to packages/react-sdk/eslint.config.cjs index 71027b0..9c2d6f7 100644 --- a/packages/react-sdk/eslint.config.js +++ b/packages/react-sdk/eslint.config.cjs @@ -1,5 +1,6 @@ const tsParser = require("@typescript-eslint/parser"); const tsPlugin = require("@typescript-eslint/eslint-plugin"); +const prettierConfig = require("eslint-config-prettier"); module.exports = [ { @@ -33,4 +34,6 @@ module.exports = [ ], }, }, + // Must be last: disables any stylistic rules that conflict with Prettier. + prettierConfig, ]; diff --git a/packages/react-sdk/package.json b/packages/react-sdk/package.json index 15d07bf..99fcee7 100644 --- a/packages/react-sdk/package.json +++ b/packages/react-sdk/package.json @@ -7,10 +7,16 @@ "types": "dist/index.d.ts", "exports": { ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.js" - } + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "./package.json": "./package.json" }, "files": [ "dist", @@ -40,11 +46,10 @@ "@playwright/test": "^1.55.0", "@testing-library/react": "^14.0.0", "@types/react": "^18.2.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^8.25.0", + "@typescript-eslint/parser": "^8.25.0", "@vitest/coverage-v8": "^3.0.0", - "eslint": "^8.0.0", - "http-server": "^14.1.1", + "eslint": "^9.30.1", "jsdom": "^24.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/packages/react-sdk/playwright.config.ts b/packages/react-sdk/playwright.config.ts index 1b484f8..eead91e 100644 --- a/packages/react-sdk/playwright.config.ts +++ b/packages/react-sdk/playwright.config.ts @@ -31,7 +31,7 @@ export default defineConfig({ ], webServer: { - command: "node ./test/serve-tests.js", + command: "node ./test/serve-tests.cjs", url: "http://127.0.0.1:8081", reuseExistingServer: true, timeout: 30000, diff --git a/packages/react-sdk/src/__tests__/mocks/miden-sdk.ts b/packages/react-sdk/src/__tests__/mocks/miden-sdk.ts index fddcb9e..99481cf 100644 --- a/packages/react-sdk/src/__tests__/mocks/miden-sdk.ts +++ b/packages/react-sdk/src/__tests__/mocks/miden-sdk.ts @@ -76,7 +76,7 @@ export const createMockNote = (id: string = "0xnote1") => ({ free: vi.fn(), }); -export const createMockOutputNote = (note = createMockNote()) => ({ +const createMockOutputNote = (note = createMockNote()) => ({ intoFull: vi.fn(() => note), }); @@ -162,7 +162,7 @@ export const createMockTransactionId = (id: string = "0xtx123") => ({ }); // Mock TransactionRecord -export const createMockTransactionRecord = ( +const createMockTransactionRecord = ( status: "committed" | "pending" | "discarded" = "committed" ) => ({ id: vi.fn(() => createMockTransactionId()), @@ -184,120 +184,6 @@ export const createMockTransactionRequest = () => ({ [Symbol.dispose]: vi.fn(), }); -// Mock NoteFilter -export const MockNoteFilter = vi.fn().mockImplementation(() => ({ - free: vi.fn(), -})); - -// Mock NoteFilterTypes enum -export const MockNoteFilterTypes = { - All: 0, - Consumed: 1, - Committed: 2, - Expected: 3, - Processing: 4, - List: 5, - Unique: 6, - Nullifiers: 7, - Unverified: 8, -}; - -// Mock NoteType enum -export const MockNoteType = { - Private: 2, - Public: 1, -}; - -export const MockNote = { - createP2IDNote: vi.fn( - ( - sender: ReturnType, - receiver: ReturnType, - assets: unknown, - noteType: number, - attachment: unknown - ) => ({ - id: vi.fn(() => ({ toString: () => "0xnote" })), - sender, - receiver, - assets, - noteType, - attachment, - }) - ), -}; - -export const MockNoteAssets = class NoteAssets { - assets: unknown[]; - constructor(assets: unknown[]) { - this.assets = assets; - } -}; - -export const MockFungibleAsset = class FungibleAsset { - faucetId: ReturnType; - amount: bigint; - constructor( - faucetId: ReturnType, - amount: bigint - ) { - this.faucetId = faucetId; - this.amount = amount; - } -}; - -export const MockNoteAttachment = class NoteAttachment {}; - -export const MockNoteArray = class NoteArray { - notes: unknown[]; - constructor(notes?: unknown[]) { - this.notes = notes ?? []; - } - push(note: unknown) { - this.notes.push(note); - } -}; - -export const MockNoteAndArgs = class NoteAndArgs { - note: unknown; - args: unknown; - constructor(note: unknown, args: unknown) { - this.note = note; - this.args = args; - } -}; - -export const MockNoteAndArgsArray = class NoteAndArgsArray { - notes: unknown[]; - constructor(notes: unknown[]) { - this.notes = notes; - } -}; - -export const MockTransactionRequestBuilder = class TransactionRequestBuilder { - withOwnOutputNotes = vi.fn(() => this); - withInputNotes = vi.fn(() => this); - build = vi.fn(() => ({})); -}; - -// Mock NoteId static methods -export const MockNoteId = { - fromHex: vi.fn((hex: string) => ({ toString: () => hex })), -}; - -// Mock AccountStorageMode -export const MockAccountStorageMode = { - private: vi.fn(() => ({ type: "private" })), - public: vi.fn(() => ({ type: "public" })), - network: vi.fn(() => ({ type: "network" })), -}; - -// Mock AccountId static methods -export const MockAccountId = { - fromHex: vi.fn((hex: string) => createMockAccountId(hex)), - fromBech32: vi.fn((bech32: string) => createMockAccountId(bech32)), -}; - // Mock FeltArray export const createMockFeltArray = (length: number = 16) => ({ length: vi.fn(() => length), @@ -306,27 +192,6 @@ export const createMockFeltArray = (length: number = 16) => ({ })), }); -// Mock AdviceInputs -export const MockAdviceInputs = class AdviceInputs {}; - -// Mock ForeignAccount -export const MockForeignAccount = Object.assign(class ForeignAccount {}, { - public: vi.fn( - (_id: unknown, _storage: unknown) => new (class ForeignAccount {})() - ), -}); - -// Mock ForeignAccountArray -export const MockForeignAccountArray = class ForeignAccountArray { - accounts: unknown[]; - constructor(accounts: unknown[] = []) { - this.accounts = accounts; - } -}; - -// Mock AccountStorageRequirements -export const MockAccountStorageRequirements = class AccountStorageRequirements {}; - // Create a mock WebClient export const createMockWebClient = ( overrides: Partial = {} @@ -406,7 +271,7 @@ export const createMockWebClient = ( return { ...defaultClient, ...overrides }; }; -export type MockWebClientType = { +type MockWebClientType = { createClient: ReturnType; getAccounts: ReturnType; getAccount: ReturnType; @@ -440,65 +305,3 @@ export type MockWebClientType = { executeProgram: ReturnType; free: ReturnType; }; - -// Factory to create mock SDK module -export const createMockSdkModule = ( - clientOverrides: Partial = {} -) => { - const mockClient = createMockWebClient(clientOverrides); - - const WebClientMock = Object.assign( - vi.fn().mockImplementation(() => mockClient), - { - createClient: vi.fn().mockResolvedValue(mockClient), - createClientWithExternalKeystore: vi.fn().mockResolvedValue(mockClient), - } - ); - - return { - WebClient: WebClientMock, - WasmWebClient: WebClientMock, - AccountId: MockAccountId, - Address: { - fromBech32: vi.fn((bech32: string) => ({ - accountId: vi.fn(() => createMockAccountId(bech32)), - toString: vi.fn(() => bech32), - })), - fromAccountId: vi.fn( - (accountId: ReturnType) => ({ - accountId: vi.fn(() => accountId), - toString: vi.fn(() => accountId.toString()), - }) - ), - }, - AccountStorageMode: MockAccountStorageMode, - NoteType: MockNoteType, - Note: MockNote, - NoteAssets: MockNoteAssets, - FungibleAsset: MockFungibleAsset, - NoteAttachment: MockNoteAttachment, - NoteArray: MockNoteArray, - NoteAndArgs: MockNoteAndArgs, - NoteAndArgsArray: MockNoteAndArgsArray, - TransactionRequestBuilder: MockTransactionRequestBuilder, - TransactionFilter: { - all: vi.fn(() => ({})), - uncommitted: vi.fn(() => ({})), - ids: vi.fn((ids: unknown) => ({ ids })), - }, - AdviceInputs: MockAdviceInputs, - ForeignAccount: MockForeignAccount, - ForeignAccountArray: MockForeignAccountArray, - AccountStorageRequirements: MockAccountStorageRequirements, - AccountFile: Object.assign( - vi.fn().mockImplementation(() => createMockAccountFile()), - { - deserialize: vi.fn(() => createMockAccountFile()), - } - ), - NoteId: MockNoteId, - NoteFilter: MockNoteFilter, - NoteFilterTypes: MockNoteFilterTypes, - __mockClient: mockClient, // Expose for test assertions - }; -}; diff --git a/packages/react-sdk/src/__tests__/mocks/signer-context.ts b/packages/react-sdk/src/__tests__/mocks/signer-context.ts index 5487a17..049ea59 100644 --- a/packages/react-sdk/src/__tests__/mocks/signer-context.ts +++ b/packages/react-sdk/src/__tests__/mocks/signer-context.ts @@ -12,7 +12,7 @@ import type { * Creates a mock AccountStorageMode. * Matches the SDK's AccountStorageMode interface. */ -export const createMockAccountStorageMode = ( +const createMockAccountStorageMode = ( mode: "private" | "public" | "network" = "public" ) => ({ toString: vi.fn(() => mode), @@ -37,7 +37,7 @@ export function createMockSignerAccountConfig( * Creates a mock sign callback function. * Returns a mock 67-byte signature (typical ECDSA signature size). */ -export function createMockSignCallback(): SignCallback { +function createMockSignCallback(): SignCallback { return vi.fn().mockResolvedValue(new Uint8Array(67).fill(0xab)); } diff --git a/packages/react-sdk/src/__tests__/store/MidenStore.test.ts b/packages/react-sdk/src/__tests__/store/MidenStore.test.ts index 54bcb66..38a91bc 100644 --- a/packages/react-sdk/src/__tests__/store/MidenStore.test.ts +++ b/packages/react-sdk/src/__tests__/store/MidenStore.test.ts @@ -280,19 +280,13 @@ describe("MidenStore", () => { }); describe("selector hooks", () => { - it("should provide useClient selector", () => { + it("setClient stores the client and flips isReady", () => { const mockClient = createMockWebClient(); - useMidenStore.getState().setClient(mockClient as any); - - // Test via getState (since we can't use React hooks directly in tests) - expect(useMidenStore.getState().client).toBe(mockClient); - }); - - it("should provide useIsReady selector", () => { expect(useMidenStore.getState().isReady).toBe(false); - useMidenStore.getState().setClient(createMockWebClient() as any); + useMidenStore.getState().setClient(mockClient as any); + expect(useMidenStore.getState().client).toBe(mockClient); expect(useMidenStore.getState().isReady).toBe(true); }); diff --git a/packages/react-sdk/src/__tests__/utils/test-utils.tsx b/packages/react-sdk/src/__tests__/utils/test-utils.tsx deleted file mode 100644 index 546e3a2..0000000 --- a/packages/react-sdk/src/__tests__/utils/test-utils.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import React, { type ReactNode } from "react"; -import { render, type RenderOptions, renderHook } from "@testing-library/react"; -import { useMidenStore } from "../../store/MidenStore"; -import type { MidenConfig } from "../../types"; -import { - createMockWebClient, - type MockWebClientType, -} from "../mocks/miden-sdk"; - -// Reset store between tests -export const resetStore = () => { - useMidenStore.getState().reset(); -}; - -// Provider wrapper with mock client already set -interface WrapperProps { - children: ReactNode; -} - -interface TestProviderOptions { - config?: MidenConfig; - mockClient?: Partial; - initialReady?: boolean; -} - -// Create a test provider that sets the client directly (bypassing async init) -export const createTestProvider = (options: TestProviderOptions = {}) => { - const mockClient = createMockWebClient(options.mockClient); - - const TestProvider = ({ children }: WrapperProps) => { - // Set up the store directly with our mock client - React.useEffect(() => { - if (options.initialReady !== false) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - useMidenStore.getState().setClient(mockClient as any); - } - }, []); - - return <>{children}; - }; - - return { TestProvider, mockClient }; -}; - -// Render hook with test provider -export const renderHookWithProvider = ( - hook: (props: TProps) => TResult, - options: TestProviderOptions & { hookProps?: TProps } = {} -) => { - const { TestProvider, mockClient } = createTestProvider(options); - - const result = renderHook(hook, { - wrapper: TestProvider, - initialProps: options.hookProps as TProps, - }); - - return { ...result, mockClient }; -}; - -// Render component with provider -export const renderWithProvider = ( - ui: React.ReactElement, - options: TestProviderOptions & Omit = {} -): ReturnType & { mockClient: MockWebClientType } => { - const { TestProvider, mockClient } = createTestProvider(options); - - const result = render(ui, { - wrapper: TestProvider, - ...options, - }); - - return { ...result, mockClient }; -}; - -// Wait for async state updates -export const waitForStateUpdate = () => - new Promise((resolve) => setTimeout(resolve, 0)); - -// Helper to wait for loading to complete -export const waitForLoading = async ( - getLoadingState: () => boolean, - timeout: number = 5000 -): Promise => { - const start = Date.now(); - while (getLoadingState() && Date.now() - start < timeout) { - await waitForStateUpdate(); - } -}; - -// Helper to set up store with mock data -export const setupStoreWithData = (options: { - client?: MockWebClientType; - accounts?: ReturnType< - typeof import("../mocks/miden-sdk").createMockAccountHeader - >[]; - notes?: ReturnType< - typeof import("../mocks/miden-sdk").createMockInputNoteRecord - >[]; - syncHeight?: number; - isReady?: boolean; -}) => { - const store = useMidenStore.getState(); - - if (options.client) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - store.setClient(options.client as any); - } - - if (options.accounts) { - store.setAccounts(options.accounts as unknown as typeof store.accounts); - } - - if (options.notes) { - store.setNotes(options.notes as unknown as typeof store.notes); - } - - if (options.syncHeight !== undefined) { - store.setSyncState({ syncHeight: options.syncHeight }); - } -}; diff --git a/packages/react-sdk/src/context/SignerContext.ts b/packages/react-sdk/src/context/SignerContext.ts index d9f467f..1a7bf64 100644 --- a/packages/react-sdk/src/context/SignerContext.ts +++ b/packages/react-sdk/src/context/SignerContext.ts @@ -27,7 +27,7 @@ export type SignCallback = ( * @param pubKey - Public key commitment bytes * @returns Promise resolving to the secret key bytes */ -export type GetKeyCallback = (pubKey: Uint8Array) => Promise; +type GetKeyCallback = (pubKey: Uint8Array) => Promise; /** * Insert-key callback for WebClient.createClientWithExternalKeystore. @@ -36,10 +36,7 @@ export type GetKeyCallback = (pubKey: Uint8Array) => Promise; * @param pubKey - Public key commitment bytes * @param secretKey - Secret key bytes to store */ -export type InsertKeyCallback = ( - pubKey: Uint8Array, - secretKey: Uint8Array -) => void; +type InsertKeyCallback = (pubKey: Uint8Array, secretKey: Uint8Array) => void; /** * Account type for signer accounts. diff --git a/packages/react-sdk/src/store/MidenStore.ts b/packages/react-sdk/src/store/MidenStore.ts index 1dcfdfc..6770919 100644 --- a/packages/react-sdk/src/store/MidenStore.ts +++ b/packages/react-sdk/src/store/MidenStore.ts @@ -242,14 +242,10 @@ export const useMidenStore = create()((set) => ({ })); // Selector hooks for optimal re-renders -export const useClient = () => useMidenStore((state) => state.client); -export const useIsReady = () => useMidenStore((state) => state.isReady); export const useSignerConnected = () => useMidenStore((state) => state.signerConnected); export const useIsInitializing = () => useMidenStore((state) => state.isInitializing); -export const useInitError = () => useMidenStore((state) => state.initError); -export const useConfig = () => useMidenStore((state) => state.config); export const useSyncStateStore = () => useMidenStore((state) => state.sync); export const useAccountsStore = () => useMidenStore((state) => state.accounts); export const useNotesStore = () => useMidenStore((state) => state.notes); diff --git a/packages/react-sdk/src/types/index.ts b/packages/react-sdk/src/types/index.ts index 630872e..47244d0 100644 --- a/packages/react-sdk/src/types/index.ts +++ b/packages/react-sdk/src/types/index.ts @@ -38,11 +38,8 @@ export type { TransactionRecord, TransactionRequest, NoteType, - NoteId, Note, AccountStorageMode, - NoteVisibility, - StorageMode, }; export type { AccountRef } from "../utils/accountParsing"; @@ -50,8 +47,6 @@ export type { AccountRef } from "../utils/accountParsing"; // Re-export signer types for external signer providers export type { SignCallback, - GetKeyCallback, - InsertKeyCallback, SignerAccountType, SignerAccountConfig, SignerContextValue, diff --git a/packages/react-sdk/src/utils/transactions.ts b/packages/react-sdk/src/utils/transactions.ts index 8c123ba..bdbaa02 100644 --- a/packages/react-sdk/src/utils/transactions.ts +++ b/packages/react-sdk/src/utils/transactions.ts @@ -1,7 +1,7 @@ import { NoteType, TransactionFilter } from "@miden-sdk/miden-sdk"; import type { Note, TransactionId } from "@miden-sdk/miden-sdk"; -export type ClientWithTransactions = { +type ClientWithTransactions = { syncState: () => Promise; getTransactions: (filter: TransactionFilter) => Promise< Array<{ diff --git a/packages/react-sdk/test/serve-tests.js b/packages/react-sdk/test/serve-tests.cjs similarity index 100% rename from packages/react-sdk/test/serve-tests.js rename to packages/react-sdk/test/serve-tests.cjs diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index a5c63e4..9082471 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -2,14 +2,20 @@ "name": "@miden-sdk/vite-plugin", "version": "0.14.5", "description": "Vite plugin for Miden dApps — WASM dedup, COOP/COEP headers, and gRPC-web proxy", + "type": "commonjs", "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", "exports": { ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.js" + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } } }, "files": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d181818..22ca8b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,18 +26,36 @@ importers: specifier: ^3.8.1 version: 3.8.3 devDependencies: - '@typescript-eslint/eslint-plugin': - specifier: 8.39.1 - version: 8.39.1(@typescript-eslint/parser@8.39.1(eslint@9.33.0)(typescript@5.9.3))(eslint@9.33.0)(typescript@5.9.3) + '@arethetypeswrong/cli': + specifier: ^0.18.2 + version: 0.18.2 '@typescript-eslint/parser': specifier: 8.39.1 - version: 8.39.1(eslint@9.33.0)(typescript@5.9.3) + version: 8.39.1(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.3) eslint: specifier: 9.33.0 - version: 9.33.0 + version: 9.33.0(jiti@2.6.1) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@9.33.0(jiti@2.6.1)) + knip: + specifier: ^6.7.0 + version: 6.7.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + lefthook: + specifier: ^1.13.6 + version: 1.13.6 + lint-staged: + specifier: ^16.4.0 + version: 16.4.0 + publint: + specifier: ^0.3.18 + version: 0.3.18 typescript: specifier: ^5.5.4 version: 5.9.3 + vitest: + specifier: ^3.0.0 + version: 3.2.4(@types/node@24.12.2)(jsdom@24.1.3) crates/idxdb-store/src: dependencies: @@ -57,30 +75,24 @@ importers: '@types/semver': specifier: ^7.5.8 version: 7.7.1 - '@typescript-eslint/eslint-plugin': - specifier: 8.39.1 - version: 8.39.1(@typescript-eslint/parser@8.39.1(eslint@9.33.0)(typescript@5.9.3))(eslint@9.33.0)(typescript@5.9.3) '@vitest/coverage-v8': specifier: ^3.0.0 version: 3.2.4(vitest@3.2.4(@types/node@24.12.2)(jsdom@24.1.3)) eslint: specifier: 9.33.0 - version: 9.33.0 + version: 9.33.0(jiti@2.6.1) fake-indexeddb: specifier: ^6.0.0 version: 6.2.5 typescript-eslint: specifier: 8.39.1 - version: 8.39.1(eslint@9.33.0)(typescript@5.9.3) + version: 8.39.1(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.3) vitest: specifier: ^3.0.0 version: 3.2.4(@types/node@24.12.2)(jsdom@24.1.3) crates/web-client: dependencies: - '@rollup/plugin-typescript': - specifier: ^12.3.0 - version: 12.3.0(rollup@4.60.2)(tslib@2.8.1)(typescript@5.9.3) dexie: specifier: ^4.0.1 version: 4.4.2 @@ -100,36 +112,18 @@ importers: '@types/node': specifier: ^24.9.2 version: 24.12.2 - '@vitest/coverage-v8': - specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/node@24.12.2)(jsdom@24.1.3)) '@wasm-tool/rollup-plugin-rust': specifier: ^3.0.3 version: 3.1.5(binaryen@129.0.0)(rollup@4.60.2) binaryen: specifier: ^129.0.0 version: 129.0.0 - chai: - specifier: ^5.1.1 - version: 5.3.3 cpr: specifier: ^3.0.1 version: 3.0.1 cross-env: specifier: ^7.0.3 version: 7.0.3 - esm: - specifier: ^3.2.25 - version: 3.2.25 - http-server: - specifier: ^14.1.1 - version: 14.1.1 - mocha: - specifier: ^10.7.3 - version: 10.8.2 - puppeteer: - specifier: ^23.1.0 - version: 23.11.1(typescript@5.9.3) rimraf: specifier: ^6.0.1 version: 6.1.3 @@ -139,9 +133,6 @@ importers: rollup-plugin-copy: specifier: ^3.5.0 version: 3.5.0 - ts-node: - specifier: ^10.9.2 - version: 10.9.2(@types/node@24.12.2)(typescript@5.9.3) typedoc: specifier: ^0.28.1 version: 0.28.19(typescript@5.9.3) @@ -151,9 +142,6 @@ importers: typescript: specifier: ^5.5.4 version: 5.9.3 - vitest: - specifier: ^3.0.0 - version: 3.2.4(@types/node@24.12.2)(jsdom@24.1.3) packages/react-sdk: dependencies: @@ -175,19 +163,16 @@ importers: version: 18.3.28 '@typescript-eslint/eslint-plugin': specifier: 8.39.1 - version: 8.39.1(@typescript-eslint/parser@8.39.1(eslint@9.33.0)(typescript@5.9.3))(eslint@9.33.0)(typescript@5.9.3) + version: 8.39.1(@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': specifier: 8.39.1 - version: 8.39.1(eslint@9.33.0)(typescript@5.9.3) + version: 8.39.1(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.3) '@vitest/coverage-v8': specifier: ^3.0.0 version: 3.2.4(vitest@3.2.4(@types/node@24.12.2)(jsdom@24.1.3)) eslint: specifier: 9.33.0 - version: 9.33.0 - http-server: - specifier: ^14.1.1 - version: 14.1.1 + version: 9.33.0(jiti@2.6.1) jsdom: specifier: ^24.0.0 version: 24.1.3 @@ -199,7 +184,7 @@ importers: version: 18.3.1(react@18.3.1) tsup: specifier: ^8.0.0 - version: 8.5.1(postcss@8.5.12)(typescript@5.9.3)(yaml@2.8.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.12)(typescript@5.9.3)(yaml@2.8.3) typescript: specifier: ^5.0.0 version: 5.9.3 @@ -217,7 +202,7 @@ importers: version: 3.2.4(vitest@3.2.4(@types/node@20.19.39)(jsdom@24.1.3)) tsup: specifier: ^8.0.0 - version: 8.5.1(postcss@8.5.12)(typescript@5.9.3)(yaml@2.8.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.12)(typescript@5.9.3)(yaml@2.8.3) typescript: specifier: ^5.0.0 version: 5.9.3 @@ -234,6 +219,18 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@andrewbranch/untar.js@1.0.3': + resolution: {integrity: sha512-Jh15/qVmrLGhkKJBdXlK1+9tY4lZruYjsgkDFj08ZmDiWVBLJcqkok7Z0/R0In+i1rScBpJlSvrTS2Lm41Pbnw==} + + '@arethetypeswrong/cli@0.18.2': + resolution: {integrity: sha512-PcFM20JNlevEDKBg4Re29Rtv2xvjvQZzg7ENnrWFSS0PHgdP2njibVFw+dRUhNkPgNfac9iUqO0ohAXqQL4hbw==} + engines: {node: '>=20'} + hasBin: true + + '@arethetypeswrong/core@0.18.2': + resolution: {integrity: sha512-GiwTmBFOU1/+UVNqqCGzFJYfBXEytUkiI+iRZ6Qx7KmUVtLm00sYySkfe203C9QtPG11yOz1ZaMek8dT/xnlgg==} + engines: {node: '>=20'} + '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} @@ -266,9 +263,12 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} + '@braidai/lang@1.1.2': + resolution: {integrity: sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA==} + + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} '@csstools/color-helpers@5.1.0': resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} @@ -298,6 +298,15 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild/aix-ppc64@0.28.0': resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} engines: {node: '>=18'} @@ -551,8 +560,14 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@loaderkit/resolve@1.0.5': + resolution: {integrity: sha512-fhkdGM57xhJ7CO91MUgbQlb0ClP0AJ9vB3yoVnBTslYJqrJOCVEbOprZcxZlexdMbmTBPQqVcQYr+j4oRRtIZA==} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -566,6 +581,228 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@oxc-parser/binding-android-arm-eabi@0.127.0': + resolution: {integrity: sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.127.0': + resolution: {integrity: sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.127.0': + resolution: {integrity: sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.127.0': + resolution: {integrity: sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.127.0': + resolution: {integrity: sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + resolution: {integrity: sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + resolution: {integrity: sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + resolution: {integrity: sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + resolution: {integrity: sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + resolution: {integrity: sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + resolution: {integrity: sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + resolution: {integrity: sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + resolution: {integrity: sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + resolution: {integrity: sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + resolution: {integrity: sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + resolution: {integrity: sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + resolution: {integrity: sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + resolution: {integrity: sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + resolution: {integrity: sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + resolution: {integrity: sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + + '@oxc-resolver/binding-android-arm-eabi@11.19.1': + resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.19.1': + resolution: {integrity: sha512-oolbkRX+m7Pq2LNjr/kKgYeC7bRDMVTWPgxBGMjSpZi/+UskVo4jsMU3MLheZV55jL6c3rNelPl4oD60ggYmqA==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.19.1': + resolution: {integrity: sha512-nUC6d2i3R5B12sUW4O646qD5cnMXf2oBGPLIIeaRfU9doJRORAbE2SGv4eW6rMqhD+G7nf2Y8TTJTLiiO3Q/dQ==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.19.1': + resolution: {integrity: sha512-cV50vE5+uAgNcFa3QY1JOeKDSkM/9ReIcc/9wn4TavhW/itkDGrXhw9jaKnkQnGbjJ198Yh5nbX/Gr2mr4Z5jQ==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.19.1': + resolution: {integrity: sha512-xZOQiYGFxtk48PBKff+Zwoym7ScPAIVp4c14lfLxizO2LTTTJe5sx9vQNGrBymrf/vatSPNMD4FgsaaRigPkqw==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1': + resolution: {integrity: sha512-lXZYWAC6kaGe/ky2su94e9jN9t6M0/6c+GrSlCqL//XO1cxi5lpAhnJYdyrKfm0ZEr/c7RNyAx3P7FSBcBd5+A==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.19.1': + resolution: {integrity: sha512-veG1kKsuK5+t2IsO9q0DErYVSw2azvCVvWHnfTOS73WE0STdLLB7Q1bB9WR+yHPQM76ASkFyRbogWo1GR1+WbQ==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.19.1': + resolution: {integrity: sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-musl@11.19.1': + resolution: {integrity: sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': + resolution: {integrity: sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ==} + cpu: [ppc64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': + resolution: {integrity: sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': + resolution: {integrity: sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': + resolution: {integrity: sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA==} + cpu: [s390x] + os: [linux] + + '@oxc-resolver/binding-linux-x64-gnu@11.19.1': + resolution: {integrity: sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-linux-x64-musl@11.19.1': + resolution: {integrity: sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-openharmony-arm64@11.19.1': + resolution: {integrity: sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.19.1': + resolution: {integrity: sha512-w8UCKhX826cP/ZLokXDS6+milN8y4X7zidsAttEdWlVoamTNf6lhBJldaWr3ukTDiye7s4HRcuPEPOXNC432Vg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.19.1': + resolution: {integrity: sha512-nJ4AsUVZrVKwnU/QRdzPCCrO0TrabBqgJ8pJhXITdZGYOV28TIYystV1VFLbQ7DtAcaBHpocT5/ZJnF78YJPtQ==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-ia32-msvc@11.19.1': + resolution: {integrity: sha512-EW+ND5q2Tl+a3pH81l1QbfgbF3HmqgwLfDfVithRFheac8OTcnbXt/JxqD2GbDkb7xYEqy1zNaVFRr3oeG8npA==} + cpu: [ia32] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.19.1': + resolution: {integrity: sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw==} + cpu: [x64] + os: [win32] + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -575,10 +812,9 @@ packages: engines: {node: '>=18'} hasBin: true - '@puppeteer/browsers@2.6.1': - resolution: {integrity: sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==} + '@publint/pack@0.1.4': + resolution: {integrity: sha512-HDVTWq3H0uTXiU0eeSQntcVUTPP3GamzeXI41+x7uU9J65JgWQh3qWZHblR1i0npXfFtF+mxBiU2nJH8znxWnQ==} engines: {node: '>=18'} - hasBin: true '@rollup/plugin-commonjs@25.0.8': resolution: {integrity: sha512-ZEZWTK5n6Qde0to4vS9Mr5x/0UZoqCxPVR9KRUjU4kA2sO7GEUn1fop0DAwpO6z0Nw/kJON9bDmSxdWxO/TT1A==} @@ -598,19 +834,6 @@ packages: rollup: optional: true - '@rollup/plugin-typescript@12.3.0': - resolution: {integrity: sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: '>=4.59.0' - tslib: '*' - typescript: '>=3.7.0' - peerDependenciesMeta: - rollup: - optional: true - tslib: - optional: true - '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -760,6 +983,10 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + '@testing-library/dom@9.3.4': resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==} engines: {node: '>=14'} @@ -771,20 +998,8 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 - '@tootallnate/quickjs-emscripten@0.23.0': - resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} - - '@tsconfig/node10@1.0.12': - resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} - - '@tsconfig/node12@1.0.11': - resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} - - '@tsconfig/node14@1.0.3': - resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} - - '@tsconfig/node16@1.0.4': - resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -840,9 +1055,6 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@types/yauzl@2.10.3': - resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - '@typescript-eslint/eslint-plugin@8.39.1': resolution: {integrity: sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -960,10 +1172,6 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn-walk@8.3.5: - resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} - engines: {node: '>=0.4.0'} - acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} @@ -976,9 +1184,9 @@ packages: ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - ansi-colors@4.1.3: - resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} - engines: {node: '>=6'} + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} @@ -1003,13 +1211,6 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - arg@4.1.3: - resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1028,16 +1229,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - ast-types@0.13.4: - resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} - engines: {node: '>=4'} - ast-v8-to-istanbul@0.3.12: resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==} - async@3.2.6: - resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -1045,14 +1239,6 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - b4a@1.8.0: - resolution: {integrity: sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==} - peerDependencies: - react-native-b4a: '*' - peerDependenciesMeta: - react-native-b4a: - optional: true - balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1060,62 +1246,6 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - bare-events@2.8.2: - resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} - peerDependencies: - bare-abort-controller: '*' - peerDependenciesMeta: - bare-abort-controller: - optional: true - - bare-fs@4.7.1: - resolution: {integrity: sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==} - engines: {bare: '>=1.16.0'} - peerDependencies: - bare-buffer: '*' - peerDependenciesMeta: - bare-buffer: - optional: true - - bare-os@3.9.0: - resolution: {integrity: sha512-JTjuZyNIDpw+GytMO4a6TK1VXdVKKJr6DRxEHasyuYyShV2deuiHJK/ahGZlebc+SG0/wJCB9XK8gprBGDFi/Q==} - engines: {bare: '>=1.14.0'} - - bare-path@3.0.0: - resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} - - bare-stream@2.13.0: - resolution: {integrity: sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==} - peerDependencies: - bare-abort-controller: '*' - bare-buffer: '*' - bare-events: '*' - peerDependenciesMeta: - bare-abort-controller: - optional: true - bare-buffer: - optional: true - bare-events: - optional: true - - bare-url@2.4.2: - resolution: {integrity: sha512-/9a2j4ac6ckpmAHvod/ob7x439OAHst/drc2Clnq+reRYd/ovddwcF4LfoxHyNk5AuGBnPg+HqFjmE/Zpq6v0A==} - - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - - basic-auth@2.0.1: - resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} - engines: {node: '>= 0.8'} - - basic-ftp@5.3.0: - resolution: {integrity: sha512-5K9eNNn7ywHPsYnFwjKgYH8Hf8B5emh7JKcPaVjjrMJFQQwGpwowEnZNEtHs7DfR7hCZsmaK3VA4HUK0YarT+w==} - engines: {node: '>=10.0.0'} - - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - binaryen@129.0.0: resolution: {integrity: sha512-NyF5J0SfRoLDthpPh36FGTycOEv3Eqnkq3+mP5Cqt6iD9BLGGJMEVuPzu81nhLy2MMpPKmRTM9VLZihfyRQv8A==} hasBin: true @@ -1131,15 +1261,6 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browser-stdout@1.3.1: - resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} - - buffer-crc32@0.2.13: - resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - - buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1166,10 +1287,6 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -1182,14 +1299,14 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + check-error@2.1.3: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -1198,18 +1315,29 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} - chromium-bidi@0.11.0: - resolution: {integrity: sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==} - peerDependencies: - devtools-protocol: '*' + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-highlight@2.1.11: + resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1220,10 +1348,21 @@ packages: colorette@1.4.0: resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1238,26 +1377,10 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - corser@2.0.1: - resolution: {integrity: sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==} - engines: {node: '>= 0.4.0'} - - cosmiconfig@9.0.1: - resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true - cpr@3.0.1: resolution: {integrity: sha512-Xch4PXQ/KC8lJ+KfJ9JI6eG/nmppLrPPWg5Q+vh65Qr9EjuJEubxh/H/Le1TmCZ7+Xv7iJuNRqapyOFZB+wsxA==} hasBin: true - create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - cross-env@7.0.3: resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} @@ -1278,10 +1401,6 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} - data-uri-to-buffer@6.0.2: - resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} - engines: {node: '>= 14'} - data-urls@5.0.0: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} @@ -1295,10 +1414,6 @@ packages: supports-color: optional: true - decamelize@4.0.0: - resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} - engines: {node: '>=10'} - decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} @@ -1325,28 +1440,13 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - degenerator@5.0.1: - resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} - engines: {node: '>= 14'} - delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - devtools-protocol@0.0.1367902: - resolution: {integrity: sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==} - dexie@4.4.2: resolution: {integrity: sha512-zMtV8q79EFE5U8FKZvt0Y/77PCU/Hr/RDxv1EDeo228L+m/HTbeN2AjoQm674rhQCX8n3ljK87lajt7UQuZfvw==} - diff@4.0.4: - resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} - engines: {node: '>=0.3.1'} - - diff@5.2.2: - resolution: {integrity: sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==} - engines: {node: '>=0.3.1'} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -1361,14 +1461,17 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - end-of-stream@1.4.5: - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + emojilib@2.4.0: + resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} @@ -1378,12 +1481,9 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} - env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} - - error-ex@1.3.4: - resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} @@ -1420,10 +1520,11 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - escodegen@2.1.0: - resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} - engines: {node: '>=6.0'} + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} hasBin: true + peerDependencies: + eslint: 9.33.0 eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} @@ -1447,19 +1548,10 @@ packages: jiti: optional: true - esm@3.2.25: - resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} - engines: {node: '>=6'} - espree@10.4.0: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - esquery@1.7.0: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} @@ -1482,21 +1574,13 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - eventemitter3@4.0.7: - resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - - events-universal@1.0.1: - resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} - extract-zip@2.0.1: - resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} - engines: {node: '>= 10.17.0'} - hasBin: true - fake-indexeddb@6.2.5: resolution: {integrity: sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==} engines: {node: '>=18'} @@ -1504,9 +1588,6 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-fifo@1.3.2: - resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} - fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -1520,8 +1601,8 @@ packages: fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - fd-slicer@1.1.0: - resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fd-package-json@2.0.0: + resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} @@ -1536,6 +1617,9 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -1555,22 +1639,9 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flat@5.0.2: - resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} - hasBin: true - flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - follow-redirects@1.16.0: - resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -1583,6 +1654,11 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + formatly@0.3.0: + resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} + engines: {node: '>=18.3.0'} + hasBin: true + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -1614,6 +1690,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -1622,13 +1702,8 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} - - get-uri@6.0.5: - resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} - engines: {node: '>= 14'} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -1703,13 +1778,8 @@ packages: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} - he@1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} - hasBin: true - - html-encoding-sniffer@3.0.0: - resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} - engines: {node: '>=12'} + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} @@ -1722,15 +1792,6 @@ packages: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} - http-proxy@1.18.1: - resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} - engines: {node: '>=8.0.0'} - - http-server@14.1.1: - resolution: {integrity: sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==} - engines: {node: '>=12'} - hasBin: true - https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -1739,9 +1800,6 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1769,10 +1827,6 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} - ip-address@10.1.1: - resolution: {integrity: sha512-1FMu8/N15Ck1BL551Jf42NYIoin2unWjLQ2Fze/DXryJRl5twqtwNHlO39qERGbIOcKYWHdgRryhOC+NG4eaLw==} - engines: {node: '>= 12'} - is-arguments@1.2.0: resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} engines: {node: '>= 0.4'} @@ -1781,17 +1835,10 @@ packages: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-bigint@1.1.0: resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} engines: {node: '>= 0.4'} - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - is-boolean-object@1.2.2: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} @@ -1816,6 +1863,10 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1835,10 +1886,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-plain-obj@2.1.0: - resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} - engines: {node: '>=8'} - is-plain-object@3.0.1: resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==} engines: {node: '>=0.10.0'} @@ -1869,10 +1916,6 @@ packages: resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} engines: {node: '>= 0.4'} - is-unicode-supported@0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} - is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -1910,6 +1953,10 @@ packages: resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} engines: {node: 20 || >=22} + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -1939,9 +1986,6 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -1954,6 +1998,65 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + knip@6.7.0: + resolution: {integrity: sha512-ckL51NDH1YJxnv1kNB0iUdDngB4f/e9Igz8uIqYfmNDoyOFmmk1V0WFv3LQ7/hzC63b2Z9X41gGUE9eOWrZpaA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + lefthook-darwin-arm64@1.13.6: + resolution: {integrity: sha512-m6Lb77VGc84/Qo21Lhq576pEvcgFCnvloEiP02HbAHcIXD0RTLy9u2yAInrixqZeaz13HYtdDaI7OBYAAdVt8A==} + cpu: [arm64] + os: [darwin] + + lefthook-darwin-x64@1.13.6: + resolution: {integrity: sha512-CoRpdzanu9RK3oXR1vbEJA5LN7iB+c7hP+sONeQJzoOXuq4PNKVtEaN84Gl1BrVtCNLHWFAvCQaZPPiiXSy8qg==} + cpu: [x64] + os: [darwin] + + lefthook-freebsd-arm64@1.13.6: + resolution: {integrity: sha512-X4A7yfvAJ68CoHTqP+XvQzdKbyd935sYy0bQT6Ajz7FL1g7hFiro8dqHSdPdkwei9hs8hXeV7feyTXbYmfjKQQ==} + cpu: [arm64] + os: [freebsd] + + lefthook-freebsd-x64@1.13.6: + resolution: {integrity: sha512-ai2m+Sj2kGdY46USfBrCqLKe9GYhzeq01nuyDYCrdGISePeZ6udOlD1k3lQKJGQCHb0bRz4St0r5nKDSh1x/2A==} + cpu: [x64] + os: [freebsd] + + lefthook-linux-arm64@1.13.6: + resolution: {integrity: sha512-cbo4Wtdq81GTABvikLORJsAWPKAJXE8Q5RXsICFUVznh5PHigS9dFW/4NXywo0+jfFPCT6SYds2zz4tCx6DA0Q==} + cpu: [arm64] + os: [linux] + + lefthook-linux-x64@1.13.6: + resolution: {integrity: sha512-uJl9vjCIIBTBvMZkemxCE+3zrZHlRO7Oc+nZJ+o9Oea3fu+W82jwX7a7clw8jqNfaeBS+8+ZEQgiMHWCloTsGw==} + cpu: [x64] + os: [linux] + + lefthook-openbsd-arm64@1.13.6: + resolution: {integrity: sha512-7r153dxrNRQ9ytRs2PmGKKkYdvZYFPre7My7XToSTiRu5jNCq++++eAKVkoyWPduk97dGIA+YWiEr5Noe0TK2A==} + cpu: [arm64] + os: [openbsd] + + lefthook-openbsd-x64@1.13.6: + resolution: {integrity: sha512-Z+UhLlcg1xrXOidK3aLLpgH7KrwNyWYE3yb7ITYnzJSEV8qXnePtVu8lvMBHs/myzemjBzeIr/U/+ipjclR06g==} + cpu: [x64] + os: [openbsd] + + lefthook-windows-arm64@1.13.6: + resolution: {integrity: sha512-Uxef6qoDxCmUNQwk8eBvddYJKSBFglfwAY9Y9+NnnmiHpWTjjYiObE9gT2mvGVpEgZRJVAatBXc+Ha5oDD/OgQ==} + cpu: [arm64] + os: [win32] + + lefthook-windows-x64@1.13.6: + resolution: {integrity: sha512-mOZoM3FQh3o08M8PQ/b3IYuL5oo36D9ehczIw1dAgp1Ly+Tr4fJ96A+4SEJrQuYeRD4mex9bR7Ps56I73sBSZA==} + cpu: [x64] + os: [win32] + + lefthook@1.13.6: + resolution: {integrity: sha512-ojj4/4IJ29Xn4drd5emqVgilegAPN3Kf0FQM2p/9+lwSTpU+SZ1v4Ig++NF+9MOa99UKY8bElmVrLhnUUNFh5g==} + hasBin: true + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -1968,6 +2071,15 @@ packages: linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + lint-staged@16.4.0: + resolution: {integrity: sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==} + engines: {node: '>=20.17'} + hasBin: true + + listr2@9.0.5: + resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} + engines: {node: '>=20.0.0'} + load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1979,9 +2091,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} @@ -1997,10 +2109,6 @@ packages: resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} engines: {node: 20 || >=22} - lru-cache@7.18.3: - resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} - engines: {node: '>=12'} - lunr@2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} @@ -2018,13 +2126,21 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - markdown-it@14.1.1: resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} hasBin: true + marked-terminal@7.3.0: + resolution: {integrity: sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==} + engines: {node: '>=16.0.0'} + peerDependencies: + marked: '>=1 <16' + + marked@9.1.6: + resolution: {integrity: sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==} + engines: {node: '>= 16'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2048,10 +2164,9 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} @@ -2072,9 +2187,6 @@ packages: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} - mitt@3.0.1: - resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} - mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -2082,10 +2194,9 @@ packages: mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - mocha@10.8.2: - resolution: {integrity: sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==} - engines: {node: '>= 14.0.0'} - hasBin: true + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2101,23 +2212,19 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - netmask@2.1.1: - resolution: {integrity: sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==} - engines: {node: '>= 0.4.0'} - node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} deprecated: Use your platform's native DOMException instead + node-emoji@2.2.0: + resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} + engines: {node: '>=18'} + node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - nwsapi@2.2.23: resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} @@ -2144,14 +2251,21 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - opener@1.5.2: - resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} - hasBin: true + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + oxc-parser@0.127.0: + resolution: {integrity: sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-resolver@11.19.1: + resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -2160,24 +2274,24 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - pac-proxy-agent@7.2.0: - resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} - engines: {node: '>= 14'} - - pac-resolver@7.0.1: - resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} - engines: {node: '>= 14'} - package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} + parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + + parse5@5.1.1: + resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -2216,9 +2330,6 @@ packages: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} - pend@1.2.0: - resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2247,10 +2358,6 @@ packages: engines: {node: '>=18'} hasBin: true - portfinder@1.0.38: - resolution: {integrity: sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==} - engines: {node: '>= 10.12'} - possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -2290,22 +2397,13 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - progress@2.0.3: - resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} - engines: {node: '>=0.4.0'} - - proxy-agent@6.5.0: - resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} - engines: {node: '>= 14'} - - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - psl@1.15.0: resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} - pump@3.0.4: - resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + publint@0.3.18: + resolution: {integrity: sha512-JRJFeBTrfx4qLwEuGFPk+haJOJN97KnPuK01yj+4k/Wj5BgoOK5uNsivporiqBjk2JDaslg7qJOhGRnpltGeog==} + engines: {node: '>=18'} + hasBin: true punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} @@ -2315,29 +2413,12 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - puppeteer-core@23.11.1: - resolution: {integrity: sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==} - engines: {node: '>=18'} - - puppeteer@23.11.1: - resolution: {integrity: sha512-53uIX3KR5en8l7Vd8n5DUv90Ae9QDQsyIthaUFVzwV6yU750RjqRznEtNMBT20VthqAdemnJN+hxVdmMHKt7Zw==} - engines: {node: '>=18'} - deprecated: < 24.15.0 is no longer supported - hasBin: true - - qs@6.15.1: - resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} - engines: {node: '>=0.6'} - querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -2350,10 +2431,6 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -2377,15 +2454,25 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.12: resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} hasBin: true + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rimraf@2.7.1: resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -2414,11 +2501,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} safe-regex-test@1.1.0: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} @@ -2434,17 +2519,11 @@ packages: scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} - secure-compare@3.0.1: - resolution: {integrity: sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==} - semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} hasBin: true - serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -2484,30 +2563,30 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + skin-tone@2.0.0: + resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} + engines: {node: '>=8'} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - smart-buffer@4.2.0: - resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} - engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} - socks-proxy-agent@8.0.5: - resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} - engines: {node: '>= 14'} + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} - socks@2.8.7: - resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} - engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + engines: {node: '>= 18'} source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - source-map@0.7.6: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} @@ -2522,8 +2601,9 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - streamx@2.25.0: - resolution: {integrity: sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==} + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -2533,6 +2613,14 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.1: + resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} + engines: {node: '>=20'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -2545,6 +2633,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} @@ -2557,9 +2649,9 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} + supports-hyperlinks@3.2.0: + resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} + engines: {node: '>=14.18'} supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} @@ -2568,26 +2660,14 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - tar-fs@3.1.2: - resolution: {integrity: sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==} - - tar-stream@3.1.8: - resolution: {integrity: sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==} - tar@7.5.13: resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==} engines: {node: '>=18'} - teex@1.0.1: - resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} - test-exclude@7.0.2: resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} engines: {node: '>=18'} - text-decoder@1.2.7: - resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} - thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -2595,15 +2675,16 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + engines: {node: '>=18'} + tinyglobby@0.2.16: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} @@ -2645,20 +2726,6 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - ts-node@10.9.2: - resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} - hasBin: true - peerDependencies: - '@swc/core': '>=1.2.50' - '@swc/wasm': '>=1.2.50' - '@types/node': '*' - typescript: '>=2.7' - peerDependenciesMeta: - '@swc/core': - optional: true - '@swc/wasm': - optional: true - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -2685,9 +2752,6 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typed-query-selector@2.12.2: - resolution: {integrity: sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==} - typedoc-plugin-markdown@4.11.0: resolution: {integrity: sha512-2iunh2ALyfyh204OF7h2u0kuQ84xB3jFZtFyUr01nThJkLvR8oGGSSDlyt2gyO4kXhvUxDcVbO0y43+qX+wFbw==} engines: {node: '>= 18'} @@ -2708,6 +2772,11 @@ packages: eslint: 9.33.0 typescript: '>=4.8.4 <6.0.0' + typescript@5.6.1-rc: + resolution: {integrity: sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ==} + engines: {node: '>=14.17'} + hasBin: true + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -2719,8 +2788,9 @@ packages: ufo@1.6.3: resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} - unbzip2-stream@1.4.3: - resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + unbash@3.0.0: + resolution: {integrity: sha512-FeFPZ/WFT0mbRCuydiZzpPFlrYN8ZUpphQKoq4EeElVIYjYyGzPMxQR/simUwCOJIyVhpFk4RbtyO7RuMpMnHA==} + engines: {node: '>=14'} undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -2728,9 +2798,9 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - union@0.5.0: - resolution: {integrity: sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==} - engines: {node: '>= 0.8.0'} + unicode-emoji-modifier-base@1.0.0: + resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} + engines: {node: '>=4'} universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} @@ -2743,14 +2813,12 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - url-join@4.0.1: - resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} - url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} - v8-compile-cache-lib@3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + validate-npm-package-name@5.0.1: + resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} @@ -2820,6 +2888,10 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} @@ -2828,11 +2900,6 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} - whatwg-encoding@2.0.0: - resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} - engines: {node: '>=12'} - deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation - whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} @@ -2872,9 +2939,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - workerpool@6.5.1: - resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} - wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -2883,6 +2947,10 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -2922,35 +2990,16 @@ packages: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs-unparser@2.0.0: - resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} - engines: {node: '>=10'} - yargs@16.2.0: resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} engines: {node: '>=10'} - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - - yauzl@2.10.0: - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} - - yn@3.1.1: - resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} - engines: {node: '>=6'} - yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zod@3.23.8: - resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} zustand@5.0.12: resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==} @@ -2977,6 +3026,29 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 + '@andrewbranch/untar.js@1.0.3': {} + + '@arethetypeswrong/cli@0.18.2': + dependencies: + '@arethetypeswrong/core': 0.18.2 + chalk: 4.1.2 + cli-table3: 0.6.5 + commander: 10.0.1 + marked: 9.1.6 + marked-terminal: 7.3.0(marked@9.1.6) + semver: 7.7.4 + + '@arethetypeswrong/core@0.18.2': + dependencies: + '@andrewbranch/untar.js': 1.0.3 + '@loaderkit/resolve': 1.0.5 + cjs-module-lexer: 1.4.3 + fflate: 0.8.2 + lru-cache: 11.3.5 + semver: 7.7.4 + typescript: 5.6.1-rc + validate-npm-package-name: 5.0.1 + '@asamuzakjp/css-color@3.2.0': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) @@ -3008,9 +3080,10 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@cspotcode/source-map-support@0.8.1': - dependencies: - '@jridgewell/trace-mapping': 0.3.9 + '@braidai/lang@1.1.2': {} + + '@colors/colors@1.5.0': + optional: true '@csstools/color-helpers@5.1.0': {} @@ -3032,6 +3105,22 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.28.0': optional: true @@ -3110,9 +3199,9 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.33.0)': + '@eslint-community/eslint-utils@4.9.1(eslint@9.33.0(jiti@2.6.1))': dependencies: - eslint: 9.33.0 + eslint: 9.33.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -3120,7 +3209,7 @@ snapshots: '@eslint/config-array@0.21.2': dependencies: '@eslint/object-schema': 2.1.7 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 minimatch: 10.2.5 transitivePeerDependencies: - supports-color @@ -3134,7 +3223,7 @@ snapshots: '@eslint/eslintrc@3.3.5': dependencies: ajv: 6.15.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -3213,10 +3302,16 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping@0.3.9': + '@loaderkit/resolve@1.0.5': dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@braidai/lang': 1.1.2 + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true '@nodelib/fs.scandir@2.1.5': dependencies: @@ -3230,6 +3325,137 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@oxc-parser/binding-android-arm-eabi@0.127.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.127.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + optional: true + + '@oxc-project/types@0.127.0': {} + + '@oxc-resolver/binding-android-arm-eabi@11.19.1': + optional: true + + '@oxc-resolver/binding-android-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.19.1': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.19.1': + optional: true + + '@oxc-resolver/binding-win32-ia32-msvc@11.19.1': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.19.1': + optional: true + '@pkgjs/parseargs@0.11.0': optional: true @@ -3237,21 +3463,7 @@ snapshots: dependencies: playwright: 1.59.1 - '@puppeteer/browsers@2.6.1': - dependencies: - debug: 4.4.3(supports-color@8.1.1) - extract-zip: 2.0.1 - progress: 2.0.3 - proxy-agent: 6.5.0 - semver: 7.7.4 - tar-fs: 3.1.2 - unbzip2-stream: 1.4.3 - yargs: 17.7.2 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - - supports-color + '@publint/pack@0.1.4': {} '@rollup/plugin-commonjs@25.0.8(rollup@4.60.2)': dependencies: @@ -3274,15 +3486,6 @@ snapshots: optionalDependencies: rollup: 4.60.2 - '@rollup/plugin-typescript@12.3.0(rollup@4.60.2)(tslib@2.8.1)(typescript@5.9.3)': - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.60.2) - resolve: 1.22.12 - typescript: 5.9.3 - optionalDependencies: - rollup: 4.60.2 - tslib: 2.8.1 - '@rollup/pluginutils@5.3.0(rollup@4.60.2)': dependencies: '@types/estree': 1.0.8 @@ -3386,6 +3589,8 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@sindresorhus/is@4.6.0': {} + '@testing-library/dom@9.3.4': dependencies: '@babel/code-frame': 7.29.0 @@ -3407,15 +3612,10 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@tootallnate/quickjs-emscripten@0.23.0': {} - - '@tsconfig/node10@1.0.12': {} - - '@tsconfig/node12@1.0.11': {} - - '@tsconfig/node14@1.0.3': {} - - '@tsconfig/node16@1.0.4': {} + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true '@types/aria-query@5.0.4': {} @@ -3472,20 +3672,15 @@ snapshots: '@types/unist@3.0.3': {} - '@types/yauzl@2.10.3': - dependencies: - '@types/node': 24.12.2 - optional: true - - '@typescript-eslint/eslint-plugin@8.39.1(@typescript-eslint/parser@8.39.1(eslint@9.33.0)(typescript@5.9.3))(eslint@9.33.0)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.39.1(@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.39.1(eslint@9.33.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.39.1(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.39.1 - '@typescript-eslint/type-utils': 8.39.1(eslint@9.33.0)(typescript@5.9.3) - '@typescript-eslint/utils': 8.39.1(eslint@9.33.0)(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.39.1(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.39.1(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.39.1 - eslint: 9.33.0 + eslint: 9.33.0(jiti@2.6.1) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -3494,14 +3689,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.39.1(eslint@9.33.0)(typescript@5.9.3)': + '@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.39.1 '@typescript-eslint/types': 8.39.1 '@typescript-eslint/typescript-estree': 8.39.1(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.39.1 - debug: 4.4.3(supports-color@8.1.1) - eslint: 9.33.0 + debug: 4.4.3 + eslint: 9.33.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -3510,7 +3705,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.59.1(typescript@5.9.3) '@typescript-eslint/types': 8.59.1 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -3528,13 +3723,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.39.1(eslint@9.33.0)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.39.1(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.39.1 '@typescript-eslint/typescript-estree': 8.39.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.39.1(eslint@9.33.0)(typescript@5.9.3) - debug: 4.4.3(supports-color@8.1.1) - eslint: 9.33.0 + '@typescript-eslint/utils': 8.39.1(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.33.0(jiti@2.6.1) ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -3550,7 +3745,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.39.1(typescript@5.9.3) '@typescript-eslint/types': 8.39.1 '@typescript-eslint/visitor-keys': 8.39.1 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 10.2.5 @@ -3560,13 +3755,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.39.1(eslint@9.33.0)(typescript@5.9.3)': + '@typescript-eslint/utils@8.39.1(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.33.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.33.0(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.39.1 '@typescript-eslint/types': 8.39.1 '@typescript-eslint/typescript-estree': 8.39.1(typescript@5.9.3) - eslint: 9.33.0 + eslint: 9.33.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -3581,7 +3776,7 @@ snapshots: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 ast-v8-to-istanbul: 0.3.12 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 @@ -3600,7 +3795,7 @@ snapshots: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 ast-v8-to-istanbul: 0.3.12 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 @@ -3681,10 +3876,6 @@ snapshots: dependencies: acorn: 8.16.0 - acorn-walk@8.3.5: - dependencies: - acorn: 8.16.0 - acorn@8.16.0: {} agent-base@7.1.4: {} @@ -3696,7 +3887,9 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ansi-colors@4.1.3: {} + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 ansi-regex@5.0.1: {} @@ -3712,13 +3905,6 @@ snapshots: any-promise@1.3.0: {} - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.2 - - arg@4.1.3: {} - argparse@2.0.1: {} aria-query@5.1.3: @@ -3734,72 +3920,22 @@ snapshots: assertion-error@2.0.1: {} - ast-types@0.13.4: - dependencies: - tslib: 2.8.1 - ast-v8-to-istanbul@0.3.12: dependencies: '@jridgewell/trace-mapping': 0.3.31 estree-walker: 3.0.3 js-tokens: 10.0.0 - async@3.2.6: {} - asynckit@0.4.0: {} available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 - b4a@1.8.0: {} - balanced-match@1.0.2: {} balanced-match@4.0.4: {} - bare-events@2.8.2: {} - - bare-fs@4.7.1: - dependencies: - bare-events: 2.8.2 - bare-path: 3.0.0 - bare-stream: 2.13.0(bare-events@2.8.2) - bare-url: 2.4.2 - fast-fifo: 1.3.2 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - - bare-os@3.9.0: {} - - bare-path@3.0.0: - dependencies: - bare-os: 3.9.0 - - bare-stream@2.13.0(bare-events@2.8.2): - dependencies: - streamx: 2.25.0 - teex: 1.0.1 - optionalDependencies: - bare-events: 2.8.2 - transitivePeerDependencies: - - react-native-b4a - - bare-url@2.4.2: - dependencies: - bare-path: 3.0.0 - - base64-js@1.5.1: {} - - basic-auth@2.0.1: - dependencies: - safe-buffer: 5.1.2 - - basic-ftp@5.3.0: {} - - binary-extensions@2.3.0: {} - binaryen@129.0.0: {} brace-expansion@2.1.0: @@ -3814,15 +3950,6 @@ snapshots: dependencies: fill-range: 7.1.1 - browser-stdout@1.3.1: {} - - buffer-crc32@0.2.13: {} - - buffer@5.7.1: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - bundle-require@5.1.0(esbuild@0.28.0): dependencies: esbuild: 0.28.0 @@ -3849,8 +3976,6 @@ snapshots: callsites@3.1.0: {} - camelcase@6.3.0: {} - chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -3866,19 +3991,9 @@ snapshots: chalk@5.6.2: {} - check-error@2.1.3: {} + char-regex@1.0.2: {} - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 + check-error@2.1.3: {} chokidar@4.0.3: dependencies: @@ -3886,19 +4001,33 @@ snapshots: chownr@3.0.0: {} - chromium-bidi@0.11.0(devtools-protocol@0.0.1367902): + cjs-module-lexer@1.4.3: {} + + cli-cursor@5.0.0: dependencies: - devtools-protocol: 0.0.1367902 - mitt: 3.0.1 - zod: 3.23.8 + restore-cursor: 5.1.0 - cliui@7.0.4: + cli-highlight@2.1.11: + dependencies: + chalk: 4.1.2 + highlight.js: 10.7.3 + mz: 2.7.0 + parse5: 5.1.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + yargs: 16.2.0 + + cli-table3@0.6.5: dependencies: string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 + optionalDependencies: + '@colors/colors': 1.5.0 - cliui@8.0.1: + cli-truncate@5.2.0: + dependencies: + slice-ansi: 8.0.0 + string-width: 8.2.1 + + cliui@7.0.4: dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 @@ -3912,10 +4041,16 @@ snapshots: colorette@1.4.0: {} + colorette@2.0.20: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 + commander@10.0.1: {} + + commander@14.0.3: {} + commander@4.1.1: {} commondir@1.0.1: {} @@ -3924,17 +4059,6 @@ snapshots: consola@3.4.2: {} - corser@2.0.1: {} - - cosmiconfig@9.0.1(typescript@5.9.3): - dependencies: - env-paths: 2.2.1 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - parse-json: 5.2.0 - optionalDependencies: - typescript: 5.9.3 - cpr@3.0.1: dependencies: graceful-fs: 4.2.11 @@ -3942,8 +4066,6 @@ snapshots: mkdirp: 0.5.6 rimraf: 2.7.1 - create-require@1.1.1: {} - cross-env@7.0.3: dependencies: cross-spawn: 7.0.6 @@ -3963,20 +4085,14 @@ snapshots: data-uri-to-buffer@4.0.1: {} - data-uri-to-buffer@6.0.2: {} - data-urls@5.0.0: dependencies: whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - debug@4.4.3(supports-color@8.1.1): + debug@4.4.3: dependencies: ms: 2.1.3 - optionalDependencies: - supports-color: 8.1.1 - - decamelize@4.0.0: {} decimal.js@10.6.0: {} @@ -4017,24 +4133,12 @@ snapshots: dependencies: define-data-property: 1.1.4 has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - - degenerator@5.0.1: - dependencies: - ast-types: 0.13.4 - escodegen: 2.1.0 - esprima: 4.0.1 + object-keys: 1.1.1 delayed-stream@1.0.0: {} - devtools-protocol@0.0.1367902: {} - dexie@4.4.2: {} - diff@4.0.4: {} - - diff@5.2.2: {} - dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -4049,23 +4153,19 @@ snapshots: eastasianwidth@0.2.0: {} + emoji-regex@10.6.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} - end-of-stream@1.4.5: - dependencies: - once: 1.4.0 + emojilib@2.4.0: {} entities@4.5.0: {} entities@6.0.1: {} - env-paths@2.2.1: {} - - error-ex@1.3.4: - dependencies: - is-arrayish: 0.2.1 + environment@1.1.0: {} es-define-property@1.0.1: {} @@ -4129,13 +4229,9 @@ snapshots: escape-string-regexp@4.0.0: {} - escodegen@2.1.0: + eslint-config-prettier@10.1.8(eslint@9.33.0(jiti@2.6.1)): dependencies: - esprima: 4.0.1 - estraverse: 5.3.0 - esutils: 2.0.3 - optionalDependencies: - source-map: 0.6.1 + eslint: 9.33.0(jiti@2.6.1) eslint-scope@8.4.0: dependencies: @@ -4146,9 +4242,9 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.33.0: + eslint@9.33.0(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.33.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.33.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.21.2 '@eslint/config-helpers': 0.3.1 @@ -4164,7 +4260,7 @@ snapshots: ajv: 6.15.0 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -4183,19 +4279,17 @@ snapshots: minimatch: 10.2.5 natural-compare: 1.4.0 optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 transitivePeerDependencies: - supports-color - esm@3.2.25: {} - espree@10.4.0: dependencies: acorn: 8.16.0 acorn-jsx: 5.3.2(acorn@8.16.0) eslint-visitor-keys: 4.2.1 - esprima@4.0.1: {} - esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -4214,32 +4308,14 @@ snapshots: esutils@2.0.3: {} - eventemitter3@4.0.7: {} - - events-universal@1.0.1: - dependencies: - bare-events: 2.8.2 - transitivePeerDependencies: - - bare-abort-controller + eventemitter3@5.0.4: {} expect-type@1.3.0: {} - extract-zip@2.0.1: - dependencies: - debug: 4.4.3(supports-color@8.1.1) - get-stream: 5.2.0 - yauzl: 2.10.0 - optionalDependencies: - '@types/yauzl': 2.10.3 - transitivePeerDependencies: - - supports-color - fake-indexeddb@6.2.5: {} fast-deep-equal@3.1.3: {} - fast-fifo@1.3.2: {} - fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4256,9 +4332,9 @@ snapshots: dependencies: reusify: 1.1.0 - fd-slicer@1.1.0: + fd-package-json@2.0.0: dependencies: - pend: 1.2.0 + walk-up-path: 4.0.0 fdir@6.5.0(picomatch@4.0.4): optionalDependencies: @@ -4269,6 +4345,8 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 + fflate@0.8.2: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -4293,12 +4371,8 @@ snapshots: flatted: 3.4.2 keyv: 4.5.4 - flat@5.0.2: {} - flatted@3.4.2: {} - follow-redirects@1.16.0: {} - for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -4316,6 +4390,10 @@ snapshots: hasown: 2.0.3 mime-types: 2.1.35 + formatly@0.3.0: + dependencies: + fd-package-json: 2.0.0 + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -4340,6 +4418,8 @@ snapshots: get-caller-file@2.0.5: {} + get-east-asian-width@1.5.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4358,17 +4438,9 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - get-stream@5.2.0: - dependencies: - pump: 3.0.4 - - get-uri@6.0.5: + get-tsconfig@4.14.0: dependencies: - basic-ftp: 5.3.0 - data-uri-to-buffer: 6.0.2 - debug: 4.4.3(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color + resolve-pkg-maps: 1.0.0 glob-parent@5.1.2: dependencies: @@ -4456,11 +4528,7 @@ snapshots: dependencies: function-bind: 1.1.2 - he@1.2.0: {} - - html-encoding-sniffer@3.0.0: - dependencies: - whatwg-encoding: 2.0.0 + highlight.js@10.7.3: {} html-encoding-sniffer@4.0.0: dependencies: @@ -4471,41 +4539,14 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.3(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - - http-proxy@1.18.1: - dependencies: - eventemitter3: 4.0.7 - follow-redirects: 1.16.0 - requires-port: 1.0.0 - transitivePeerDependencies: - - debug - - http-server@14.1.1: - dependencies: - basic-auth: 2.0.1 - chalk: 4.1.2 - corser: 2.0.1 - he: 1.2.0 - html-encoding-sniffer: 3.0.0 - http-proxy: 1.18.1 - mime: 1.6.0 - minimist: 1.2.8 - opener: 1.5.2 - portfinder: 1.0.38 - secure-compare: 3.0.1 - union: 0.5.0 - url-join: 4.0.1 + debug: 4.4.3 transitivePeerDependencies: - - debug - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -4513,8 +4554,6 @@ snapshots: dependencies: safer-buffer: 2.1.2 - ieee754@1.2.1: {} - ignore@5.3.2: {} ignore@7.0.5: {} @@ -4539,8 +4578,6 @@ snapshots: hasown: 2.0.3 side-channel: 1.1.0 - ip-address@10.1.1: {} - is-arguments@1.2.0: dependencies: call-bound: 1.0.4 @@ -4552,16 +4589,10 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 - is-arrayish@0.2.1: {} - is-bigint@1.1.0: dependencies: has-bigints: 1.1.0 - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - is-boolean-object@1.2.2: dependencies: call-bound: 1.0.4 @@ -4582,6 +4613,10 @@ snapshots: is-fullwidth-code-point@3.0.0: {} + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.5.0 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -4597,8 +4632,6 @@ snapshots: is-number@7.0.0: {} - is-plain-obj@2.1.0: {} - is-plain-object@3.0.1: {} is-potential-custom-element-name@1.0.1: {} @@ -4631,8 +4664,6 @@ snapshots: has-symbols: 1.1.0 safe-regex-test: 1.1.0 - is-unicode-supported@0.1.0: {} - is-weakmap@2.0.2: {} is-weakset@2.0.4: @@ -4655,7 +4686,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.31 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -4675,6 +4706,8 @@ snapshots: dependencies: '@isaacs/cliui': 9.0.0 + jiti@2.6.1: {} + joycon@3.1.1: {} js-tokens@10.0.0: {} @@ -4717,8 +4750,6 @@ snapshots: json-buffer@3.0.1: {} - json-parse-even-better-errors@2.3.1: {} - json-schema-traverse@0.4.1: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -4731,6 +4762,69 @@ snapshots: dependencies: json-buffer: 3.0.1 + knip@6.7.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + formatly: 0.3.0 + get-tsconfig: 4.14.0 + jiti: 2.6.1 + minimist: 1.2.8 + oxc-parser: 0.127.0 + oxc-resolver: 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + picomatch: 4.0.4 + smol-toml: 1.6.1 + strip-json-comments: 5.0.3 + tinyglobby: 0.2.16 + unbash: 3.0.0 + yaml: 2.8.3 + zod: 4.3.6 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + lefthook-darwin-arm64@1.13.6: + optional: true + + lefthook-darwin-x64@1.13.6: + optional: true + + lefthook-freebsd-arm64@1.13.6: + optional: true + + lefthook-freebsd-x64@1.13.6: + optional: true + + lefthook-linux-arm64@1.13.6: + optional: true + + lefthook-linux-x64@1.13.6: + optional: true + + lefthook-openbsd-arm64@1.13.6: + optional: true + + lefthook-openbsd-x64@1.13.6: + optional: true + + lefthook-windows-arm64@1.13.6: + optional: true + + lefthook-windows-x64@1.13.6: + optional: true + + lefthook@1.13.6: + optionalDependencies: + lefthook-darwin-arm64: 1.13.6 + lefthook-darwin-x64: 1.13.6 + lefthook-freebsd-arm64: 1.13.6 + lefthook-freebsd-x64: 1.13.6 + lefthook-linux-arm64: 1.13.6 + lefthook-linux-x64: 1.13.6 + lefthook-openbsd-arm64: 1.13.6 + lefthook-openbsd-x64: 1.13.6 + lefthook-windows-arm64: 1.13.6 + lefthook-windows-x64: 1.13.6 + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -4744,6 +4838,24 @@ snapshots: dependencies: uc.micro: 2.1.0 + lint-staged@16.4.0: + dependencies: + commander: 14.0.3 + listr2: 9.0.5 + picomatch: 4.0.4 + string-argv: 0.3.2 + tinyexec: 1.1.1 + yaml: 2.8.3 + + listr2@9.0.5: + dependencies: + cli-truncate: 5.2.0 + colorette: 2.0.20 + eventemitter3: 5.0.4 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.2 + load-tsconfig@0.2.5: {} locate-path@6.0.0: @@ -4752,10 +4864,13 @@ snapshots: lodash.merge@4.6.2: {} - log-symbols@4.1.0: + log-update@6.1.0: dependencies: - chalk: 4.1.2 - is-unicode-supported: 0.1.0 + ansi-escapes: 7.3.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 loose-envify@1.4.0: dependencies: @@ -4767,8 +4882,6 @@ snapshots: lru-cache@11.3.5: {} - lru-cache@7.18.3: {} - lunr@2.3.9: {} lz-string@1.5.0: {} @@ -4787,8 +4900,6 @@ snapshots: dependencies: semver: 7.7.4 - make-error@1.3.6: {} - markdown-it@14.1.1: dependencies: argparse: 2.0.1 @@ -4798,6 +4909,19 @@ snapshots: punycode.js: 2.3.1 uc.micro: 2.1.0 + marked-terminal@7.3.0(marked@9.1.6): + dependencies: + ansi-escapes: 7.3.0 + ansi-regex: 6.2.2 + chalk: 5.6.2 + cli-highlight: 2.1.11 + cli-table3: 0.6.5 + marked: 9.1.6 + node-emoji: 2.2.0 + supports-hyperlinks: 3.2.0 + + marked@9.1.6: {} + math-intrinsics@1.1.0: {} mdurl@2.0.0: {} @@ -4815,7 +4939,7 @@ snapshots: dependencies: mime-db: 1.52.0 - mime@1.6.0: {} + mimic-function@5.0.1: {} minimatch@10.2.5: dependencies: @@ -4833,8 +4957,6 @@ snapshots: dependencies: minipass: 7.1.3 - mitt@3.0.1: {} - mkdirp@0.5.6: dependencies: minimist: 1.2.8 @@ -4846,28 +4968,7 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.3 - mocha@10.8.2: - dependencies: - ansi-colors: 4.1.3 - browser-stdout: 1.3.1 - chokidar: 3.6.0 - debug: 4.4.3(supports-color@8.1.1) - diff: 5.2.2 - escape-string-regexp: 4.0.0 - find-up: 5.0.0 - glob: 8.1.0 - he: 1.2.0 - js-yaml: 4.1.1 - log-symbols: 4.1.0 - minimatch: 10.2.5 - ms: 2.1.3 - serialize-javascript: 6.0.2 - strip-json-comments: 3.1.1 - supports-color: 8.1.1 - workerpool: 6.5.1 - yargs: 16.2.0 - yargs-parser: 20.2.9 - yargs-unparser: 2.0.0 + mri@1.2.0: {} ms@2.1.3: {} @@ -4881,18 +4982,21 @@ snapshots: natural-compare@1.4.0: {} - netmask@2.1.1: {} - node-domexception@1.0.0: {} + node-emoji@2.2.0: + dependencies: + '@sindresorhus/is': 4.6.0 + char-regex: 1.0.2 + emojilib: 2.4.0 + skin-tone: 2.0.0 + node-fetch@3.3.2: dependencies: data-uri-to-buffer: 4.0.1 fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - normalize-path@3.0.0: {} - nwsapi@2.2.23: {} object-assign@4.1.1: {} @@ -4919,7 +5023,9 @@ snapshots: dependencies: wrappy: 1.0.2 - opener@1.5.2: {} + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 optionator@0.9.4: dependencies: @@ -4930,6 +5036,57 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + oxc-parser@0.127.0: + dependencies: + '@oxc-project/types': 0.127.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.127.0 + '@oxc-parser/binding-android-arm64': 0.127.0 + '@oxc-parser/binding-darwin-arm64': 0.127.0 + '@oxc-parser/binding-darwin-x64': 0.127.0 + '@oxc-parser/binding-freebsd-x64': 0.127.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.127.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.127.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.127.0 + '@oxc-parser/binding-linux-arm64-musl': 0.127.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.127.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-musl': 0.127.0 + '@oxc-parser/binding-openharmony-arm64': 0.127.0 + '@oxc-parser/binding-wasm32-wasi': 0.127.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.127.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.127.0 + '@oxc-parser/binding-win32-x64-msvc': 0.127.0 + + oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.19.1 + '@oxc-resolver/binding-android-arm64': 11.19.1 + '@oxc-resolver/binding-darwin-arm64': 11.19.1 + '@oxc-resolver/binding-darwin-x64': 11.19.1 + '@oxc-resolver/binding-freebsd-x64': 11.19.1 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.19.1 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.19.1 + '@oxc-resolver/binding-linux-arm64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-arm64-musl': 11.19.1 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-riscv64-musl': 11.19.1 + '@oxc-resolver/binding-linux-s390x-gnu': 11.19.1 + '@oxc-resolver/binding-linux-x64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-x64-musl': 11.19.1 + '@oxc-resolver/binding-openharmony-arm64': 11.19.1 + '@oxc-resolver/binding-wasm32-wasi': 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@oxc-resolver/binding-win32-arm64-msvc': 11.19.1 + '@oxc-resolver/binding-win32-ia32-msvc': 11.19.1 + '@oxc-resolver/binding-win32-x64-msvc': 11.19.1 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -4938,36 +5095,21 @@ snapshots: dependencies: p-limit: 3.1.0 - pac-proxy-agent@7.2.0: - dependencies: - '@tootallnate/quickjs-emscripten': 0.23.0 - agent-base: 7.1.4 - debug: 4.4.3(supports-color@8.1.1) - get-uri: 6.0.5 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - pac-resolver: 7.0.1 - socks-proxy-agent: 8.0.5 - transitivePeerDependencies: - - supports-color - - pac-resolver@7.0.1: - dependencies: - degenerator: 5.0.1 - netmask: 2.1.1 - package-json-from-dist@1.0.1: {} + package-manager-detector@1.6.0: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 - parse-json@5.2.0: + parse5-htmlparser2-tree-adapter@6.0.1: dependencies: - '@babel/code-frame': 7.29.0 - error-ex: 1.3.4 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 + parse5: 6.0.1 + + parse5@5.1.1: {} + + parse5@6.0.1: {} parse5@7.3.0: dependencies: @@ -4997,8 +5139,6 @@ snapshots: pathval@2.0.1: {} - pend@1.2.0: {} - picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -5021,19 +5161,13 @@ snapshots: optionalDependencies: fsevents: 2.3.2 - portfinder@1.0.38: - dependencies: - async: 3.2.6 - debug: 4.4.3(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - possible-typed-array-names@1.1.0: {} - postcss-load-config@6.0.1(postcss@8.5.12)(yaml@2.8.3): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.12)(yaml@2.8.3): dependencies: lilconfig: 3.1.3 optionalDependencies: + jiti: 2.6.1 postcss: 8.5.12 yaml: 2.8.3 @@ -5053,81 +5187,25 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 - progress@2.0.3: {} - - proxy-agent@6.5.0: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3(supports-color@8.1.1) - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - lru-cache: 7.18.3 - pac-proxy-agent: 7.2.0 - proxy-from-env: 1.1.0 - socks-proxy-agent: 8.0.5 - transitivePeerDependencies: - - supports-color - - proxy-from-env@1.1.0: {} - psl@1.15.0: dependencies: punycode: 2.3.1 - pump@3.0.4: + publint@0.3.18: dependencies: - end-of-stream: 1.4.5 - once: 1.4.0 + '@publint/pack': 0.1.4 + package-manager-detector: 1.6.0 + picocolors: 1.1.1 + sade: 1.8.1 punycode.js@2.3.1: {} punycode@2.3.1: {} - puppeteer-core@23.11.1: - dependencies: - '@puppeteer/browsers': 2.6.1 - chromium-bidi: 0.11.0(devtools-protocol@0.0.1367902) - debug: 4.4.3(supports-color@8.1.1) - devtools-protocol: 0.0.1367902 - typed-query-selector: 2.12.2 - ws: 8.20.0 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - bufferutil - - react-native-b4a - - supports-color - - utf-8-validate - - puppeteer@23.11.1(typescript@5.9.3): - dependencies: - '@puppeteer/browsers': 2.6.1 - chromium-bidi: 0.11.0(devtools-protocol@0.0.1367902) - cosmiconfig: 9.0.1(typescript@5.9.3) - devtools-protocol: 0.0.1367902 - puppeteer-core: 23.11.1 - typed-query-selector: 2.12.2 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - bufferutil - - react-native-b4a - - supports-color - - typescript - - utf-8-validate - - qs@6.15.1: - dependencies: - side-channel: 1.1.0 - querystringify@2.2.0: {} queue-microtask@1.2.3: {} - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 @@ -5140,10 +5218,6 @@ snapshots: dependencies: loose-envify: 1.4.0 - readdirp@3.6.0: - dependencies: - picomatch: 2.3.2 - readdirp@4.1.2: {} regexp.prototype.flags@1.5.4: @@ -5163,6 +5237,8 @@ snapshots: resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve@1.22.12: dependencies: es-errors: 1.3.0 @@ -5170,8 +5246,15 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + reusify@1.1.0: {} + rfdc@1.4.1: {} + rimraf@2.7.1: dependencies: glob: 7.2.3 @@ -5228,9 +5311,9 @@ snapshots: dependencies: queue-microtask: 1.2.3 - safe-buffer@5.1.2: {} - - safe-buffer@5.2.1: {} + sade@1.8.1: + dependencies: + mri: 1.2.0 safe-regex-test@1.1.0: dependencies: @@ -5248,14 +5331,8 @@ snapshots: dependencies: loose-envify: 1.4.0 - secure-compare@3.0.1: {} - semver@7.7.4: {} - serialize-javascript@6.0.2: - dependencies: - randombytes: 2.1.0 - set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -5310,27 +5387,25 @@ snapshots: signal-exit@4.1.0: {} - slash@3.0.0: {} + skin-tone@2.0.0: + dependencies: + unicode-emoji-modifier-base: 1.0.0 - smart-buffer@4.2.0: {} + slash@3.0.0: {} - socks-proxy-agent@8.0.5: + slice-ansi@7.1.2: dependencies: - agent-base: 7.1.4 - debug: 4.4.3(supports-color@8.1.1) - socks: 2.8.7 - transitivePeerDependencies: - - supports-color + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 - socks@2.8.7: + slice-ansi@8.0.0: dependencies: - ip-address: 10.1.1 - smart-buffer: 4.2.0 + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 - source-map-js@1.2.1: {} + smol-toml@1.6.1: {} - source-map@0.6.1: - optional: true + source-map-js@1.2.1: {} source-map@0.7.6: {} @@ -5343,14 +5418,7 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - streamx@2.25.0: - dependencies: - events-universal: 1.0.1 - fast-fifo: 1.3.2 - text-decoder: 1.2.7 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a + string-argv@0.3.2: {} string-width@4.2.3: dependencies: @@ -5364,6 +5432,17 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.2.0 + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + + string-width@8.2.1: + dependencies: + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -5374,6 +5453,8 @@ snapshots: strip-json-comments@3.1.1: {} + strip-json-comments@5.0.3: {} + strip-literal@3.1.0: dependencies: js-tokens: 9.0.1 @@ -5392,37 +5473,15 @@ snapshots: dependencies: has-flag: 4.0.0 - supports-color@8.1.1: + supports-hyperlinks@3.2.0: dependencies: has-flag: 4.0.0 + supports-color: 7.2.0 supports-preserve-symlinks-flag@1.0.0: {} symbol-tree@3.2.4: {} - tar-fs@3.1.2: - dependencies: - pump: 3.0.4 - tar-stream: 3.1.8 - optionalDependencies: - bare-fs: 4.7.1 - bare-path: 3.0.0 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - - tar-stream@3.1.8: - dependencies: - b4a: 1.8.0 - bare-fs: 4.7.1 - fast-fifo: 1.3.2 - streamx: 2.25.0 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - tar@7.5.13: dependencies: '@isaacs/fs-minipass': 4.0.1 @@ -5431,25 +5490,12 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - teex@1.0.1: - dependencies: - streamx: 2.25.0 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - test-exclude@7.0.2: dependencies: '@istanbuljs/schema': 0.1.6 glob: 10.5.0 minimatch: 9.0.9 - text-decoder@1.2.7: - dependencies: - b4a: 1.8.0 - transitivePeerDependencies: - - react-native-b4a - thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -5458,12 +5504,12 @@ snapshots: dependencies: any-promise: 1.3.0 - through@2.3.8: {} - tinybench@2.9.0: {} tinyexec@0.3.2: {} + tinyexec@1.1.1: {} + tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -5498,38 +5544,21 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-node@10.9.2(@types/node@24.12.2)(typescript@5.9.3): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.12 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 24.12.2 - acorn: 8.16.0 - acorn-walk: 8.3.5 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.4 - make-error: 1.3.6 - typescript: 5.9.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - - tslib@2.8.1: {} + tslib@2.8.1: + optional: true - tsup@8.5.1(postcss@8.5.12)(typescript@5.9.3)(yaml@2.8.3): + tsup@8.5.1(jiti@2.6.1)(postcss@8.5.12)(typescript@5.9.3)(yaml@2.8.3): dependencies: bundle-require: 5.1.0(esbuild@0.28.0) cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 esbuild: 0.28.0 fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(postcss@8.5.12)(yaml@2.8.3) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.12)(yaml@2.8.3) resolve-from: 5.0.0 rollup: 4.60.2 source-map: 0.7.6 @@ -5550,8 +5579,6 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typed-query-selector@2.12.2: {} - typedoc-plugin-markdown@4.11.0(typedoc@0.28.19(typescript@5.9.3)): dependencies: typedoc: 0.28.19(typescript@5.9.3) @@ -5565,35 +5592,32 @@ snapshots: typescript: 5.9.3 yaml: 2.8.3 - typescript-eslint@8.39.1(eslint@9.33.0)(typescript@5.9.3): + typescript-eslint@8.39.1(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.39.1(@typescript-eslint/parser@8.39.1(eslint@9.33.0)(typescript@5.9.3))(eslint@9.33.0)(typescript@5.9.3) - '@typescript-eslint/parser': 8.39.1(eslint@9.33.0)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.39.1(@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.39.1(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.39.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.39.1(eslint@9.33.0)(typescript@5.9.3) - eslint: 9.33.0 + '@typescript-eslint/utils': 8.39.1(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.33.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color + typescript@5.6.1-rc: {} + typescript@5.9.3: {} uc.micro@2.1.0: {} ufo@1.6.3: {} - unbzip2-stream@1.4.3: - dependencies: - buffer: 5.7.1 - through: 2.3.8 + unbash@3.0.0: {} undici-types@6.21.0: {} undici-types@7.16.0: {} - union@0.5.0: - dependencies: - qs: 6.15.1 + unicode-emoji-modifier-base@1.0.0: {} universalify@0.1.2: {} @@ -5603,19 +5627,17 @@ snapshots: dependencies: punycode: 2.3.1 - url-join@4.0.1: {} - url-parse@1.5.10: dependencies: querystringify: 2.2.0 requires-port: 1.0.0 - v8-compile-cache-lib@3.0.1: {} + validate-npm-package-name@5.0.1: {} vite-node@3.2.4(@types/node@20.19.39): dependencies: cac: 6.7.14 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 vite: 5.4.21(@types/node@20.19.39) @@ -5633,7 +5655,7 @@ snapshots: vite-node@3.2.4(@types/node@24.12.2): dependencies: cac: 6.7.14 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 vite: 5.4.21(@types/node@24.12.2) @@ -5677,7 +5699,7 @@ snapshots: '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.3.3 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 expect-type: 1.3.0 magic-string: 0.30.21 pathe: 2.0.3 @@ -5716,7 +5738,7 @@ snapshots: '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.3.3 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 expect-type: 1.3.0 magic-string: 0.30.21 pathe: 2.0.3 @@ -5748,14 +5770,12 @@ snapshots: dependencies: xml-name-validator: 5.0.0 + walk-up-path@4.0.0: {} + web-streams-polyfill@3.3.3: {} webidl-conversions@7.0.0: {} - whatwg-encoding@2.0.0: - dependencies: - iconv-lite: 0.6.3 - whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 @@ -5803,8 +5823,6 @@ snapshots: word-wrap@1.2.5: {} - workerpool@6.5.1: {} - wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -5817,6 +5835,12 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.2.0 + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrappy@1.0.2: {} ws@8.20.0: {} @@ -5833,15 +5857,6 @@ snapshots: yargs-parser@20.2.9: {} - yargs-parser@21.1.1: {} - - yargs-unparser@2.0.0: - dependencies: - camelcase: 6.3.0 - decamelize: 4.0.0 - flat: 5.0.2 - is-plain-obj: 2.1.0 - yargs@16.2.0: dependencies: cliui: 7.0.4 @@ -5852,26 +5867,9 @@ snapshots: y18n: 5.0.8 yargs-parser: 20.2.9 - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - - yauzl@2.10.0: - dependencies: - buffer-crc32: 0.2.13 - fd-slicer: 1.1.0 - - yn@3.1.1: {} - yocto-queue@0.1.0: {} - zod@3.23.8: {} + zod@4.3.6: {} zustand@5.0.12(@types/react@18.3.28)(react@18.3.1): optionalDependencies: diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..985e31b --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from "vitest/config"; + +// Vitest 3 modern shape: `test.projects` supersedes the older +// `defineWorkspace` API. Each entry points to that package's +// existing vitest config — the per-package config remains the +// source of truth for environment, includes, coverage thresholds, +// etc. Running `vitest` from the repo root aggregates them. +// +// `crates/web-client` ships its vitest config as `.js` (not `.ts`), +// so the path reflects that. +export default defineConfig({ + test: { + projects: [ + "./packages/react-sdk/vitest.config.ts", + "./packages/vite-plugin/vitest.config.ts", + "./crates/web-client/vitest.config.js", + ], + }, +});