diff --git a/amethyst/plans/2026-06-21-napplet-code-audit.md b/amethyst/plans/2026-06-21-napplet-code-audit.md index 1673a401ed..f14f94bd27 100644 --- a/amethyst/plans/2026-06-21-napplet-code-audit.md +++ b/amethyst/plans/2026-06-21-napplet-code-audit.md @@ -44,12 +44,11 @@ All compile; `commons:jvmTest` + the amethyst napplet suite stay green. `commonMain`. No app policy leaked in. - **commons (shared logic): mostly correct.** Broker, capability, identity, request/response, permissions ledger/store, and gateway interfaces are in `commonMain` — right home. -- **Recommended move: `NappletProtocolJson` → `commons/jvmAndroid`.** It's pure wire-marshalling - (kotlinx.serialization + `java.util.Base64`) over the commons protocol types; commons already has - a `jvmAndroid` source set and depends on `kotlinx.serialization.json`. Moving it (and its test) - co-locates the codec with the protocol and lets a future **desktop** napplet host reuse it. Modest - churn (the test is JUnit4 → would move to commons `jvmTest`), no functional gain today, so it's a - *recommended*, not urgent, refactor. (Left in `amethyst/` for now.) +- **DONE: `NappletProtocolJson` → `commons/jvmAndroid`.** Moved to + `commons/.../napplet/protocol/` (next to the types it marshals) so the future **desktop** host + reuses the exact wire codec. Its tests stay in `amethyst` (JUnit4) for now and still exercise it + via the commons dependency; converting them to `kotlin.test` and moving to commons `jvmTest` is a + small follow-up. See `desktopApp/plans/2026-06-21-napplet-desktop-host.md`. - **amethyst (Android-only): correctly platform-bound.** `NappletHostActivity` (WebView/process), `NappletBrokerService` (Service/Messenger/account), the gateway *implementations* (account, `BlossomUploader`, DataStore, NWC), `NappletLauncher`, consent UI, `NappletIpc`, and the screens diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt index b85bc44d97..cf71084d97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt @@ -47,6 +47,7 @@ import com.vitorpamplona.amethyst.commons.napplet.NappletUploadGateway import com.vitorpamplona.amethyst.commons.napplet.NappletUploadResult import com.vitorpamplona.amethyst.commons.napplet.NappletWalletGateway import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger +import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletProtocolJson import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletRequest import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletResponse import com.vitorpamplona.amethyst.model.Account diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletHostActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletHostActivity.kt index 2f220de985..49cb0f70ac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletHostActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletHostActivity.kt @@ -43,6 +43,7 @@ import androidx.webkit.WebMessageCompat import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletProtocolJson import com.vitorpamplona.amethyst.commons.napplet.resolveRequiredCapabilities import com.vitorpamplona.quartz.nip5aStaticWebsites.resolver.BlobFetcher import com.vitorpamplona.quartz.nip5aStaticWebsites.resolver.StaticSiteResolution diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletIpc.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletIpc.kt index 06fcd6333b..487ce7bd6b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletIpc.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletIpc.kt @@ -24,7 +24,7 @@ package com.vitorpamplona.amethyst.napplet * The Messenger wire contract between the untrusted `:napplet` process (the WebView host) and * the main-process [NappletBrokerService]. Kept tiny and string-only on purpose: nothing the * applet controls is ever interpreted as a Binder object, and the only payloads are JSON - * strings ([NappletProtocolJson]) plus the applet's identity coordinate. + * strings (`NappletProtocolJson`, in commons) plus the applet's identity coordinate. */ object NappletIpc { /** Host → broker: a capability request. Carries [KEY_REQUEST_ID], the identity keys, and [KEY_PAYLOAD]. */ diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/napplet/NappletProtocolJsonTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/napplet/NappletProtocolJsonTest.kt index a332c193ba..9fc041b421 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/napplet/NappletProtocolJsonTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/napplet/NappletProtocolJsonTest.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.napplet import com.vitorpamplona.amethyst.commons.napplet.NappletCapability +import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletProtocolJson import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletRequest import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletResponse import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/napplet/NappletSdkConformanceTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/napplet/NappletSdkConformanceTest.kt index 50e08355f7..7e4b0acd65 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/napplet/NappletSdkConformanceTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/napplet/NappletSdkConformanceTest.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.napplet import com.vitorpamplona.amethyst.commons.napplet.NappletCapability +import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletProtocolJson import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletRequest import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletResponse import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletProtocolJson.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/napplet/protocol/NappletProtocolJson.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletProtocolJson.kt rename to commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/napplet/protocol/NappletProtocolJson.kt index 82c3fb603b..4eddf792bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletProtocolJson.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/napplet/protocol/NappletProtocolJson.kt @@ -18,10 +18,8 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.napplet +package com.vitorpamplona.amethyst.commons.napplet.protocol -import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletRequest -import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletResponse import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import kotlinx.serialization.json.Json @@ -42,12 +40,17 @@ import java.util.Base64 /** * Marshals the KMP-pure [NappletRequest] / [NappletResponse] types to and from the wire the * applet exchanges over `window.napplet.*`. The envelope matches the upstream napplet SDK - * (`@napplet/web`): requests are `{ "type": ".", "id", ...fields }` and replies - * are `{ "type": "..result", "id", "ok", ...fields }`. + * (`@napplet/shim` / `@napplet/nap`): requests are `{ "type": ".", "id", ...fields }` + * and replies are `{ "type": "..result", "id", ...fields }`. + * + * Lives in `commons/jvmAndroid` (not in any single front end) because the wire contract is identical + * across hosts: the Android `:napplet` WebView host and a future desktop host both marshal through + * here. It depends only on `quartz` (Event/Filter), kotlinx.serialization, and `java.util.Base64` + * (available on Android API 26+ and the JVM) — no platform-UI or process APIs. * * This is the only place the boundary parses untrusted applet input, so it is deliberately * strict: an unrecognized `type` decodes to `null` (the broker denies it) and a malformed/short - * body throws (the broker wraps it into a `Failed`). Uses kotlinx.serialization so it is + * body throws (the host wraps it into a `Failed`). Uses kotlinx.serialization so it is * unit-testable off-device. */ object NappletProtocolJson { diff --git a/desktopApp/plans/2026-06-21-napplet-desktop-host.md b/desktopApp/plans/2026-06-21-napplet-desktop-host.md new file mode 100644 index 0000000000..866d3befc8 --- /dev/null +++ b/desktopApp/plans/2026-06-21-napplet-desktop-host.md @@ -0,0 +1,80 @@ +# Desktop napplet / nsite host — implementation plan + +**Date:** 2026-06-21. Goal: a desktop NIP-5A (nsite) + NIP-5D (napplet) host that **reuses the +shared core** the Android host already runs on, so the two stay wire- and policy-identical. This +doc is the map: what already works for free, what desktop must build, and the decisions to make. + +## What is already shared (reuse verbatim) + +**quartz (`commonMain`)** — protocol, no work needed: +- NIP-5A/5D events, `NappletManifest`, `StaticSiteResolver` (path → hash resolution + per-blob + sha256 verify), `StaticSitePathLookup` (`sniffContentType`, SPA `index.html` normalization), + `SiteAggregateHash`. + +**commons (`commonMain` / `jvmAndroid`)** — the security brain + wire, already platform-agnostic: +- `NappletBroker` (the trust boundary: declaration gate → ledger → consent → execute), `NappletCapability`, + `NappletIdentity`, `NappletRequest`/`NappletResponse`, the permissions `Ledger`/`Store`/`GrantState`, + and the gateway **interfaces** (`NappletRelayGateway`, `NappletStorage`, `NappletWalletGateway`, + `NappletResourceGateway`, `NappletUploadGateway`, `NappletIdentityGateway`, `NappletConsentPrompt`). +- **`NappletProtocolJson`** (`commons/jvmAndroid`) — the wire codec. Desktop's host marshals through + the same object, so request/result/push shapes can never drift between platforms. + +**The web contract (`amethyst/assets/napplet/`)** — `shell.html` (the trusted shell page that hosts +the applet in an opaque-origin `sandbox="allow-scripts"` iframe and bridges object↔string) and +`shim.js` (the injected `window.napplet.*`). **These must be reused byte-for-byte** — they embody +the envelope/push contract. See "Shared web assets" below for how to share them. + +## What desktop must build (platform-specific) + +| Concern | Android (today) | Desktop (to build) | +|---|---|---| +| **Web engine** | Android `WebView` (Chromium) | **KCEF/JCEF** (Chromium Embedded for the JVM). Gives the same CSP, opaque-origin iframe, and custom-scheme interception we rely on. Compose's experimental WebView is too limited. | +| **Isolation** | separate `:napplet` OS process (no keys) | The CEF **renderer is already sandboxed**; combine with the same CSP (`connect-src 'none'`) + no-`allow-same-origin` iframe. For parity with Android's process model, evaluate running CEF in a **child JVM process**; at minimum rely on the renderer sandbox + CSP. | +| **Resource serving** | `WebViewClient.shouldInterceptRequest` → shell + verified blobs | a CEF **custom scheme handler** that serves `https://napplet.local/__shell__` + `/app/*` from `StaticSiteResolver` with the identical CSP headers. | +| **Transport** | `Messenger` across processes | in-process: a direct bridge (CEF JS-query ↔ broker). If child-process: stdio/socket carrying the **same JSON envelopes**. Either way, the payloads are `NappletProtocolJson`. | +| **Live subscriptions** | `INostrClient.subscribe` + push over Messenger | same `INostrClient.subscribe`; push `relay.event/eose/closed` over the desktop transport. | +| **Gateways** | impls bound to `Account`/`BlossomUploader`/DataStore/NWC in `NappletBrokerService` | implement the same interfaces against the desktop account + relay client + uploader (the back end is shared). | +| **Entry point** | Activity + bottom-nav; feed card | a desktop window/pane + sidebar; the same inert feed card. | + +## Decisions to make first + +1. **Web engine:** KCEF (Kotlin wrapper over JCEF) vs raw JCEF. KCEF is the lighter integration for + Compose Desktop. Confirm license (JCEF/CEF is BSD — permissive, OK) before adding the dependency. +2. **Isolation model:** renderer-sandbox-only (simpler) vs CEF-in-child-process (closer to Android's + keyless-process guarantee). Start with renderer + CSP; treat child-process as a hardening follow-up. +3. **Transport:** in-process bridge (fine if isolation is renderer-only) vs child-process IPC. + +## Recommended shared extractions (do as part of, or just before, desktop work) + +These reduce desktop reimplementation and prevent drift. None are done yet: + +- **`NappletRequestRouter` (commons/jvmAndroid).** Today the orchestration (readType → `relay.close` + / `resource.cancel` short-circuit → decode → `broker.handle` → encode reply / detect + `Subscribed`) lives in Android's `NappletBrokerService.handleMessage`. Extract it into a pure + router returning a small `Outcome` (`Reply(payload)` / `OpenSubscription(subId, filters)` / + `CloseSubscription(subId)`), so both hosts share the brain and only supply transport + relay + client. It's unit-testable in commons. +- **Shared web assets.** Move `shell.html` + `shim.js` into `commons/commonMain/composeResources/files/napplet/` + and read them via the generated `Res.readBytes("files/napplet/...")` on both platforms (the + Material Symbols font already lives in commons composeResources). This makes the web contract + single-sourced. (Android's host then reads them via `Res` instead of `assets.open`.) +- **The inert feed card.** `RenderStaticWebsite` (+ the napplet/nsite branches) in + `amethyst/.../note/types/StaticWebsite.kt` is plain Compose; move it to `commons` so the desktop + feed renders the identical card and launches the desktop host. + +## Security parity checklist (desktop must match Android) + +- Opaque-origin `sandbox="allow-scripts"` iframe (no `allow-same-origin`). +- CSP `connect-src 'none'` (applet has no direct network) + `default-src` locked to the internal origin. +- Serve **only** the manifest's declared paths, each **sha256-verified** before serving (reuse `StaticSiteResolver`). +- The web engine/renderer holds **no keys**; all signing/consent stays in the broker. +- Capability **declaration gate** + consent + signer-aware deferral (all already in `NappletBroker`). +- Foreground-only execution (pause the engine when the pane is hidden) — mirror Android's `onPause`. +- Route external links to the system browser only on a user gesture; never navigate the sandbox away. + +## Status + +Shared core (broker, protocol, codec, resolver) is ready and unit-tested. The codec now lives in +`commons/jvmAndroid` specifically so this desktop host can consume it. The remaining work is the +desktop **edge** (engine + scheme handler + transport + gateways + UI) plus the three recommended +extractions above — and on-device/desktop verification of the whole round-trip.