Audited all 143 plan files across the 10 plans/ folders. Each plan now carries a Status header (shipped | in-progress | queued | abandoned) backed by codebase evidence, and every folder has a README.md index grouping plans by status. Shipped plans were moved into a per-folder plans/archive/ (via git mv, history preserved) so each plans/ folder surfaces only live work: shipped (archived): 122 in-progress: 8 queued: 7 abandoned: 4 docs/plans/ is the frozen legacy folder; its plans were stamped and indexed in place (48 of 52 archived) but it remains closed to new plans. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016hpUivtmq4pgzqRbY6MYrA
7.2 KiB
Desktop napplet / nsite host — implementation plan
Status: in-progress — the three shared extractions are DONE (
NappletRequestRouter,NappletWebContract,StaticWebsiteCardin commons), but the desktop edge (KCEF/JCEF engine, scheme handler, transport, gateways, host UI) has no code indesktopApp/. 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, SPAindex.htmlnormalization),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 permissionsLedger/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 (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
- 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.
- 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.
- 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:
- DONE:
NappletRequestRouter(commons/jvmAndroid). The orchestration (readType →relay.close/resource.cancelshort-circuit → decode →broker.handle→ encode reply / detectSubscribed) used to live in Android'sNappletBrokerService.handleMessage. It is now a pure router returning a smallOutcome(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 incommons/jvmTest(NappletRequestRouterTest); Android's service consumes it via awhen (outcome)dispatch. The desktop host will route the exact same way. - DONE: Shared web assets + contract.
shell.html+shim.jsnow live incommons/commonMain/composeResources/files/napplet/and are read viaRes.readBytes("files/napplet/..."). A newcommons/commonMain/.../napplet/NappletWebContractsingle-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 inonCreateand reads every origin/CSP constant fromNappletWebContract; 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,LocalUriHandlerfor links, inlined card chrome), parameterized byisNappletand anonOpenlaunch slot. Android'snote/types/StaticWebsite.ktis now a thin set of event→card adapters; the desktop feed renders the identical card and supplies its ownonOpen.
Security parity checklist (desktop must match Android)
- Opaque-origin
sandbox="allow-scripts"iframe (noallow-same-origin). - CSP
connect-src 'none'(applet has no direct network) +default-srclocked 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.