From 22128e8d1d9b3153924aff35c65a9c6b930c7b07 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:09:20 +0000 Subject: [PATCH 1/3] Initial plan From 4194a8ad7669443274d6fe4dcef5292861f946cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:25:50 +0000 Subject: [PATCH 2/3] feat: CI hardening, type safety, DX improvements across all 11 fixes Co-authored-by: numbers-official <181934381+numbers-official@users.noreply.github.com> --- .github/workflows/ci.yml | 17 +- .github/workflows/release.yml | 6 +- python/numbersprotocol_capture/client.py | 26 +- python/numbersprotocol_capture/types.py | 21 +- python/pyproject.toml | 10 +- python/tests/test_client_api.py | 349 ++++++++ python/tests/test_verify.py | 48 + scripts/check-feature-parity.py | 7 +- scripts/sync-versions.py | 23 +- ts/package-lock.json | 1025 ++++++++++++++++++++-- ts/package.json | 2 + ts/src/client.test.ts | 192 ++++ ts/src/client.ts | 16 +- ts/src/crypto.test.ts | 73 ++ ts/src/errors.test.ts | 182 ++++ ts/src/types.ts | 21 +- ts/vitest.config.ts | 17 + 17 files changed, 1935 insertions(+), 100 deletions(-) create mode 100644 python/tests/test_client_api.py create mode 100644 python/tests/test_verify.py create mode 100644 ts/src/crypto.test.ts create mode 100644 ts/src/errors.test.ts create mode 100644 ts/vitest.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13d4dbb..8438580 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.14" + python-version: "3.13" - name: Check version sync run: python scripts/sync-versions.py --check @@ -32,15 +32,18 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.14" + python-version: "3.13" - name: Check feature parity run: python scripts/check-feature-parity.py # TypeScript CI typescript: - name: TypeScript + name: TypeScript (Node ${{ matrix.node-version }}) runs-on: ubuntu-latest + strategy: + matrix: + node-version: ["18", "20", "22"] defaults: run: working-directory: ts @@ -50,7 +53,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: "20" + node-version: ${{ matrix.node-version }} cache: "npm" cache-dependency-path: ts/package-lock.json @@ -64,7 +67,7 @@ jobs: run: npm run build - name: Run tests - run: npm test + run: npm run test:coverage # Python CI python: @@ -72,7 +75,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.14"] + python-version: ["3.11", "3.12", "3.13"] defaults: run: working-directory: python @@ -96,7 +99,7 @@ jobs: run: mypy numbersprotocol_capture --ignore-missing-imports - name: Run tests - run: pytest -v + run: pytest -v --cov-fail-under=80 # All checks passed ci-success: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8defd7c..9ea93f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.14" + python-version: "3.13" - name: Extract version from tag id: version @@ -92,7 +92,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.14" + python-version: "3.13" - name: Install build dependencies run: | @@ -119,7 +119,7 @@ jobs: - uses: actions/checkout@v4 - name: Create Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: name: v${{ needs.validate.outputs.version }} body: | diff --git a/python/numbersprotocol_capture/client.py b/python/numbersprotocol_capture/client.py index 529391a..8c39fa1 100644 --- a/python/numbersprotocol_capture/client.py +++ b/python/numbersprotocol_capture/client.py @@ -134,6 +134,10 @@ def __init__( *, testnet: bool = False, base_url: str | None = None, + history_api_url: str | None = None, + merge_tree_api_url: str | None = None, + asset_search_api_url: str | None = None, + nft_search_api_url: str | None = None, options: CaptureOptions | None = None, ): """ @@ -143,12 +147,20 @@ def __init__( token: Authentication token for API access. testnet: Use testnet environment (default: False). base_url: Custom base URL (overrides testnet setting). + history_api_url: Override URL for the history API endpoint. + merge_tree_api_url: Override URL for the merge-tree API endpoint. + asset_search_api_url: Override URL for the asset search API endpoint. + nft_search_api_url: Override URL for the NFT search API endpoint. options: CaptureOptions object (alternative to individual args). """ if options: token = options.token testnet = options.testnet base_url = options.base_url + history_api_url = options.history_api_url + merge_tree_api_url = options.merge_tree_api_url + asset_search_api_url = options.asset_search_api_url + nft_search_api_url = options.nft_search_api_url if not token: raise ValidationError("token is required") @@ -156,6 +168,10 @@ def __init__( self._token = token self._testnet = testnet self._base_url = base_url or DEFAULT_BASE_URL + self._history_api_url = history_api_url or HISTORY_API_URL + self._merge_tree_api_url = merge_tree_api_url or MERGE_TREE_API_URL + self._asset_search_api_url = asset_search_api_url or ASSET_SEARCH_API_URL + self._nft_search_api_url = nft_search_api_url or NFT_SEARCH_API_URL self._client = httpx.Client(timeout=30.0) def __enter__(self) -> Capture: @@ -444,7 +460,7 @@ def get_history(self, nid: str) -> list[Commit]: if self._testnet: params["testnet"] = "true" - url = f"{HISTORY_API_URL}?{urlencode(params)}" + url = f"{self._history_api_url}?{urlencode(params)}" headers = { "Content-Type": "application/json", @@ -518,7 +534,7 @@ def get_asset_tree(self, nid: str) -> AssetTree: try: response = self._client.post( - MERGE_TREE_API_URL, + self._merge_tree_api_url, headers=headers, json=commit_data, ) @@ -680,14 +696,14 @@ def search_asset( try: if files_data: response = self._client.post( - ASSET_SEARCH_API_URL, + self._asset_search_api_url, headers=headers, data=form_data, files=files_data, ) else: response = self._client.post( - ASSET_SEARCH_API_URL, + self._asset_search_api_url, headers=headers, data=form_data, ) @@ -747,7 +763,7 @@ def search_nft(self, nid: str) -> NftSearchResult: try: response = self._client.post( - NFT_SEARCH_API_URL, + self._nft_search_api_url, headers=headers, json={"nid": nid}, ) diff --git a/python/numbersprotocol_capture/types.py b/python/numbersprotocol_capture/types.py index 0eff64d..c67e4d9 100644 --- a/python/numbersprotocol_capture/types.py +++ b/python/numbersprotocol_capture/types.py @@ -32,6 +32,18 @@ class CaptureOptions: base_url: str | None = None """Custom base URL (overrides testnet setting).""" + history_api_url: str | None = None + """Override URL for the history API endpoint.""" + + merge_tree_api_url: str | None = None + """Override URL for the merge-tree API endpoint.""" + + asset_search_api_url: str | None = None + """Override URL for the asset search API endpoint.""" + + nft_search_api_url: str | None = None + """Override URL for the NFT search API endpoint.""" + @dataclass class SignOptions: @@ -115,7 +127,10 @@ class Commit: """Address that made this commit.""" timestamp: int - """Unix timestamp of the commit.""" + """Unix timestamp of the commit in **seconds**. + + Note: ``IntegrityProof.created_at`` uses milliseconds; this field uses seconds. + """ action: str """Description of the action.""" @@ -203,6 +218,10 @@ class IntegrityProof: proof_hash: str asset_mime_type: str created_at: int + """Creation timestamp in **milliseconds** since Unix epoch. + + Note: ``Commit.timestamp`` from the API uses seconds; this field uses milliseconds. + """ @dataclass diff --git a/python/pyproject.toml b/python/pyproject.toml index 5c4a629..ee0b47e 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -8,7 +8,7 @@ version = "0.2.1" description = "Python SDK for Numbers Protocol Capture API" readme = "README.md" license = "MIT" -requires-python = ">=3.14" +requires-python = ">=3.11" authors = [ { name = "Numbers Protocol" } ] @@ -26,7 +26,9 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed", ] @@ -61,7 +63,7 @@ packages = ["numbersprotocol_capture"] [tool.ruff] line-length = 100 -target-version = "py313" # Use py313 until ruff supports py314 +target-version = "py311" [tool.ruff.lint] select = [ @@ -81,7 +83,7 @@ ignore = [ known-first-party = ["numbersprotocol_capture"] [tool.mypy] -python_version = "3.14" +python_version = "3.11" strict = true warn_return_any = true warn_unused_ignores = true diff --git a/python/tests/test_client_api.py b/python/tests/test_client_api.py new file mode 100644 index 0000000..bfbd690 --- /dev/null +++ b/python/tests/test_client_api.py @@ -0,0 +1,349 @@ +""" +Tests for Capture client API methods: get, update, get_history, search_nft, +URL overrides, and context manager. +""" + +import pytest +import respx +from httpx import Response + +from numbersprotocol_capture import Capture, ValidationError +from numbersprotocol_capture.client import ( + DEFAULT_BASE_URL, + HISTORY_API_URL, + NFT_SEARCH_API_URL, +) +from numbersprotocol_capture.types import CaptureOptions + +TEST_NID = "bafybeif3mhxhkhfwuszl2lybtai3hz3q6naqpfisd4q55mcc7opkmiv5ei" + +MOCK_ASSET_RESPONSE = { + "id": TEST_NID, + "asset_file_name": "photo.jpg", + "asset_file_mime_type": "image/jpeg", + "caption": "My photo", + "headline": "Demo", +} + +MOCK_HISTORY_RESPONSE = { + "nid": TEST_NID, + "commits": [ + { + "assetTreeCid": "bafy_tree_cid", + "txHash": "0xabc", + "author": "0xauthor", + "committer": "0xcommitter", + "timestampCreated": 1700000000, + "action": "initial-commit", + } + ], +} + + +class TestGetAsset: + """Tests for the get() method.""" + + @respx.mock + def test_get_returns_asset(self) -> None: + respx.get(f"{DEFAULT_BASE_URL}/assets/{TEST_NID}/").mock( + return_value=Response(200, json=MOCK_ASSET_RESPONSE) + ) + + with Capture(token="test-token") as capture: + asset = capture.get(TEST_NID) + + assert asset.nid == TEST_NID + assert asset.filename == "photo.jpg" + assert asset.mime_type == "image/jpeg" + assert asset.caption == "My photo" + assert asset.headline == "Demo" + + def test_get_empty_nid_raises(self) -> None: + with Capture(token="test-token") as capture: + with pytest.raises(ValidationError, match="nid is required"): + capture.get("") + + @respx.mock + def test_get_not_found_raises(self) -> None: + from numbersprotocol_capture import NotFoundError + + respx.get(f"{DEFAULT_BASE_URL}/assets/{TEST_NID}/").mock( + return_value=Response(404, json={"detail": "Not found"}) + ) + + with Capture(token="test-token") as capture: + with pytest.raises(NotFoundError): + capture.get(TEST_NID) + + @respx.mock + def test_get_sends_auth_header(self) -> None: + route = respx.get(f"{DEFAULT_BASE_URL}/assets/{TEST_NID}/").mock( + return_value=Response(200, json=MOCK_ASSET_RESPONSE) + ) + + with Capture(token="secret-token") as capture: + capture.get(TEST_NID) + + request = route.calls.last.request + assert request.headers["authorization"] == "token secret-token" + + +class TestUpdateAsset: + """Tests for the update() method.""" + + @respx.mock + def test_update_returns_asset(self) -> None: + updated = {**MOCK_ASSET_RESPONSE, "caption": "Updated caption"} + respx.patch(f"{DEFAULT_BASE_URL}/assets/{TEST_NID}/").mock( + return_value=Response(200, json=updated) + ) + + with Capture(token="test-token") as capture: + asset = capture.update(TEST_NID, caption="Updated caption") + + assert asset.nid == TEST_NID + assert asset.caption == "Updated caption" + + def test_update_empty_nid_raises(self) -> None: + with Capture(token="test-token") as capture: + with pytest.raises(ValidationError, match="nid is required"): + capture.update("", caption="test") + + def test_update_long_headline_raises(self) -> None: + with Capture(token="test-token") as capture: + with pytest.raises(ValidationError, match="headline must be 25 characters or less"): + capture.update(TEST_NID, headline="a" * 26) + + @respx.mock + def test_update_with_custom_metadata(self) -> None: + respx.patch(f"{DEFAULT_BASE_URL}/assets/{TEST_NID}/").mock( + return_value=Response(200, json=MOCK_ASSET_RESPONSE) + ) + + with Capture(token="test-token") as capture: + asset = capture.update( + TEST_NID, + caption="test", + custom_metadata={"key": "value"}, + ) + + assert asset.nid == TEST_NID + + +class TestGetHistory: + """Tests for the get_history() method.""" + + @respx.mock + def test_get_history_returns_commits(self) -> None: + respx.get(HISTORY_API_URL).mock( + return_value=Response(200, json=MOCK_HISTORY_RESPONSE) + ) + + with Capture(token="test-token") as capture: + commits = capture.get_history(TEST_NID) + + assert len(commits) == 1 + commit = commits[0] + assert commit.asset_tree_cid == "bafy_tree_cid" + assert commit.tx_hash == "0xabc" + assert commit.author == "0xauthor" + assert commit.committer == "0xcommitter" + assert commit.timestamp == 1700000000 + assert commit.action == "initial-commit" + + def test_get_history_empty_nid_raises(self) -> None: + with Capture(token="test-token") as capture: + with pytest.raises(ValidationError, match="nid is required"): + capture.get_history("") + + @respx.mock + def test_get_history_with_testnet(self) -> None: + route = respx.get(HISTORY_API_URL).mock( + return_value=Response(200, json=MOCK_HISTORY_RESPONSE) + ) + + with Capture(token="test-token", testnet=True) as capture: + capture.get_history(TEST_NID) + + request_url = str(route.calls.last.request.url) + assert "testnet=true" in request_url + + +class TestSearchNft: + """Tests for the search_nft() method.""" + + @respx.mock + def test_search_nft_returns_records(self) -> None: + respx.post(NFT_SEARCH_API_URL).mock( + return_value=Response( + 200, + json={ + "records": [ + { + "token_id": "42", + "contract": "0xcontract", + "network": "polygon", + "owner": "0xowner", + } + ], + "order_id": "nft_order_1", + }, + ) + ) + + with Capture(token="test-token") as capture: + result = capture.search_nft(TEST_NID) + + assert len(result.records) == 1 + assert result.records[0].token_id == "42" + assert result.records[0].contract == "0xcontract" + assert result.records[0].network == "polygon" + assert result.records[0].owner == "0xowner" + assert result.order_id == "nft_order_1" + + def test_search_nft_empty_nid_raises(self) -> None: + with Capture(token="test-token") as capture: + with pytest.raises(ValidationError, match="nid is required for NFT search"): + capture.search_nft("") + + @respx.mock + def test_search_nft_empty_records(self) -> None: + respx.post(NFT_SEARCH_API_URL).mock( + return_value=Response(200, json={"records": [], "order_id": "o1"}) + ) + + with Capture(token="test-token") as capture: + result = capture.search_nft(TEST_NID) + + assert result.records == [] + + +class TestRegisterAsset: + """Tests for the register() method.""" + + @respx.mock + def test_register_with_bytes(self) -> None: + respx.post(f"{DEFAULT_BASE_URL}/assets/").mock( + return_value=Response(200, json=MOCK_ASSET_RESPONSE) + ) + + with Capture(token="test-token") as capture: + asset = capture.register(b"test content", filename="test.txt") + + assert asset.nid == TEST_NID + + @respx.mock + def test_register_with_file_path(self, tmp_path) -> None: + test_file = tmp_path / "test.txt" + test_file.write_bytes(b"test content") + + respx.post(f"{DEFAULT_BASE_URL}/assets/").mock( + return_value=Response(200, json=MOCK_ASSET_RESPONSE) + ) + + with Capture(token="test-token") as capture: + asset = capture.register(str(test_file)) + + assert asset.nid == TEST_NID + + @respx.mock + def test_register_sends_auth_header(self) -> None: + route = respx.post(f"{DEFAULT_BASE_URL}/assets/").mock( + return_value=Response(200, json=MOCK_ASSET_RESPONSE) + ) + + with Capture(token="my-token") as capture: + capture.register(b"data", filename="f.bin") + + request = route.calls.last.request + assert request.headers["authorization"] == "token my-token" + + @respx.mock + def test_register_with_caption_and_headline(self, tmp_path) -> None: + test_file = tmp_path / "test.jpg" + test_file.write_bytes(b"jpeg data") + + respx.post(f"{DEFAULT_BASE_URL}/assets/").mock( + return_value=Response(200, json=MOCK_ASSET_RESPONSE) + ) + + with Capture(token="test-token") as capture: + asset = capture.register( + str(test_file), caption="My caption", headline="Short title" + ) + + assert asset.nid == TEST_NID + + +class TestUrlOverrides: + """Tests for URL override support in CaptureOptions.""" + + def test_custom_history_api_url(self) -> None: + custom_url = "https://custom-history.example.com" + capture = Capture(token="test-token", history_api_url=custom_url) + assert capture._history_api_url == custom_url + capture.close() + + def test_custom_merge_tree_api_url(self) -> None: + custom_url = "https://custom-merge.example.com" + capture = Capture(token="test-token", merge_tree_api_url=custom_url) + assert capture._merge_tree_api_url == custom_url + capture.close() + + def test_custom_asset_search_api_url(self) -> None: + custom_url = "https://custom-search.example.com" + capture = Capture(token="test-token", asset_search_api_url=custom_url) + assert capture._asset_search_api_url == custom_url + capture.close() + + def test_custom_nft_search_api_url(self) -> None: + custom_url = "https://custom-nft.example.com" + capture = Capture(token="test-token", nft_search_api_url=custom_url) + assert capture._nft_search_api_url == custom_url + capture.close() + + def test_url_overrides_via_capture_options(self) -> None: + opts = CaptureOptions( + token="test-token", + history_api_url="https://custom-history.example.com", + asset_search_api_url="https://custom-search.example.com", + ) + capture = Capture(options=opts) + assert capture._history_api_url == "https://custom-history.example.com" + assert capture._asset_search_api_url == "https://custom-search.example.com" + capture.close() + + @respx.mock + def test_custom_asset_search_url_is_used(self) -> None: + custom_url = "https://custom-search.example.com/search" + respx.post(custom_url).mock( + return_value=Response( + 200, + json={ + "precise_match": "", + "input_file_mime_type": "", + "similar_matches": [], + "order_id": "o1", + }, + ) + ) + + with Capture(token="test-token", asset_search_api_url=custom_url) as capture: + result = capture.search_asset(nid=TEST_NID) + + assert result.order_id == "o1" + + @respx.mock + def test_custom_nft_search_url_is_used(self) -> None: + custom_url = "https://custom-nft.example.com/nft" + respx.post(custom_url).mock( + return_value=Response( + 200, + json={"records": [], "order_id": "nft_o1"}, + ) + ) + + with Capture(token="test-token", nft_search_api_url=custom_url) as capture: + result = capture.search_nft(TEST_NID) + + assert result.order_id == "nft_o1" diff --git a/python/tests/test_verify.py b/python/tests/test_verify.py new file mode 100644 index 0000000..aad250d --- /dev/null +++ b/python/tests/test_verify.py @@ -0,0 +1,48 @@ +"""Tests for the Verify Engine URL helpers.""" + +from numbersprotocol_capture import verify +from numbersprotocol_capture.verify import VERIFY_BASE_URL + +TEST_NID = "bafybeif3mhxhkhfwuszl2lybtai3hz3q6naqpfisd4q55mcc7opkmiv5ei" + + +class TestSearchByNid: + def test_returns_correct_url(self) -> None: + url = verify.search_by_nid(TEST_NID) + assert url == f"{VERIFY_BASE_URL}/search?nid={TEST_NID}" + + def test_includes_nid_in_url(self) -> None: + url = verify.search_by_nid("some-nid") + assert "some-nid" in url + assert "/search" in url + + +class TestSearchByNft: + def test_returns_correct_url(self) -> None: + url = verify.search_by_nft("123", "0xabc") + assert url == f"{VERIFY_BASE_URL}/search?nft=123&contract=0xabc" + + def test_url_encodes_params(self) -> None: + url = verify.search_by_nft("token id", "0x contract") + assert "token+id" in url or "token%20id" in url + + +class TestAssetProfile: + def test_returns_correct_url(self) -> None: + url = verify.asset_profile(TEST_NID) + assert url == f"{VERIFY_BASE_URL}/asset-profile?nid={TEST_NID}" + + def test_includes_nid_in_url(self) -> None: + url = verify.asset_profile("some-nid") + assert "some-nid" in url + assert "asset-profile" in url + + +class TestAssetProfileByNft: + def test_returns_correct_url(self) -> None: + url = verify.asset_profile_by_nft("42", "0xdef") + assert url == f"{VERIFY_BASE_URL}/asset-profile?nft=42&contract=0xdef" + + def test_url_encodes_params(self) -> None: + url = verify.asset_profile_by_nft("token id", "0x contract") + assert "token+id" in url or "token%20id" in url diff --git a/scripts/check-feature-parity.py b/scripts/check-feature-parity.py index af32cae..4779d15 100644 --- a/scripts/check-feature-parity.py +++ b/scripts/check-feature-parity.py @@ -10,6 +10,7 @@ """ import re +import sys from dataclasses import dataclass from pathlib import Path @@ -204,15 +205,19 @@ def print_report() -> None: if parity_count == total_features: print("\n✓ Full feature parity achieved!") + return True else: missing = total_features - parity_count print(f"\n✗ {missing} feature(s) missing parity") + return False def main() -> None: check_ts_features() check_py_features() - print_report() + success = print_report() + if not success: + sys.exit(1) if __name__ == "__main__": diff --git a/scripts/sync-versions.py b/scripts/sync-versions.py index 2a105bd..567411c 100644 --- a/scripts/sync-versions.py +++ b/scripts/sync-versions.py @@ -100,20 +100,37 @@ def bump_version(current: str, bump_type: str) -> str: raise ValueError(f"Invalid bump type: {bump_type}") +def get_py_init_version() -> str: + """Get version from Python __init__.py.""" + content = PY_INIT_FILE.read_text() + match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE) + if match: + return match.group(1) + raise ValueError("Could not find __version__ in __init__.py") + + def check_versions() -> bool: """Check if versions are in sync.""" ts_version = get_ts_version() py_version = get_py_version() + py_init_version = get_py_init_version() print("Current versions:") - print(f" TypeScript: {ts_version}") - print(f" Python: {py_version}") + print(f" TypeScript (package.json): {ts_version}") + print(f" Python (pyproject.toml): {py_version}") + print(f" Python (__init__.py): {py_init_version}") - if ts_version == py_version: + if ts_version == py_version == py_init_version: print("\n✓ Versions are in sync") return True else: print("\n✗ Versions are out of sync!") + if ts_version != py_version: + print(f" package.json ({ts_version}) != pyproject.toml ({py_version})") + if ts_version != py_init_version: + print(f" package.json ({ts_version}) != __init__.py ({py_init_version})") + if py_version != py_init_version: + print(f" pyproject.toml ({py_version}) != __init__.py ({py_init_version})") return False diff --git a/ts/package-lock.json b/ts/package-lock.json index a6df0d6..92cc0bd 100644 --- a/ts/package-lock.json +++ b/ts/package-lock.json @@ -13,6 +13,7 @@ }, "devDependencies": { "@types/node": "^20.10.0", + "@vitest/coverage-v8": "^4.1.0", "tsup": "^8.0.0", "typescript": "^5.3.0", "vitest": "^4.0.18" @@ -27,6 +28,100 @@ "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", "license": "MIT" }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", + "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", @@ -508,6 +603,23 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, "node_modules/@noble/curves": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", @@ -532,6 +644,288 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@oxc-project/runtime": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz", + "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", + "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", + "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", + "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", + "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.54.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", @@ -847,6 +1241,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -882,18 +1287,49 @@ "undici-types": "~6.21.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.0.tgz", + "integrity": "sha512-nDWulKeik2bL2Va/Wl4x7DLuTKAXa906iRFooIRPR+huHkcvp9QDkPQ2RJdmjOFrqOqvNfoSQLF68deE3xC3CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.0", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.0", + "vitest": "4.1.0" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", - "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.0.0", + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "chai": "^6.2.1", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "chai": "^6.2.2", "tinyrainbow": "^3.0.3" }, "funding": { @@ -901,13 +1337,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", - "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.18", + "@vitest/spy": "4.1.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -916,7 +1352,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -928,9 +1364,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", - "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", "dev": true, "license": "MIT", "dependencies": { @@ -941,13 +1377,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", - "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.18", + "@vitest/utils": "4.1.0", "pathe": "^2.0.3" }, "funding": { @@ -955,13 +1391,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", - "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", + "@vitest/pretty-format": "4.1.0", + "@vitest/utils": "4.1.0", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -970,9 +1407,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", - "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", "dev": true, "license": "MIT", "funding": { @@ -980,13 +1417,14 @@ } }, "node_modules/@vitest/utils": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", - "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" }, "funding": { @@ -1029,6 +1467,18 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, "node_modules/bundle-require": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", @@ -1108,6 +1558,13 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1126,10 +1583,20 @@ } } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, "license": "MIT" }, @@ -1283,6 +1750,62 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -1293,6 +1816,274 @@ "node": ">=10" } }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -1333,6 +2124,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mlly": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", @@ -1455,9 +2274,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "dev": true, "funding": [ { @@ -1550,6 +2369,40 @@ "node": ">=8" } }, + "node_modules/rolldown": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz", + "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.115.0", + "@rolldown/pluginutils": "1.0.0-rc.9" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-x64": "1.0.0-rc.9", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" + } + }, "node_modules/rollup": { "version": "4.54.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", @@ -1592,6 +2445,19 @@ "fsevents": "~2.3.2" } }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -1627,9 +2493,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", "dev": true, "license": "MIT" }, @@ -1656,6 +2522,19 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -1825,17 +2704,17 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", + "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", + "@oxc-project/runtime": "0.115.0", + "lightningcss": "^1.32.0", "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.9", "tinyglobby": "^0.2.15" }, "bin": { @@ -1852,9 +2731,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.0.0-alpha.31", + "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -1867,13 +2747,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { + "optional": true + }, + "jiti": { "optional": true }, - "lightningcss": { + "less": { "optional": true }, "sass": { @@ -1900,31 +2783,31 @@ } }, "node_modules/vitest": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", - "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.0.18", - "@vitest/mocker": "4.0.18", - "@vitest/pretty-format": "4.0.18", - "@vitest/runner": "4.0.18", - "@vitest/snapshot": "4.0.18", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", + "@vitest/expect": "4.1.0", + "@vitest/mocker": "4.1.0", + "@vitest/pretty-format": "4.1.0", + "@vitest/runner": "4.1.0", + "@vitest/snapshot": "4.1.0", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", - "std-env": "^3.10.0", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -1940,12 +2823,13 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.18", - "@vitest/browser-preview": "4.0.18", - "@vitest/browser-webdriverio": "4.0.18", - "@vitest/ui": "4.0.18", + "@vitest/browser-playwright": "4.1.0", + "@vitest/browser-preview": "4.1.0", + "@vitest/browser-webdriverio": "4.1.0", + "@vitest/ui": "4.1.0", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -1974,6 +2858,9 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, diff --git a/ts/package.json b/ts/package.json index 81f6d0d..1bd67de 100644 --- a/ts/package.json +++ b/ts/package.json @@ -27,6 +27,7 @@ "typecheck": "tsc --noEmit", "clean": "rm -rf dist", "test": "vitest run", + "test:coverage": "vitest run --coverage", "test:watch": "vitest", "prepublishOnly": "npm run build" }, @@ -54,6 +55,7 @@ }, "devDependencies": { "@types/node": "^20.10.0", + "@vitest/coverage-v8": "^4.1.0", "tsup": "^8.0.0", "typescript": "^5.3.0", "vitest": "^4.0.18" diff --git a/ts/src/client.test.ts b/ts/src/client.test.ts index e1e50fb..027987e 100644 --- a/ts/src/client.test.ts +++ b/ts/src/client.test.ts @@ -337,3 +337,195 @@ describe('Asset Search Validation', () => { ).rejects.toThrow('sampleCount must be a positive integer') }) }) + +describe('URL overrides', () => { + let originalFetch: typeof global.fetch + + beforeEach(() => { + originalFetch = global.fetch + }) + + afterEach(() => { + global.fetch = originalFetch + vi.restoreAllMocks() + }) + + it('should use custom assetSearchApiUrl when provided', async () => { + const customUrl = 'https://custom-search.example.com/search' + const capture = new Capture({ + token: 'test-token', + assetSearchApiUrl: customUrl, + }) + + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + precise_match: '', + input_file_mime_type: '', + similar_matches: [], + order_id: 'test-order', + }), + } as Response) + + global.fetch = mockFetch + + await capture.searchAsset({ nid: TEST_NID }) + + const [url] = mockFetch.mock.calls[0] + expect(url).toBe(customUrl) + }) + + it('should use custom nftSearchApiUrl when provided', async () => { + const customUrl = 'https://custom-nft.example.com/search' + const capture = new Capture({ + token: 'test-token', + nftSearchApiUrl: customUrl, + }) + + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + records: [], + order_id: 'test-order', + }), + } as Response) + + global.fetch = mockFetch + + await capture.searchNft(TEST_NID) + + const [url] = mockFetch.mock.calls[0] + expect(url).toBe(customUrl) + }) +}) + +describe('get()', () => { + let originalFetch: typeof global.fetch + + beforeEach(() => { + originalFetch = global.fetch + }) + + afterEach(() => { + global.fetch = originalFetch + vi.restoreAllMocks() + }) + + it('should fetch asset by NID', async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + id: TEST_NID, + asset_file_name: 'photo.jpg', + asset_file_mime_type: 'image/jpeg', + caption: 'My photo', + headline: 'Demo', + }), + } as Response) + + global.fetch = mockFetch + + const capture = new Capture({ token: 'test-token' }) + const asset = await capture.get(TEST_NID) + + expect(asset.nid).toBe(TEST_NID) + expect(asset.filename).toBe('photo.jpg') + expect(asset.mimeType).toBe('image/jpeg') + expect(asset.caption).toBe('My photo') + expect(asset.headline).toBe('Demo') + }) + + it('should throw ValidationError when NID is empty', async () => { + const capture = new Capture({ token: 'test-token' }) + await expect(capture.get('')).rejects.toThrow('nid is required') + }) +}) + +describe('searchNft()', () => { + let originalFetch: typeof global.fetch + + beforeEach(() => { + originalFetch = global.fetch + }) + + afterEach(() => { + global.fetch = originalFetch + vi.restoreAllMocks() + }) + + it('should throw ValidationError when NID is empty', async () => { + const capture = new Capture({ token: 'test-token' }) + await expect(capture.searchNft('')).rejects.toThrow( + 'nid is required for NFT search' + ) + }) + + it('should return NFT search result', async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + records: [ + { + token_id: '1', + contract: '0xabc', + network: 'ethereum', + owner: '0xowner', + }, + ], + order_id: 'order_nft', + }), + } as Response) + + global.fetch = mockFetch + + const capture = new Capture({ token: 'test-token' }) + const result = await capture.searchNft(TEST_NID) + + expect(result.records).toHaveLength(1) + expect(result.records[0].tokenId).toBe('1') + expect(result.records[0].contract).toBe('0xabc') + expect(result.records[0].network).toBe('ethereum') + expect(result.orderId).toBe('order_nft') + }) +}) + +describe('register() validation', () => { + it('should throw ValidationError when headline exceeds 25 chars', async () => { + const capture = new Capture({ token: 'test-token' }) + const longHeadline = 'a'.repeat(26) + + await expect( + capture.register(new Uint8Array([1, 2, 3]), { + headline: longHeadline, + filename: 'test.bin', + }) + ).rejects.toThrow('headline must be 25 characters or less') + }) + + it('should throw ValidationError for empty file', async () => { + const capture = new Capture({ token: 'test-token' }) + + await expect( + capture.register(new Uint8Array(0), { filename: 'empty.bin' }) + ).rejects.toThrow('file cannot be empty') + }) +}) + +describe('update() validation', () => { + it('should throw ValidationError when NID is empty', async () => { + const capture = new Capture({ token: 'test-token' }) + + await expect(capture.update('', { caption: 'test' })).rejects.toThrow( + 'nid is required' + ) + }) + + it('should throw ValidationError when headline exceeds 25 chars', async () => { + const capture = new Capture({ token: 'test-token' }) + const longHeadline = 'a'.repeat(26) + + await expect( + capture.update(TEST_NID, { headline: longHeadline }) + ).rejects.toThrow('headline must be 25 characters or less') + }) +}) diff --git a/ts/src/client.ts b/ts/src/client.ts index c86440e..b006b3a 100644 --- a/ts/src/client.ts +++ b/ts/src/client.ts @@ -142,6 +142,10 @@ export class Capture { private readonly token: string private readonly baseUrl: string private readonly testnet: boolean + private readonly historyApiUrl: string + private readonly mergeTreeApiUrl: string + private readonly assetSearchApiUrl: string + private readonly nftSearchApiUrl: string constructor(options: CaptureOptions) { if (!options.token) { @@ -150,6 +154,10 @@ export class Capture { this.token = options.token this.testnet = options.testnet ?? false this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL + this.historyApiUrl = options.historyApiUrl ?? HISTORY_API_URL + this.mergeTreeApiUrl = options.mergeTreeApiUrl ?? MERGE_TREE_API_URL + this.assetSearchApiUrl = options.assetSearchApiUrl ?? ASSET_SEARCH_API_URL + this.nftSearchApiUrl = options.nftSearchApiUrl ?? NFT_SEARCH_API_URL } /** @@ -365,7 +373,7 @@ export class Capture { throw new ValidationError('nid is required') } - const url = new URL(HISTORY_API_URL) + const url = new URL(this.historyApiUrl) url.searchParams.set('nid', nid) if (this.testnet) { url.searchParams.set('testnet', 'true') @@ -427,7 +435,7 @@ export class Capture { timestampCreated: c.timestamp, })) - const response = await fetch(MERGE_TREE_API_URL, { + const response = await fetch(this.mergeTreeApiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -517,7 +525,7 @@ export class Capture { } // Verify Engine API requires token in Authorization header, not form data - const response = await fetch(ASSET_SEARCH_API_URL, { + const response = await fetch(this.assetSearchApiUrl, { method: 'POST', headers: { Authorization: `token ${this.token}`, @@ -573,7 +581,7 @@ export class Capture { throw new ValidationError('nid is required for NFT search') } - const response = await fetch(NFT_SEARCH_API_URL, { + const response = await fetch(this.nftSearchApiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/ts/src/crypto.test.ts b/ts/src/crypto.test.ts new file mode 100644 index 0000000..90070a3 --- /dev/null +++ b/ts/src/crypto.test.ts @@ -0,0 +1,73 @@ +/** + * Unit tests for Capture SDK cryptographic utilities. + */ + +import { describe, it, expect } from 'vitest' +import { sha256, createIntegrityProof, verifySignature } from './crypto.js' + +describe('sha256', () => { + it('should compute correct SHA-256 hash', async () => { + // Known SHA-256 of empty string + const emptyHash = await sha256(new Uint8Array(0)) + expect(emptyHash).toBe( + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + ) + }) + + it('should compute correct SHA-256 hash for known input', async () => { + // SHA-256 of "hello" + const data = new TextEncoder().encode('hello') + const hash = await sha256(data) + expect(hash).toBe( + '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824' + ) + }) + + it('should return a 64-character hex string', async () => { + const data = new TextEncoder().encode('test data') + const hash = await sha256(data) + expect(hash).toHaveLength(64) + expect(hash).toMatch(/^[0-9a-f]{64}$/) + }) + + it('should produce different hashes for different inputs', async () => { + const hash1 = await sha256(new TextEncoder().encode('input1')) + const hash2 = await sha256(new TextEncoder().encode('input2')) + expect(hash1).not.toBe(hash2) + }) +}) + +describe('createIntegrityProof', () => { + it('should create integrity proof with correct fields', () => { + const proofHash = 'abc123' + const mimeType = 'image/jpeg' + const before = Date.now() + const proof = createIntegrityProof(proofHash, mimeType) + const after = Date.now() + + expect(proof.proof_hash).toBe(proofHash) + expect(proof.asset_mime_type).toBe(mimeType) + expect(proof.created_at).toBeGreaterThanOrEqual(before) + expect(proof.created_at).toBeLessThanOrEqual(after) + }) + + it('should use millisecond timestamp', () => { + const proof = createIntegrityProof('hash', 'image/png') + // Millisecond timestamps are > 1e12 (current year timestamps) + expect(proof.created_at).toBeGreaterThan(1e12) + }) +}) + +describe('verifySignature', () => { + it('should return false for invalid signature', () => { + const result = verifySignature('message', 'invalid-signature', '0x1234') + expect(result).toBe(false) + }) + + it('should return false for address mismatch', async () => { + // Use a different address than the actual signer + const wrongAddress = '0x0000000000000000000000000000000000000001' + const result = verifySignature('test message', '0x' + '0'.repeat(130), wrongAddress) + expect(result).toBe(false) + }) +}) diff --git a/ts/src/errors.test.ts b/ts/src/errors.test.ts new file mode 100644 index 0000000..9780cdd --- /dev/null +++ b/ts/src/errors.test.ts @@ -0,0 +1,182 @@ +/** + * Unit tests for Capture SDK error classes. + */ + +import { describe, it, expect } from 'vitest' +import { + CaptureError, + AuthenticationError, + PermissionError, + NotFoundError, + InsufficientFundsError, + ValidationError, + NetworkError, + createApiError, +} from './errors.js' + +describe('CaptureError', () => { + it('should create error with message, code and statusCode', () => { + const err = new CaptureError('test message', 'TEST_CODE', 500) + expect(err.message).toBe('test message') + expect(err.code).toBe('TEST_CODE') + expect(err.statusCode).toBe(500) + expect(err.name).toBe('CaptureError') + }) + + it('should be an instance of Error', () => { + const err = new CaptureError('test', 'CODE') + expect(err).toBeInstanceOf(Error) + expect(err).toBeInstanceOf(CaptureError) + }) + + it('should work without statusCode', () => { + const err = new CaptureError('test', 'CODE') + expect(err.statusCode).toBeUndefined() + }) +}) + +describe('AuthenticationError', () => { + it('should have correct defaults', () => { + const err = new AuthenticationError() + expect(err.code).toBe('AUTHENTICATION_ERROR') + expect(err.statusCode).toBe(401) + expect(err.name).toBe('AuthenticationError') + }) + + it('should accept custom message', () => { + const err = new AuthenticationError('Custom auth error') + expect(err.message).toBe('Custom auth error') + }) + + it('should be instanceof CaptureError', () => { + const err = new AuthenticationError() + expect(err).toBeInstanceOf(CaptureError) + expect(err).toBeInstanceOf(AuthenticationError) + }) +}) + +describe('PermissionError', () => { + it('should have correct defaults', () => { + const err = new PermissionError() + expect(err.code).toBe('PERMISSION_ERROR') + expect(err.statusCode).toBe(403) + expect(err.name).toBe('PermissionError') + }) + + it('should be instanceof CaptureError', () => { + expect(new PermissionError()).toBeInstanceOf(CaptureError) + }) +}) + +describe('NotFoundError', () => { + it('should include NID in message when provided', () => { + const nid = 'bafybei123' + const err = new NotFoundError(nid) + expect(err.message).toBe(`Asset not found: ${nid}`) + expect(err.code).toBe('NOT_FOUND') + expect(err.statusCode).toBe(404) + expect(err.name).toBe('NotFoundError') + }) + + it('should use generic message without NID', () => { + const err = new NotFoundError() + expect(err.message).toBe('Asset not found') + }) + + it('should be instanceof CaptureError', () => { + expect(new NotFoundError()).toBeInstanceOf(CaptureError) + }) +}) + +describe('InsufficientFundsError', () => { + it('should have correct defaults', () => { + const err = new InsufficientFundsError() + expect(err.code).toBe('INSUFFICIENT_FUNDS') + expect(err.statusCode).toBe(400) + expect(err.name).toBe('InsufficientFundsError') + }) + + it('should be instanceof CaptureError', () => { + expect(new InsufficientFundsError()).toBeInstanceOf(CaptureError) + }) +}) + +describe('ValidationError', () => { + it('should set correct code and name', () => { + const err = new ValidationError('Invalid input') + expect(err.message).toBe('Invalid input') + expect(err.code).toBe('VALIDATION_ERROR') + expect(err.name).toBe('ValidationError') + expect(err.statusCode).toBeUndefined() + }) + + it('should be instanceof CaptureError', () => { + expect(new ValidationError('msg')).toBeInstanceOf(CaptureError) + }) +}) + +describe('NetworkError', () => { + it('should create with message and optional statusCode', () => { + const err = new NetworkError('Connection failed', 503) + expect(err.message).toBe('Connection failed') + expect(err.code).toBe('NETWORK_ERROR') + expect(err.statusCode).toBe(503) + expect(err.name).toBe('NetworkError') + }) + + it('should work without statusCode', () => { + const err = new NetworkError('Connection failed') + expect(err.statusCode).toBeUndefined() + }) + + it('should be instanceof CaptureError', () => { + expect(new NetworkError('msg')).toBeInstanceOf(CaptureError) + }) +}) + +describe('createApiError', () => { + it('should return AuthenticationError for 401', () => { + const err = createApiError(401, 'Unauthorized') + expect(err).toBeInstanceOf(AuthenticationError) + expect(err.statusCode).toBe(401) + }) + + it('should return PermissionError for 403', () => { + const err = createApiError(403, 'Forbidden') + expect(err).toBeInstanceOf(PermissionError) + expect(err.statusCode).toBe(403) + }) + + it('should return NotFoundError for 404', () => { + const err = createApiError(404, 'Not found') + expect(err).toBeInstanceOf(NotFoundError) + expect(err.statusCode).toBe(404) + }) + + it('should return NotFoundError with NID when provided', () => { + const nid = 'bafybei123' + const err = createApiError(404, 'Not found', nid) + expect(err).toBeInstanceOf(NotFoundError) + expect(err.message).toBe(`Asset not found: ${nid}`) + }) + + it('should return InsufficientFundsError for 400 with insufficient message', () => { + const err = createApiError(400, 'Insufficient NUM tokens') + expect(err).toBeInstanceOf(InsufficientFundsError) + }) + + it('should return ValidationError for 400 without insufficient message', () => { + const err = createApiError(400, 'Bad request') + expect(err).toBeInstanceOf(ValidationError) + }) + + it('should return NetworkError for other status codes', () => { + const err = createApiError(500, 'Server error') + expect(err).toBeInstanceOf(NetworkError) + expect(err.statusCode).toBe(500) + + const err503 = createApiError(503, 'Service unavailable') + expect(err503).toBeInstanceOf(NetworkError) + expect(err503.statusCode).toBe(503) + }) +}) diff --git a/ts/src/types.ts b/ts/src/types.ts index bc57312..4d2c1a4 100644 --- a/ts/src/types.ts +++ b/ts/src/types.ts @@ -18,6 +18,14 @@ export interface CaptureOptions { testnet?: boolean /** Custom base URL (overrides testnet setting) */ baseUrl?: string + /** Override URL for history API endpoint */ + historyApiUrl?: string + /** Override URL for merge-tree API endpoint */ + mergeTreeApiUrl?: string + /** Override URL for asset search API endpoint */ + assetSearchApiUrl?: string + /** Override URL for NFT search API endpoint */ + nftSearchApiUrl?: string } /** @@ -86,7 +94,10 @@ export interface Commit { author: string /** Address that made this commit */ committer: string - /** Unix timestamp of the commit */ + /** + * Unix timestamp of the commit in **seconds**. + * Note: `IntegrityProof.created_at` uses milliseconds; this field uses seconds. + */ timestamp: number /** Description of the action */ action: string @@ -140,8 +151,8 @@ export interface AssetTree { miningPreference?: string /** AI/algorithm information for generated content */ generatedBy?: string - /** Additional fields from commits */ - [key: string]: unknown + /** Additional fields from commits not covered by the named properties above */ + extra?: Record } /** @@ -151,6 +162,10 @@ export interface AssetTree { export interface IntegrityProof { proof_hash: string asset_mime_type: string + /** + * Creation timestamp in **milliseconds** since Unix epoch. + * Note: `Commit.timestamp` from the API uses seconds; this field uses milliseconds. + */ created_at: number } diff --git a/ts/vitest.config.ts b/ts/vitest.config.ts new file mode 100644 index 0000000..88a11e1 --- /dev/null +++ b/ts/vitest.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + coverage: { + provider: 'v8', + include: ['src/**/*.ts'], + exclude: [ + 'src/**/*.test.ts', + 'src/types.ts', + 'src/index.ts', + 'src/integration-runner.ts', + ], + reporter: ['text', 'lcov'], + }, + }, +}) From 73e41ec532af6ce1fecc2af1ff9aa4e89e3006a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:30:26 +0000 Subject: [PATCH 3/3] fix: address code review comments and add workflow permissions Co-authored-by: numbers-official <181934381+numbers-official@users.noreply.github.com> --- .github/workflows/ci.yml | 3 +++ python/numbersprotocol_capture/types.py | 1 + ts/src/types.ts | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8438580..620575c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: # Check version synchronization version-check: diff --git a/python/numbersprotocol_capture/types.py b/python/numbersprotocol_capture/types.py index c67e4d9..bc987ee 100644 --- a/python/numbersprotocol_capture/types.py +++ b/python/numbersprotocol_capture/types.py @@ -130,6 +130,7 @@ class Commit: """Unix timestamp of the commit in **seconds**. Note: ``IntegrityProof.created_at`` uses milliseconds; this field uses seconds. + To convert to a ``datetime`` object: ``datetime.fromtimestamp(commit.timestamp)``. """ action: str diff --git a/ts/src/types.ts b/ts/src/types.ts index 4d2c1a4..a55657a 100644 --- a/ts/src/types.ts +++ b/ts/src/types.ts @@ -151,7 +151,9 @@ export interface AssetTree { miningPreference?: string /** AI/algorithm information for generated content */ generatedBy?: string - /** Additional fields from commits not covered by the named properties above */ + /** Additional fields from commits not covered by the named properties above. + * When the API returns keys that do not map to a known field, they are + * collected here rather than being silently discarded. */ extra?: Record }