Files
amethyst/desktopApp/plans/2026-06-21-napplet-desktop-host.md
Claude 3d03c82970 fix: audit follow-ups for the commons move - Compose string whitespace, backup labels, store edge cases
The one real regression predates this branch: Compose resources draw a value's
XML whitespace verbatim, where aapt collapsed it. Values wrapped over indented
lines rendered with a leading line break and eight spaces, which turns the
account-backup tips' markdown heading into a code block, and Crowdin's stray
translator edge spaces (" miejsca zniknęły", Hindi's " दि॰" suffix) showed up.
fix_escapes.py now applies aapt's rule to line-break/tab runs and trims an
edge space a translation has but its source lacks. The Crowdin workflow
already runs it after each sync, and compose_escaping_check.py now fails on raw
line breaks. 598 values across 54 files repaired.

Also:
- rememberPresentation resolves only the labels its diff type writes, instead
  of all 16 on every recomposition. A test pins the list against presentationOf.
- ScheduledPostStore: a failed stat is "no file" (okio posix throws on EACCES
  where File.exists() did not), symlinked stores still count, no chmod against
  an injected non-system FileSystem, the cleanup log keeps its throwable.
- ClickableEmail percent-encodes '%' in the mailto: URI.
- Dropped a misleading WatchScrollToTop import, a stray blank line, and a stale
  doc path; noted the Base64 pad-bit difference beside the decoder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjQQN9CgWHVtNCSWqnKoqY
2026-09-26 22:03:54 +00:00

7.2 KiB

Desktop napplet / nsite host — implementation plan

Status: in-progress — the three shared extractions are DONE (NappletRequestRouter, NappletWebContract, StaticWebsiteCard in commons), but the desktop edge (KCEF/JCEF engine, scheme handler, transport, gateways, host UI) has no code in desktopApp/. Audited 2026-06-30.

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/commonMain) — the wire codec. Desktop's host marshals through the same object, so request/result/push shapes can never drift between platforms.

The web contract (commons/commonMain/composeResources/files/napplet/ + NappletWebContract) — shell.html (the trusted shell page that hosts the applet in an opaque-origin sandbox="allow-scripts" iframe and bridges object↔string), shim.js (the injected window.napplet.*), and NappletWebContract (the origin/URLs + both CSP strings + shell/shim loaders). Reused byte-for-byte — they embody the envelope/push contract. Desktop reads the same Res.readBytes(...) and serves the same CSP headers.

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.

These reduce desktop reimplementation and prevent drift:

  • DONE: NappletRequestRouter (commons/jvmAndroid). The orchestration (readType → relay.close / resource.cancel short-circuit → decode → broker.handle → encode reply / detect Subscribed) used to live in Android's NappletBrokerService.handleMessage. It is now a pure router returning a small Outcome (Ignore / Reply(payload) / OpenSubscription(subId, filters) / CloseSubscription(subId) / Push(payloads)), so both hosts share the brain and only supply the broker + transport + live relay subscription. Unit-tested in commons/jvmTest (NappletRequestRouterTest); Android's service consumes it via a when (outcome) dispatch. The desktop host will route the exact same way.
  • DONE: Shared web assets + contract. shell.html + shim.js now live in commons/commonMain/composeResources/files/napplet/ and are read via Res.readBytes("files/napplet/..."). A new commons/commonMain/.../napplet/NappletWebContract single-sources the whole web contract: the shell/shim loaders plus the origin/host/URLs and both CSP strings (SHELL_CSP, APP_CSP). Android's host preloads the bytes in onCreate and reads every origin/CSP constant from NappletWebContract; the desktop scheme handler will serve the same bytes + headers.
  • DONE: The inert feed card. The preview card is now commons/commonMain/.../ui/note/StaticWebsiteCard — self-contained (commons compose-resource strings, LocalUriHandler for links, inlined card chrome), parameterized by isNapplet and an onOpen launch slot. Android's note/types/StaticWebsite.kt is now a thin set of event→card adapters; the desktop feed renders the identical card and supplies its own onOpen.

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.