path proxy
This commit is contained in:
+18
-1
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import glob
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
@@ -38,10 +39,10 @@ from lnbits.core.tasks import (
|
||||
wait_for_paid_invoices,
|
||||
wait_notification_messages,
|
||||
)
|
||||
from lnbits.core.wasm.extension_host import register_wasm_ext_routes
|
||||
from lnbits.exceptions import register_exception_handlers
|
||||
from lnbits.helpers import version_parse
|
||||
from lnbits.settings import settings
|
||||
from lnbits.core.wasm.extension_host import register_wasm_ext_routes
|
||||
from lnbits.tasks import (
|
||||
cancel_all_tasks,
|
||||
create_permanent_task,
|
||||
@@ -431,6 +432,8 @@ def register_ext_tasks(ext: Extension) -> None:
|
||||
|
||||
def register_ext_routes(app: FastAPI, ext: Extension) -> None:
|
||||
"""Register FastAPI routes for extension."""
|
||||
if ext.extension_type != "wasm":
|
||||
ext.extension_type = _load_extension_type(ext.code) or ext.extension_type
|
||||
if ext.extension_type == "wasm":
|
||||
settings.activate_extension_paths(ext.code, ext.upgrade_hash, [])
|
||||
register_wasm_ext_routes(app, ext)
|
||||
@@ -460,6 +463,20 @@ def register_ext_routes(app: FastAPI, ext: Extension) -> None:
|
||||
app.include_router(router=ext_route, prefix=prefix)
|
||||
|
||||
|
||||
def _load_extension_type(ext_id: str) -> str | None:
|
||||
try:
|
||||
conf_path = Path(
|
||||
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
||||
)
|
||||
if not conf_path.is_file():
|
||||
return None
|
||||
with open(conf_path, "r+") as json_file:
|
||||
config_json = json.load(json_file)
|
||||
return config_json.get("extension_type")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
async def check_and_register_extensions(app: FastAPI) -> None:
|
||||
await check_installed_extensions(app)
|
||||
for ext in await get_valid_extensions(False):
|
||||
|
||||
@@ -22,6 +22,8 @@ from lnbits.settings import settings
|
||||
async def migrate_extension_database(
|
||||
ext: InstallableExtension, current_version: DbVersion | None = None
|
||||
):
|
||||
if _is_wasm_extension(ext):
|
||||
return
|
||||
|
||||
try:
|
||||
ext_migrations = importlib.import_module(f"{ext.module_name}.migrations")
|
||||
@@ -58,6 +60,34 @@ async def run_migration(
|
||||
await update_migration_version(conn, db_name, version)
|
||||
|
||||
|
||||
def _is_wasm_extension(ext: InstallableExtension) -> bool:
|
||||
if ext.meta and ext.meta.extension_type == "wasm":
|
||||
return True
|
||||
|
||||
try:
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
candidate_dirs = [
|
||||
Path(ext.ext_dir),
|
||||
Path(settings.lnbits_extensions_path, "extensions", ext.id),
|
||||
Path(settings.lnbits_path, "extensions", ext.id),
|
||||
Path.cwd() / "extensions" / ext.id,
|
||||
]
|
||||
for base in candidate_dirs:
|
||||
conf_path = Path(base, "config.json")
|
||||
if not conf_path.is_file():
|
||||
continue
|
||||
with open(conf_path, "r+") as json_file:
|
||||
config_json = json.load(json_file)
|
||||
if config_json.get("extension_type") == "wasm":
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def to_valid_user_id(user_id: str) -> UUID:
|
||||
if len(user_id) < 32:
|
||||
raise ValueError("User ID must have at least 128 bits")
|
||||
|
||||
@@ -83,7 +83,7 @@ class ExtensionConfig(BaseModel):
|
||||
warning: str | None = ""
|
||||
min_lnbits_version: str | None
|
||||
max_lnbits_version: str | None
|
||||
permissions: list["ExtensionPermission"] = []
|
||||
permissions: list[ExtensionPermission] = []
|
||||
extension_type: str | None = "python"
|
||||
public_kv_keys: list[str] = []
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import json
|
||||
import sys
|
||||
import traceback
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
from bolt11 import decode as bolt11_decode
|
||||
@@ -24,6 +26,7 @@ from lnbits.core.models.extensions import (
|
||||
ExtensionReview,
|
||||
ExtensionReviewPaymentRequest,
|
||||
ExtensionReviewsStatus,
|
||||
ExtensionPermission,
|
||||
ExtensionPermissionsGrant,
|
||||
InstallableExtension,
|
||||
PayToEnableInfo,
|
||||
@@ -196,9 +199,12 @@ async def api_enable_extension(
|
||||
user_ext = UserExtension(user=account_id.id, extension=ext_id, active=False)
|
||||
await create_user_extension(user_ext)
|
||||
|
||||
required_permissions = (
|
||||
[p.id for p in ext.meta.permissions] if ext.meta and ext.meta.permissions else []
|
||||
permissions_source = (
|
||||
ext.meta.permissions
|
||||
if ext.meta and ext.meta.permissions
|
||||
else _load_permissions_from_config(ext_id)
|
||||
)
|
||||
required_permissions = [p.id for p in permissions_source] if permissions_source else []
|
||||
granted_permissions = []
|
||||
if grant and grant.permissions:
|
||||
granted_permissions = grant.permissions
|
||||
@@ -212,10 +218,6 @@ async def api_enable_extension(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"Missing required permissions to enable this extension.",
|
||||
)
|
||||
user_ext_info = user_ext.extra or UserExtensionInfo()
|
||||
user_ext_info.granted_permissions = granted_permissions
|
||||
user_ext.extra = user_ext_info
|
||||
await update_user_extension(user_ext)
|
||||
|
||||
if account_id.is_admin_id or not ext.requires_payment:
|
||||
user_ext.active = True
|
||||
@@ -267,10 +269,58 @@ async def api_disable_extension(
|
||||
)
|
||||
logger.info(f"Disabling extension: {ext_id}.")
|
||||
user_ext.active = False
|
||||
if user_ext.extra and user_ext.extra.granted_permissions:
|
||||
user_ext.extra.granted_permissions = []
|
||||
await update_user_extension(user_ext)
|
||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' disabled.")
|
||||
|
||||
|
||||
@extension_router.put("/{ext_id}/permissions")
|
||||
async def api_update_extension_permissions(
|
||||
ext_id: str,
|
||||
account_id: AccountId = Depends(check_account_id_exists),
|
||||
grant: ExtensionPermissionsGrant | None = Body(default=None),
|
||||
) -> SimpleStatus:
|
||||
if ext_id not in [e.code for e in await get_valid_extensions()]:
|
||||
raise HTTPException(
|
||||
HTTPStatus.NOT_FOUND, f"Extension '{ext_id}' doesn't exist."
|
||||
)
|
||||
|
||||
ext = await get_installed_extension(ext_id)
|
||||
if not ext:
|
||||
raise ValueError(f"Extension '{ext_id}' is not installed.")
|
||||
if not ext.active:
|
||||
raise ValueError(f"Extension '{ext_id}' is not activated.")
|
||||
|
||||
user_ext = await get_user_extension(account_id.id, ext_id)
|
||||
if not user_ext:
|
||||
user_ext = UserExtension(user=account_id.id, extension=ext_id, active=False)
|
||||
await create_user_extension(user_ext)
|
||||
|
||||
permissions_source = (
|
||||
ext.meta.permissions
|
||||
if ext.meta and ext.meta.permissions
|
||||
else _load_permissions_from_config(ext_id)
|
||||
)
|
||||
required_permissions = [p.id for p in permissions_source] if permissions_source else []
|
||||
granted_permissions = grant.permissions if grant and grant.permissions else []
|
||||
|
||||
if required_permissions:
|
||||
missing = [p for p in required_permissions if p not in granted_permissions]
|
||||
if missing:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"Missing required permissions to save for this extension.",
|
||||
)
|
||||
|
||||
user_ext_info = user_ext.extra or UserExtensionInfo()
|
||||
user_ext_info.granted_permissions = granted_permissions
|
||||
user_ext.extra = user_ext_info
|
||||
await update_user_extension(user_ext)
|
||||
|
||||
return SimpleStatus(success=True, message=f"Permissions saved for '{ext_id}'.")
|
||||
|
||||
|
||||
@extension_router.put("/{ext_id}/activate", dependencies=[Depends(check_admin)])
|
||||
async def api_activate_extension(ext_id: str) -> SimpleStatus:
|
||||
try:
|
||||
@@ -543,6 +593,7 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
installed_exts: list[InstallableExtension] = await get_installed_extensions(
|
||||
conn=conn
|
||||
)
|
||||
user_exts = await get_user_extensions(account_id.id, conn=conn)
|
||||
all_ext_ids = [ext.code for ext in await get_valid_extensions(conn=conn)]
|
||||
inactive_extensions = [
|
||||
e.id for e in await get_installed_extensions(active=False, conn=conn)
|
||||
@@ -550,6 +601,7 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
db_versions = await get_db_versions(conn=conn)
|
||||
|
||||
installed_exts_ids = [e.id for e in installed_exts]
|
||||
user_exts_map = {e.extension: e for e in user_exts}
|
||||
|
||||
installable_exts = await InstallableExtension.get_installable_extensions(
|
||||
post_refresh_cache=account_id.is_admin_id
|
||||
@@ -578,6 +630,8 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
e.name = installed_ext.name
|
||||
e.short_description = installed_ext.short_description
|
||||
e.icon = installed_ext.icon
|
||||
if e.meta and not e.meta.permissions:
|
||||
e.meta.permissions = _load_permissions_from_config(e.id)
|
||||
|
||||
extension_data = [
|
||||
{
|
||||
@@ -598,6 +652,11 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
"permissions": (
|
||||
[dict(p) for p in ext.meta.permissions]
|
||||
if ext.meta and ext.meta.permissions
|
||||
else [dict(p) for p in _load_permissions_from_config(ext.id)]
|
||||
),
|
||||
"grantedPermissions": (
|
||||
user_exts_map.get(ext.id).extra.granted_permissions
|
||||
if user_exts_map.get(ext.id) and user_exts_map.get(ext.id).extra
|
||||
else []
|
||||
),
|
||||
"latestRelease": (
|
||||
@@ -627,6 +686,21 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
return extension_data
|
||||
|
||||
|
||||
def _load_permissions_from_config(ext_id: str) -> list[ExtensionPermission]:
|
||||
try:
|
||||
conf_path = Path(
|
||||
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
||||
)
|
||||
if not conf_path.is_file():
|
||||
return []
|
||||
with open(conf_path, "r+") as json_file:
|
||||
config_json = json.load(json_file)
|
||||
permissions = config_json.get("permissions", [])
|
||||
return [ExtensionPermission(**p) for p in permissions]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
@extension_router.get(
|
||||
"/reviews/tags",
|
||||
dependencies=[Depends(check_account_exists)],
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, WebSocket
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.responses import HTMLResponse, Response
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from lnbits.core.crud.extensions import get_user_extension
|
||||
from lnbits.core.crud.wallets import get_wallet_for_key, get_wallets_ids
|
||||
from lnbits.core.models import User
|
||||
from lnbits.core.services import create_invoice, pay_invoice, websocket_updater
|
||||
from lnbits.decorators import check_user_exists
|
||||
from lnbits.db import Database
|
||||
from lnbits.decorators import check_user_exists
|
||||
from lnbits.helpers import template_renderer
|
||||
from lnbits.settings import settings
|
||||
from lnbits.tasks import register_invoice_listener, unregister_invoice_listener
|
||||
|
||||
from .service import WasmExecutionError, wasm_call
|
||||
|
||||
|
||||
@@ -26,7 +29,9 @@ def _renderer(ext_id: str):
|
||||
def _ext_static_dir(ext_id: str, upgrade_hash: str | None = None) -> Path:
|
||||
if upgrade_hash:
|
||||
return Path(
|
||||
settings.lnbits_extensions_upgrade_path, f"{ext_id}-{upgrade_hash}", "static"
|
||||
settings.lnbits_extensions_upgrade_path,
|
||||
f"{ext_id}-{upgrade_hash}",
|
||||
"static",
|
||||
)
|
||||
return Path(settings.lnbits_extensions_path, "extensions", ext_id, "static")
|
||||
|
||||
@@ -88,10 +93,38 @@ async def _require_wallet_access(user_id: str, wallet_id: str) -> None:
|
||||
raise HTTPException(403, "Wallet does not belong to user.")
|
||||
|
||||
|
||||
async def _wait_for_increment_payment(
|
||||
ext_id: str, payment_hash: str, db: Database, upgrade_hash: str | None
|
||||
) -> None:
|
||||
queue_name = f"wasm:{ext_id}:{payment_hash}:{time.time()}"
|
||||
invoice_queue: asyncio.Queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue, queue_name)
|
||||
try:
|
||||
while True:
|
||||
payment = await asyncio.wait_for(invoice_queue.get(), timeout=3600)
|
||||
if payment.payment_hash != payment_hash:
|
||||
continue
|
||||
if payment.pending is False:
|
||||
try:
|
||||
new_value = await wasm_call(
|
||||
ext_id, "increment_counter", [], upgrade_hash=upgrade_hash
|
||||
)
|
||||
except WasmExecutionError:
|
||||
return
|
||||
await _kv_set(db, ext_id, "counter", str(new_value))
|
||||
await websocket_updater(f"{ext_id}:counter", str(new_value))
|
||||
return
|
||||
except asyncio.TimeoutError:
|
||||
return
|
||||
finally:
|
||||
unregister_invoice_listener(queue_name)
|
||||
|
||||
|
||||
def register_wasm_ext_routes(app, ext) -> None:
|
||||
ext_id = ext.code
|
||||
db = Database(f"ext_{ext_id}")
|
||||
router = APIRouter(prefix=f"/{ext_id}", tags=[f"{ext_id} (wasm)"])
|
||||
proxy_block = f"/{ext_id}/api/v1/proxy"
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
async def index(req: Request, user: User = Depends(check_user_exists)):
|
||||
@@ -116,9 +149,7 @@ def register_wasm_ext_routes(app, ext) -> None:
|
||||
@router.get("/api/v1/kv/{key}")
|
||||
async def api_kv_get(key: str, user: User = Depends(check_user_exists)):
|
||||
await _require_permission(user.id, ext_id, "ext.db.read_write")
|
||||
_check_quota(
|
||||
user.id, ext_id, "db", settings.lnbits_wasm_max_db_ops_per_min
|
||||
)
|
||||
_check_quota(user.id, ext_id, "db", settings.lnbits_wasm_max_db_ops_per_min)
|
||||
value = await _kv_get(db, ext_id, key)
|
||||
return {"key": key, "value": value}
|
||||
|
||||
@@ -132,11 +163,11 @@ def register_wasm_ext_routes(app, ext) -> None:
|
||||
return {"key": key, "value": value}
|
||||
|
||||
@router.post("/api/v1/kv/{key}")
|
||||
async def api_kv_set(key: str, payload: dict, user: User = Depends(check_user_exists)):
|
||||
async def api_kv_set(
|
||||
key: str, payload: dict, user: User = Depends(check_user_exists)
|
||||
):
|
||||
await _require_permission(user.id, ext_id, "ext.db.read_write")
|
||||
_check_quota(
|
||||
user.id, ext_id, "db", settings.lnbits_wasm_max_db_ops_per_min
|
||||
)
|
||||
_check_quota(user.id, ext_id, "db", settings.lnbits_wasm_max_db_ops_per_min)
|
||||
value = payload.get("value")
|
||||
if value is None:
|
||||
raise HTTPException(400, "Missing value")
|
||||
@@ -147,9 +178,7 @@ def register_wasm_ext_routes(app, ext) -> None:
|
||||
@router.post("/api/v1/kv/{key}/increment")
|
||||
async def api_kv_increment(key: str, user: User = Depends(check_user_exists)):
|
||||
await _require_permission(user.id, ext_id, "ext.db.read_write")
|
||||
_check_quota(
|
||||
user.id, ext_id, "db", settings.lnbits_wasm_max_db_ops_per_min
|
||||
)
|
||||
_check_quota(user.id, ext_id, "db", settings.lnbits_wasm_max_db_ops_per_min)
|
||||
if key != "counter":
|
||||
raise HTTPException(400, "Only 'counter' is supported in this example.")
|
||||
try:
|
||||
@@ -163,7 +192,9 @@ def register_wasm_ext_routes(app, ext) -> None:
|
||||
return {"key": key, "value": new_value}
|
||||
|
||||
@router.post("/api/v1/invoices")
|
||||
async def api_create_invoice(payload: dict, user: User = Depends(check_user_exists)):
|
||||
async def api_create_invoice(
|
||||
payload: dict, user: User = Depends(check_user_exists)
|
||||
):
|
||||
await _require_permission(user.id, ext_id, "lnbits.invoice.create")
|
||||
_check_quota(
|
||||
user.id, ext_id, "invoice", settings.lnbits_wasm_max_invoice_ops_per_min
|
||||
@@ -187,6 +218,43 @@ def register_wasm_ext_routes(app, ext) -> None:
|
||||
"amount": payment.amount,
|
||||
}
|
||||
|
||||
@router.post("/api/v1/invoices/increment")
|
||||
async def api_create_increment_invoice(
|
||||
payload: dict, user: User = Depends(check_user_exists)
|
||||
):
|
||||
await _require_permission(user.id, ext_id, "ext.db.read_write")
|
||||
await _require_permission(user.id, ext_id, "lnbits.invoice.create")
|
||||
await _require_permission(user.id, ext_id, "lnbits.payments.subscribe")
|
||||
_check_quota(
|
||||
user.id, ext_id, "invoice", settings.lnbits_wasm_max_invoice_ops_per_min
|
||||
)
|
||||
wallet_id = payload.get("wallet_id")
|
||||
memo = payload.get("memo") or f"{ext_id} increment"
|
||||
if not wallet_id:
|
||||
raise HTTPException(400, "Missing wallet_id")
|
||||
await _require_wallet_access(user.id, wallet_id)
|
||||
|
||||
try:
|
||||
amount = await wasm_call(
|
||||
ext_id, "get_increment_amount", [], upgrade_hash=ext.upgrade_hash
|
||||
)
|
||||
except WasmExecutionError as exc:
|
||||
raise HTTPException(500, str(exc)) from exc
|
||||
if not amount or amount < 0:
|
||||
raise HTTPException(400, "Invalid amount")
|
||||
|
||||
payment = await create_invoice(wallet_id=wallet_id, amount=amount, memo=memo)
|
||||
asyncio.create_task(
|
||||
_wait_for_increment_payment(
|
||||
ext_id, payment.payment_hash, db, ext.upgrade_hash
|
||||
)
|
||||
)
|
||||
return {
|
||||
"payment_hash": payment.payment_hash,
|
||||
"payment_request": payment.bolt11,
|
||||
"amount": payment.amount,
|
||||
}
|
||||
|
||||
@router.post("/api/v1/invoices/pay")
|
||||
async def api_pay_invoice(payload: dict, user: User = Depends(check_user_exists)):
|
||||
await _require_permission(user.id, ext_id, "lnbits.invoice.pay")
|
||||
@@ -211,6 +279,37 @@ def register_wasm_ext_routes(app, ext) -> None:
|
||||
"status": payment.status,
|
||||
}
|
||||
|
||||
@router.post("/api/v1/proxy")
|
||||
async def api_proxy(payload: dict, req: Request, user: User = Depends(check_user_exists)):
|
||||
method = str(payload.get("method", "GET")).upper()
|
||||
path = str(payload.get("path", "")).strip()
|
||||
if not path.startswith("/") or "://" in path:
|
||||
raise HTTPException(400, "Invalid path")
|
||||
if path.startswith(proxy_block):
|
||||
raise HTTPException(400, "Proxy loop blocked")
|
||||
if method not in {"GET", "POST", "PUT", "PATCH", "DELETE"}:
|
||||
raise HTTPException(400, "Unsupported method")
|
||||
|
||||
await _require_permission(user.id, ext_id, f"api.{method}:{path}")
|
||||
|
||||
headers = {}
|
||||
for key in ("x-api-key", "authorization", "content-type", "accept"):
|
||||
if key in req.headers:
|
||||
headers[key] = req.headers[key]
|
||||
|
||||
query = payload.get("query") or {}
|
||||
body = payload.get("body")
|
||||
|
||||
async with httpx.AsyncClient(app=app, base_url="http://lnbits") as client:
|
||||
resp = await client.request(
|
||||
method, path, params=query, json=body, headers=headers
|
||||
)
|
||||
return Response(
|
||||
content=resp.content,
|
||||
status_code=resp.status_code,
|
||||
media_type=resp.headers.get("content-type"),
|
||||
)
|
||||
|
||||
@router.websocket("/api/v1/events/ws")
|
||||
async def events_ws(websocket: WebSocket, api_key: str = Query(default="")):
|
||||
await websocket.accept()
|
||||
@@ -249,6 +348,8 @@ def register_wasm_ext_routes(app, ext) -> None:
|
||||
|
||||
prefix = f"/upgrades/{ext.upgrade_hash}" if ext.upgrade_hash else ""
|
||||
app.include_router(router, prefix=prefix)
|
||||
|
||||
|
||||
_quota_events: dict[tuple[str, str, str], list[float]] = {}
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import asyncio
|
||||
|
||||
from wasmtime import (
|
||||
Caller,
|
||||
Config,
|
||||
@@ -108,11 +107,14 @@ def _load_module(module_path: Path, ext_id: str):
|
||||
config.consume_fuel = True
|
||||
engine = Engine(config)
|
||||
store = Store(engine)
|
||||
store.add_fuel(settings.lnbits_wasm_fuel)
|
||||
if hasattr(store, "add_fuel"):
|
||||
store.add_fuel(settings.lnbits_wasm_fuel)
|
||||
module = Module.from_file(engine, str(module_path))
|
||||
db = Database(f"ext_{ext_id}")
|
||||
|
||||
def db_get(caller: Caller, key_ptr: int, key_len: int, out_ptr: int, out_len: int) -> int:
|
||||
def db_get(
|
||||
caller: Caller, key_ptr: int, key_len: int, out_ptr: int, out_len: int
|
||||
) -> int:
|
||||
return _db_get(db, ext_id, caller, key_ptr, key_len, out_ptr, out_len)
|
||||
|
||||
def db_set(
|
||||
@@ -126,7 +128,10 @@ def _load_module(module_path: Path, ext_id: str):
|
||||
"db_get",
|
||||
Func(
|
||||
store,
|
||||
FuncType([ValType.i32(), ValType.i32(), ValType.i32(), ValType.i32()], [ValType.i32()]),
|
||||
FuncType(
|
||||
[ValType.i32(), ValType.i32(), ValType.i32(), ValType.i32()],
|
||||
[ValType.i32()],
|
||||
),
|
||||
db_get,
|
||||
),
|
||||
)
|
||||
@@ -135,7 +140,10 @@ def _load_module(module_path: Path, ext_id: str):
|
||||
"db_set",
|
||||
Func(
|
||||
store,
|
||||
FuncType([ValType.i32(), ValType.i32(), ValType.i32(), ValType.i32()], [ValType.i32()]),
|
||||
FuncType(
|
||||
[ValType.i32(), ValType.i32(), ValType.i32(), ValType.i32()],
|
||||
[ValType.i32()],
|
||||
),
|
||||
db_set,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -67,14 +67,23 @@ async def wasm_call(
|
||||
proc.kill()
|
||||
raise WasmExecutionError("WASM execution timed out") from exc
|
||||
|
||||
payload = {}
|
||||
if stdout:
|
||||
try:
|
||||
payload = json.loads(stdout.decode())
|
||||
except json.JSONDecodeError:
|
||||
payload = {}
|
||||
|
||||
if proc.returncode != 0:
|
||||
detail = stderr.decode().strip() if stderr else "WASM runner error"
|
||||
detail = payload.get("error")
|
||||
if not detail and stderr:
|
||||
detail = stderr.decode().strip()
|
||||
if not detail:
|
||||
detail = "WASM runner error"
|
||||
raise WasmExecutionError(detail)
|
||||
|
||||
try:
|
||||
payload = json.loads(stdout.decode()) if stdout else {}
|
||||
except json.JSONDecodeError as exc:
|
||||
raise WasmExecutionError("Invalid WASM runner output") from exc
|
||||
if not payload:
|
||||
raise WasmExecutionError("Invalid WASM runner output")
|
||||
|
||||
if not payload.get("ok"):
|
||||
raise WasmExecutionError(payload.get("error", "WASM execution failed"))
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ class ExtensionsSettings(LNbitsSettings):
|
||||
lnbits_extensions_builder_manifest_url: str = Field(
|
||||
default="https://raw.githubusercontent.com/lnbits/extension_builder_stub/refs/heads/main/manifest.json"
|
||||
)
|
||||
lnbits_wasm_timeout_seconds: float = Field(default=1.0, ge=0.1)
|
||||
lnbits_wasm_timeout_seconds: float = Field(default=3.0, ge=0.1)
|
||||
lnbits_wasm_fuel: int = Field(default=50_000, ge=1_000)
|
||||
lnbits_wasm_max_module_bytes: int = Field(default=1_000_000, ge=0)
|
||||
lnbits_wasm_max_db_ops_per_min: int = Field(default=120, ge=0)
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -255,8 +255,13 @@ window.PageExtensions = {
|
||||
},
|
||||
async enableExtensionForUser(extension) {
|
||||
if (extension.permissions && extension.permissions.length) {
|
||||
this.openPermissionsDialog(extension)
|
||||
return
|
||||
if (!extension._grantedPermissions) {
|
||||
Quasar.Notify.create({
|
||||
type: 'warning',
|
||||
message: 'Save permissions before enabling this extension.'
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if (extension.isPaymentRequired) {
|
||||
this.showPayToEnable(extension)
|
||||
@@ -329,7 +334,19 @@ window.PageExtensions = {
|
||||
this.permissionsDialog.extension = null
|
||||
this.permissionsDialog.checked = []
|
||||
},
|
||||
confirmPermissionsDialog() {
|
||||
openPermissionsForExtension(extension) {
|
||||
if (!extension.permissions || !extension.permissions.length) {
|
||||
Quasar.Notify.create({
|
||||
type: 'warning',
|
||||
message: 'This extension does not declare permissions.'
|
||||
})
|
||||
return
|
||||
}
|
||||
this.permissionsDialog.extension = extension
|
||||
this.permissionsDialog.checked = []
|
||||
this.permissionsDialog.show = true
|
||||
},
|
||||
async confirmPermissionsDialog() {
|
||||
const ext = this.permissionsDialog.extension
|
||||
const granted = this.permissionsDialog.checked.slice()
|
||||
this.permissionsDialog.show = false
|
||||
@@ -337,10 +354,19 @@ window.PageExtensions = {
|
||||
this.permissionsDialog.checked = []
|
||||
if (!ext) return
|
||||
ext._grantedPermissions = granted
|
||||
if (ext.isPaymentRequired) {
|
||||
this.showPayToEnable(ext, granted)
|
||||
} else {
|
||||
this.enableExtension(ext, granted)
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'PUT',
|
||||
`/api/v1/extension/${ext.id}/permissions`,
|
||||
this.g.user.wallets[0].adminkey,
|
||||
{permissions: granted}
|
||||
)
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Permissions saved.'
|
||||
})
|
||||
} catch (err) {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
}
|
||||
},
|
||||
updatePayToInstallData(extension) {
|
||||
@@ -881,6 +907,11 @@ window.PageExtensions = {
|
||||
async fetchAllExtensions() {
|
||||
try {
|
||||
const {data} = await LNbits.api.request('GET', `/api/v1/extension/all`)
|
||||
data.forEach(ext => {
|
||||
if (ext.grantedPermissions && ext.grantedPermissions.length) {
|
||||
ext._grantedPermissions = ext.grantedPermissions
|
||||
}
|
||||
})
|
||||
return data
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
|
||||
@@ -329,6 +329,19 @@
|
||||
<span v-text="$t('enable_extension_details')">
|
||||
</span> </q-tooltip
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-if="
|
||||
extension.isInstalled &&
|
||||
extension.isActive &&
|
||||
!g.user.extensions.includes(extension.id) &&
|
||||
extension.permissions &&
|
||||
extension.permissions.length
|
||||
"
|
||||
flat
|
||||
color="grey-5"
|
||||
@click="openPermissionsForExtension(extension)"
|
||||
label="Permissions"
|
||||
></q-btn>
|
||||
|
||||
<q-btn
|
||||
@click="showManageExtension(extension)"
|
||||
@@ -925,9 +938,7 @@
|
||||
<q-card class="q-pa-md" style="min-width: 360px; max-width: 90vw">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Permissions required</div>
|
||||
<div class="text-caption text-grey">
|
||||
This extension can:
|
||||
</div>
|
||||
<div class="text-caption text-grey">This extension can:</div>
|
||||
<q-list v-if="permissionsDialog.extension">
|
||||
<q-item
|
||||
v-for="perm in permissionsDialog.extension.permissions"
|
||||
@@ -954,7 +965,7 @@
|
||||
<q-btn
|
||||
color="primary"
|
||||
:disable="!permissionsAllChecked"
|
||||
v-text="$t('enable')"
|
||||
label="Save"
|
||||
@click="confirmPermissionsDialog"
|
||||
></q-btn>
|
||||
</q-card-actions>
|
||||
|
||||
Reference in New Issue
Block a user