mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 08:04:45 +00:00
refactor(napplet): move wire codec to commons for desktop reuse + desktop host plan
Set the desktopApp up to host napplets/nsites by maximizing the shared core and documenting the edge it must build. - Moved NappletProtocolJson (the wire codec) from amethyst to commons/jvmAndroid (package ...commons.napplet.protocol), next to the NappletRequest/Response types it marshals. It depends only on quartz + kotlinx.serialization + java.util.Base64 (Android 26+/JVM), so a future desktop host marshals through the identical object — request/result/push shapes can't drift between platforms. amethyst host/service/tests updated to import it; tests stay in amethyst and still exercise it. - Added desktopApp/plans/2026-06-21-napplet-desktop-host.md: what's already shared (broker, protocol, codec, resolver, the shell.html/shim.js web contract), what desktop must build (KCEF/JCEF engine, custom-scheme serving, isolation, transport, gateways, UI), the decisions to make, a security-parity checklist, and recommended further extractions (NappletRequestRouter, shared web assets, the inert feed card). commons:jvmTest and the amethyst napplet suite pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]. */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+9
-6
@@ -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": "<domain>.<action>", "id", ...fields }` and replies
|
||||
* are `{ "type": "<domain>.<action>.result", "id", "ok", ...fields }`.
|
||||
* (`@napplet/shim` / `@napplet/nap`): requests are `{ "type": "<domain>.<action>", "id", ...fields }`
|
||||
* and replies are `{ "type": "<domain>.<action>.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 {
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user