From 3eb3126efdcb5321a36f86abf1fac7528bd84cae Mon Sep 17 00:00:00 2001 From: Arc Date: Thu, 23 Jul 2026 01:24:32 +0100 Subject: [PATCH] needed for permssions to trigger from wasm extension json --- lnbits/core/models/extensions.py | 35 ++++++++-- lnbits/core/views/extension_api.py | 4 ++ tests/api/test_extension_api.py | 100 +++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 7 deletions(-) diff --git a/lnbits/core/models/extensions.py b/lnbits/core/models/extensions.py index 665e0c29f..5b798f810 100644 --- a/lnbits/core/models/extensions.py +++ b/lnbits/core/models/extensions.py @@ -47,6 +47,7 @@ class ExplicitRelease(BaseModel): details_link: str | None paid_features: str | None pay_link: str | None + extension_type: str | None = None def is_version_compatible(self): return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version) @@ -111,6 +112,12 @@ class ExtensionConfig(BaseModel): def is_version_compatible(self) -> bool: return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version) + @classmethod + async def fetch_release_config(cls, url: str) -> ExtensionConfig: + error_msg = "Cannot fetch extension release config" + config = await github_api_get(url, error_msg) + return ExtensionConfig.parse_obj(config) + @classmethod async def fetch_github_release_config( cls, org: str, repo: str, tag_name: str @@ -118,9 +125,7 @@ class ExtensionConfig(BaseModel): config_url = ( f"https://raw.githubusercontent.com/{org}/{repo}/{tag_name}/config.json" ) - error_msg = "Cannot fetch GitHub extension config" - config = await github_api_get(config_url, error_msg) - return ExtensionConfig.parse_obj(config) + return await cls.fetch_release_config(config_url) class ReleasePaymentInfo(BaseModel): @@ -350,6 +355,8 @@ class ExtensionRelease(BaseModel): repo: str | None = None icon: str | None = None details_link: str | None = None + extension_type: str | None = None + permissions: list[ExtensionPermission] = [] paid_features: str | None = None pay_link: str | None = None @@ -357,6 +364,14 @@ class ExtensionRelease(BaseModel): paid_sats: int | None = 0 payment_hash: str | None = None + def apply_config(self, config: ExtensionConfig) -> None: + self.min_lnbits_version = config.min_lnbits_version + self.max_lnbits_version = config.max_lnbits_version + self.is_version_compatible = config.is_version_compatible() + self.warning = config.warning + self.extension_type = config.extension_type + self.permissions = config.permissions + @property def archive_url(self) -> str: if not self.pay_link: @@ -420,6 +435,7 @@ class ExtensionRelease(BaseModel): warning=e.warning, html_url=e.html_url, details_link=e.details_link, + extension_type=e.extension_type, pay_link=e.pay_link, paid_features=e.paid_features, repo=e.repo, @@ -447,10 +463,7 @@ class ExtensionRelease(BaseModel): if not config: continue - release.min_lnbits_version = config.min_lnbits_version - release.max_lnbits_version = config.max_lnbits_version - release.is_version_compatible = config.is_version_compatible() - + release.apply_config(config) release.icon = icon_to_github_url(f"{org}/{repo}", config.tile) return extension_releases @@ -930,6 +943,14 @@ class InstallableExtension(BaseModel): if e.id != ext_id: continue explicit_release = ExtensionRelease.from_explicit_release(url, e) + if ( + explicit_release.extension_type == "wasm" + and explicit_release.details_link + ): + config = await ExtensionConfig.fetch_release_config( + explicit_release.details_link + ) + explicit_release.apply_config(config) await explicit_release.check_payment_requirements() extension_releases.append(explicit_release) diff --git a/lnbits/core/views/extension_api.py b/lnbits/core/views/extension_api.py index 08c67b0b3..d72a2baa7 100644 --- a/lnbits/core/views/extension_api.py +++ b/lnbits/core/views/extension_api.py @@ -713,6 +713,10 @@ async def get_extension_releases(ext_id: str) -> list[ExtensionRelease]: extension_releases: list[ExtensionRelease] = ( await InstallableExtension.get_extension_releases(ext_id) ) + for release in extension_releases: + release.permissions = validate_extension_permissions( + ext_id, release.permissions + ) installed_ext = await get_installed_extension(ext_id) if not installed_ext: diff --git a/tests/api/test_extension_api.py b/tests/api/test_extension_api.py index e257844ce..db0e78331 100644 --- a/tests/api/test_extension_api.py +++ b/tests/api/test_extension_api.py @@ -21,12 +21,14 @@ from lnbits.core.models import Account, CreateInvoice from lnbits.core.models.extensions import ( CreateExtension, CreateExtensionReview, + ExplicitRelease, Extension, ExtensionConfig, ExtensionPermission, ExtensionPermissionsUpdate, ExtensionRelease, InstallableExtension, + Manifest, PayToEnableInfo, ReleasePaymentInfo, UserExtensionInfo, @@ -173,6 +175,104 @@ async def test_extension_api_install_details_and_release_endpoints(mocker): assert release_info["is_version_compatible"] is True +@pytest.mark.anyio +async def test_explicit_wasm_release_loads_install_permissions( + settings, + mocker, +): + ext_id = f"wasm_{uuid4().hex[:8]}" + non_wasm_ext_id = f"python_{uuid4().hex[:8]}" + manifest_url = "https://extensions.example/manifest.json" + details_link = f"https://extensions.example/{ext_id}/config.json" + explicit_release = ExplicitRelease( + id=ext_id, + name="Explicit WASM Extension", + version="1.0.0", + archive=f"https://extensions.example/{ext_id}.zip", + hash="archive-hash", + repo=f"https://github.com/example/{ext_id}", + icon=None, + short_description="Explicit WASM release", + min_lnbits_version="0.1.0", + max_lnbits_version=None, + html_url=None, + warning=None, + info_notification=None, + critical_notification=None, + details_link=details_link, + paid_features=None, + pay_link=None, + extension_type="wasm", + ) + non_wasm_release = explicit_release.copy( + update={ + "id": non_wasm_ext_id, + "name": "Explicit Python Extension", + "details_link": f"https://extensions.example/{non_wasm_ext_id}/config.json", + "extension_type": None, + } + ) + config_permissions = [ExtensionPermission(id="wallet.list")] + config = ExtensionConfig( + name=ext_id, + short_description="Explicit WASM release", + min_lnbits_version="0.1.0", + max_lnbits_version=None, + extension_type="wasm", + permissions=config_permissions, + ) + + async def fetch_manifest(url): + if url == manifest_url: + return Manifest(extensions=[explicit_release, non_wasm_release]) + return Manifest() + + mocker.patch.object(settings, "lnbits_extensions_manifests", [manifest_url]) + mocker.patch.object( + settings, + "lnbits_extensions_builder_manifest_url", + "https://extensions.example/builder.json", + ) + mocker.patch.object( + InstallableExtension, + "fetch_manifest", + mocker.AsyncMock(side_effect=fetch_manifest), + ) + fetch_config_mock = mocker.patch.object( + ExtensionConfig, + "fetch_release_config", + mocker.AsyncMock(return_value=config), + ) + + releases = await InstallableExtension.get_extension_releases(ext_id) + + assert len(releases) == 1 + assert releases[0].extension_type == "wasm" + assert releases[0].permissions == config_permissions + fetch_config_mock.assert_awaited_once_with(details_link) + + fetch_config_mock.reset_mock() + non_wasm_releases = await InstallableExtension.get_extension_releases( + non_wasm_ext_id + ) + assert len(non_wasm_releases) == 1 + assert non_wasm_releases[0].extension_type is None + assert non_wasm_releases[0].permissions == [] + fetch_config_mock.assert_not_awaited() + + mocker.patch.object( + InstallableExtension, + "get_extension_releases", + mocker.AsyncMock(return_value=releases), + ) + mocker.patch( + "lnbits.core.views.extension_api.get_installed_extension", + mocker.AsyncMock(return_value=None), + ) + api_releases = await get_extension_releases(ext_id) + assert api_releases[0].permissions == config_permissions + + @pytest.mark.anyio async def test_extension_api_installs_wasm_with_granted_permissions( tmp_path,