mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 19:53:08 +00:00
refactor: replace napplet host websiteMode boolean with a HostProfile type
The "locked nApplet vs open nSite" distinction was a `websiteMode: Boolean` threaded through an Intent extra and re-branched at eight independent sites across the launcher, the content server, both host surfaces, and the chrome. That is one coupled security posture (capabilities, CSP, NIP-07 injection, off-origin policy, network UI) expressed as scattered, drift-prone flags — and boolean-blind, since the "website" is actually the *more* capable mode. Introduce `HostProfile` (NAPPLET | WEBSITE), resolved once in the trusted main process and carried over the Intent/Messenger boundary as its name. Every coupled consequence now reads from one place: - `declaredCapabilities(requires)` — THE broker grant, minted into the token - `appCsp` / `injectsNip07` / `allowsOffOrigin` / `exposesNetwork` Pure mechanical mapping (every branch 1:1, no behavior change). The wire extra `EXTRA_WEBSITE_MODE` boolean becomes `EXTRA_HOST_PROFILE` string — safe, as it is in-app process-to-process IPC, never persisted. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HrxLCMcQADUPnm8Sj9ZJ63
This commit is contained in:
@@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.ThemeType
|
||||
import com.vitorpamplona.amethyst.napplet.NappletLauncher
|
||||
import com.vitorpamplona.amethyst.napplet.WebAppNetworkRegistry
|
||||
import com.vitorpamplona.amethyst.napplethost.HostProfile
|
||||
import com.vitorpamplona.amethyst.napplethost.NappletBrowserActivity
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent
|
||||
@@ -117,7 +118,7 @@ object FavoriteAppLauncher {
|
||||
aggregateHash = null,
|
||||
title = event.title() ?: "nsite",
|
||||
requires = emptyList(),
|
||||
websiteMode = true,
|
||||
profile = HostProfile.WEBSITE,
|
||||
)
|
||||
is NamedSiteEvent ->
|
||||
NappletLauncher.launch(
|
||||
@@ -129,7 +130,7 @@ object FavoriteAppLauncher {
|
||||
aggregateHash = null,
|
||||
title = event.title() ?: event.identifier(),
|
||||
requires = emptyList(),
|
||||
websiteMode = true,
|
||||
profile = HostProfile.WEBSITE,
|
||||
)
|
||||
else -> {
|
||||
Log.w("FavoriteAppLauncher", "Favorited app not resolvable yet: $coordinate")
|
||||
@@ -160,7 +161,7 @@ object FavoriteAppLauncher {
|
||||
event.declaredAggregateHash() ?: event.computeAggregateHash(),
|
||||
event.title() ?: "Napplet",
|
||||
event.requires(),
|
||||
false,
|
||||
HostProfile.NAPPLET,
|
||||
)
|
||||
is NamedNappletEvent ->
|
||||
NappletLauncher.buildLaunchParams(
|
||||
@@ -172,7 +173,7 @@ object FavoriteAppLauncher {
|
||||
event.declaredAggregateHash() ?: event.computeAggregateHash(),
|
||||
event.title() ?: event.identifier(),
|
||||
event.requires(),
|
||||
false,
|
||||
HostProfile.NAPPLET,
|
||||
)
|
||||
is RootSiteEvent ->
|
||||
NappletLauncher.buildLaunchParams(
|
||||
@@ -184,7 +185,7 @@ object FavoriteAppLauncher {
|
||||
null,
|
||||
event.title() ?: "nsite",
|
||||
emptyList(),
|
||||
true,
|
||||
HostProfile.WEBSITE,
|
||||
)
|
||||
is NamedSiteEvent ->
|
||||
NappletLauncher.buildLaunchParams(
|
||||
@@ -196,7 +197,7 @@ object FavoriteAppLauncher {
|
||||
null,
|
||||
event.title() ?: event.identifier(),
|
||||
emptyList(),
|
||||
true,
|
||||
HostProfile.WEBSITE,
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -25,11 +25,10 @@ import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.napplet.NappletCapability
|
||||
import com.vitorpamplona.amethyst.commons.napplet.NappletIdentity
|
||||
import com.vitorpamplona.amethyst.commons.napplet.resolveRequiredCapabilities
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.ThemeType
|
||||
import com.vitorpamplona.amethyst.napplethost.HostProfile
|
||||
import com.vitorpamplona.amethyst.napplethost.NappletHostActivity
|
||||
import com.vitorpamplona.amethyst.napplethost.NappletHostContract
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
@@ -74,12 +73,12 @@ object NappletLauncher {
|
||||
aggregateHash: HexKey?,
|
||||
title: String,
|
||||
requires: List<String>,
|
||||
// nSites open in "website mode": a NIP-07 window.nostr provider + normal network. The broker
|
||||
// then grants the IDENTITY + RELAY capabilities NIP-07 needs (consent-gated), regardless of the
|
||||
// (empty) manifest `requires`. Napplets pass false and keep their declared-only, locked sandbox.
|
||||
websiteMode: Boolean = false,
|
||||
// nSites open as [HostProfile.WEBSITE]: a NIP-07 window.nostr provider + normal network. The
|
||||
// broker then grants the IDENTITY + RELAY capabilities NIP-07 needs (consent-gated), regardless
|
||||
// of the (empty) manifest `requires`. Napplets keep the default locked [HostProfile.NAPPLET].
|
||||
profile: HostProfile = HostProfile.NAPPLET,
|
||||
) {
|
||||
val params = buildLaunchParams(context, paths, servers, authorPubKey, identifier, aggregateHash, title, requires, websiteMode)
|
||||
val params = buildLaunchParams(context, paths, servers, authorPubKey, identifier, aggregateHash, title, requires, profile)
|
||||
val intent =
|
||||
Intent(context, NappletHostActivity::class.java).apply {
|
||||
putExtras(params)
|
||||
@@ -104,7 +103,7 @@ object NappletLauncher {
|
||||
aggregateHash: HexKey?,
|
||||
title: String,
|
||||
requires: List<String>,
|
||||
websiteMode: Boolean,
|
||||
profile: HostProfile,
|
||||
): Bundle {
|
||||
val proxyPort = Amethyst.instance.torManager.activePortOrNull.value ?: -1
|
||||
|
||||
@@ -120,18 +119,13 @@ object NappletLauncher {
|
||||
// Mint the launch token in the (trusted) main process: the broker resolves the sandbox's
|
||||
// requests back to THIS identity + declared set, regardless of anything the sandbox sends.
|
||||
val identity = NappletIdentity(authorPubKey = authorPubKey, identifier = identifier, aggregateHash = aggregateHash)
|
||||
val declared =
|
||||
if (websiteMode) {
|
||||
setOf(NappletCapability.IDENTITY, NappletCapability.RELAY)
|
||||
} else {
|
||||
resolveRequiredCapabilities(requires).capabilities.toSet()
|
||||
}
|
||||
val declared = profile.declaredCapabilities(requires)
|
||||
val launchToken = NappletLaunchRegistry.register(identity, declared)
|
||||
|
||||
// Resolve the per-site network choice (Tor default; a site can be opted out to the open web).
|
||||
// Locked napplets always keep Tor for their blob fetches — only nSites expose the toggle.
|
||||
NappletNetworkRegistry.init(context.applicationContext)
|
||||
val useTor = if (websiteMode) NappletNetworkRegistry.useTor(identity.coordinate) else true
|
||||
val useTor = if (profile.exposesNetwork) NappletNetworkRegistry.useTor(identity.coordinate) else true
|
||||
|
||||
// Resolve capability labels here (the app has the resources) so the sandbox module needs none.
|
||||
val capLabels = declared.map { context.getString(it.labelRes()) }
|
||||
@@ -159,7 +153,7 @@ object NappletLauncher {
|
||||
putStringArrayList(NappletHostContract.EXTRA_CAP_LABELS, ArrayList(capLabels))
|
||||
putString(NappletHostContract.EXTRA_LAUNCH_TOKEN, launchToken)
|
||||
putInt(NappletHostContract.EXTRA_PROXY_PORT, proxyPort)
|
||||
putBoolean(NappletHostContract.EXTRA_WEBSITE_MODE, websiteMode)
|
||||
putString(NappletHostContract.EXTRA_HOST_PROFILE, profile.name)
|
||||
putBoolean(NappletHostContract.EXTRA_USE_TOR, useTor)
|
||||
putString(NappletHostContract.EXTRA_THEME, theme)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.StaticWebsiteCard
|
||||
import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.napplet.NappletLauncher
|
||||
import com.vitorpamplona.amethyst.napplethost.HostProfile
|
||||
import com.vitorpamplona.amethyst.napplethost.NappletBlobPrefetcher
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -132,7 +133,7 @@ fun RenderRootSiteEvent(
|
||||
aggregateHash = null,
|
||||
title = event.title() ?: "nsite",
|
||||
requires = emptyList(),
|
||||
websiteMode = true,
|
||||
profile = HostProfile.WEBSITE,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -173,7 +174,7 @@ fun RenderNamedSiteEvent(
|
||||
aggregateHash = null,
|
||||
title = event.title() ?: event.identifier(),
|
||||
requires = emptyList(),
|
||||
websiteMode = true,
|
||||
profile = HostProfile.WEBSITE,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
||||
+5
-4
@@ -58,6 +58,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp
|
||||
import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher
|
||||
import com.vitorpamplona.amethyst.favorites.FavoriteAppsRegistry
|
||||
import com.vitorpamplona.amethyst.napplethost.HostProfile
|
||||
import com.vitorpamplona.amethyst.napplethost.NappletEmbedContract
|
||||
import com.vitorpamplona.amethyst.napplethost.NappletHostContract
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
||||
@@ -122,7 +123,7 @@ private fun EmbeddedNostrAppTab(
|
||||
|
||||
val title = params.getString(NappletHostContract.EXTRA_TITLE).orEmpty()
|
||||
val capLabels = params.getStringArrayList(NappletHostContract.EXTRA_CAP_LABELS).orEmpty()
|
||||
val websiteMode = params.getBoolean(NappletHostContract.EXTRA_WEBSITE_MODE, false)
|
||||
val profile = HostProfile.fromName(params.getString(NappletHostContract.EXTRA_HOST_PROFILE))
|
||||
val useTor = params.getBoolean(NappletHostContract.EXTRA_USE_TOR, true)
|
||||
|
||||
var canGoBack by remember { mutableStateOf(false) }
|
||||
@@ -199,7 +200,7 @@ private fun EmbeddedNostrAppTab(
|
||||
BackHandler(enabled = canGoBack) { controller.back() }
|
||||
|
||||
if (showAccess) {
|
||||
AccessDialog(title, capLabels, websiteMode, useTor) { showAccess = false }
|
||||
AccessDialog(title, capLabels, profile.exposesNetwork, useTor) { showAccess = false }
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
@@ -251,7 +252,7 @@ private fun UnavailableTab(
|
||||
private fun AccessDialog(
|
||||
title: String,
|
||||
capLabels: List<String>,
|
||||
websiteMode: Boolean,
|
||||
showsNetwork: Boolean,
|
||||
useTor: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
@@ -262,7 +263,7 @@ private fun AccessDialog(
|
||||
capLabels.joinToString("\n") { "• $it" }
|
||||
}
|
||||
val networkBody =
|
||||
if (websiteMode) {
|
||||
if (showsNetwork) {
|
||||
"\n\n" + stringResource(if (useTor) R.string.favorite_app_network_tor else R.string.favorite_app_network_open)
|
||||
} else {
|
||||
""
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* 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.napplethost
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.napplet.NappletCapability
|
||||
import com.vitorpamplona.amethyst.commons.napplet.NappletWebContract
|
||||
import com.vitorpamplona.amethyst.commons.napplet.resolveRequiredCapabilities
|
||||
|
||||
/**
|
||||
* The security posture a sandbox host renders under — the single decision that "locked nApplet vs
|
||||
* open nSite" actually *is*. Replaces the old scattered `websiteMode` boolean: resolved once in the
|
||||
* trusted main process, carried over the Intent/Messenger boundary as [name], and every coupled
|
||||
* consequence (capabilities, CSP, NIP-07, off-origin, network UI) reads from here — so they can't
|
||||
* drift out of sync, and a third posture is one new constant instead of several edited call sites.
|
||||
*/
|
||||
enum class HostProfile {
|
||||
/** NIP-5D napplet: locked sandbox — declared-only capabilities, strict app CSP, no off-origin, pinned to Tor. */
|
||||
NAPPLET,
|
||||
|
||||
/** NIP-5A nSite ("website mode"): a normal web app — NIP-07 provider, no app CSP, off-origin + per-site Tor toggle. */
|
||||
WEBSITE,
|
||||
;
|
||||
|
||||
/**
|
||||
* What this posture is allowed to ask the broker for — THE security decision, minted into the
|
||||
* launch token in the trusted main process. A website gets the IDENTITY + RELAY pair NIP-07 needs
|
||||
* (consent-gated); a locked napplet gets only what its manifest `requires` declares.
|
||||
*/
|
||||
fun declaredCapabilities(requires: List<String>): Set<NappletCapability> =
|
||||
when (this) {
|
||||
WEBSITE -> setOf(NappletCapability.IDENTITY, NappletCapability.RELAY)
|
||||
NAPPLET -> resolveRequiredCapabilities(requires).capabilities.toSet()
|
||||
}
|
||||
|
||||
/** Strict app CSP for a locked applet (connect-src 'none', etc.); null for a website, which sets its own. */
|
||||
val appCsp: String?
|
||||
get() = if (this == NAPPLET) NappletWebContract.APP_CSP else null
|
||||
|
||||
/** Install the NIP-07 `window.nostr` provider before the shim runs. */
|
||||
val injectsNip07: Boolean get() = this == WEBSITE
|
||||
|
||||
/** Let off-origin requests reach the network (a locked applet 404s them — its CSP is connect-src 'none'). */
|
||||
val allowsOffOrigin: Boolean get() = this == WEBSITE
|
||||
|
||||
/**
|
||||
* Apply the WebView SOCKS proxy and expose the Tor/network toggle. A website can re-route over Tor
|
||||
* or the open web; a locked napplet is pinned to Tor with no toggle.
|
||||
*/
|
||||
val exposesNetwork: Boolean get() = this == WEBSITE
|
||||
|
||||
companion object {
|
||||
/** Reconstruct from the wire [name], defaulting to the locked [NAPPLET] posture on anything unknown. */
|
||||
fun fromName(name: String?): HostProfile = entries.firstOrNull { it.name == name } ?: NAPPLET
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -57,9 +57,9 @@ class NappletContentServer(
|
||||
// The applet's own per-applet origin (a distinct napplet.local subdomain). The shell is on
|
||||
// NappletWebContract.ORIGIN; app blobs are served here so the applet has a real, isolated origin.
|
||||
private val appOrigin: String,
|
||||
// nSite "website mode": the applet is a normal web app — it gets a NIP-07 window.nostr provider,
|
||||
// normal network (no app CSP; off-origin requests defer to the WebView), unlike a locked napplet.
|
||||
private val websiteMode: Boolean = false,
|
||||
// The host posture: a WEBSITE nSite is a normal web app — NIP-07 window.nostr provider, normal
|
||||
// network (no app CSP; off-origin requests defer to the WebView), unlike a locked NAPPLET.
|
||||
private val profile: HostProfile = HostProfile.NAPPLET,
|
||||
// Embedded surfaces (a windowless Service) can't host the soft keyboard, so the shim installs the
|
||||
// IME proxy agent that relays the focused field to the host's keyboard. The full-screen Activity
|
||||
// host has a native keyboard and leaves this false.
|
||||
@@ -126,7 +126,7 @@ class NappletContentServer(
|
||||
}
|
||||
// Off-origin: a locked napplet 404s (connect-src 'none' means it shouldn't ask). An nSite in
|
||||
// website mode is a normal web app — defer to the WebView so it can load external resources.
|
||||
return if (websiteMode) null else notFound()
|
||||
return if (profile.allowsOffOrigin) null else notFound()
|
||||
}
|
||||
|
||||
private fun serveShell(): WebResourceResponse {
|
||||
@@ -172,7 +172,7 @@ class NappletContentServer(
|
||||
|
||||
// Locked napplets get the strict app CSP (connect-src 'none', etc.). An nSite in website mode
|
||||
// is a normal web app: no app CSP, so it can talk to relays (wss) and load external resources.
|
||||
val headers = if (websiteMode) emptyMap() else mapOf("Content-Security-Policy" to NappletWebContract.APP_CSP)
|
||||
val headers = profile.appCsp?.let { mapOf("Content-Security-Policy" to it) } ?: emptyMap()
|
||||
return WebResourceResponse(
|
||||
mime,
|
||||
charset,
|
||||
@@ -193,7 +193,7 @@ class NappletContentServer(
|
||||
// theme-independently. Allowed by the app CSP's `style-src 'unsafe-inline'`.
|
||||
val style = "<style>html,body{overscroll-behavior:none !important}</style>"
|
||||
// In website mode, set the NIP-07 flag synchronously *before* the shim so window.nostr installs.
|
||||
val nip07Flag = if (websiteMode) "<script>window.__nappletNip07=true;</script>" else ""
|
||||
val nip07Flag = if (profile.injectsNip07) "<script>window.__nappletNip07=true;</script>" else ""
|
||||
// Embedded surface: turn on the IME proxy agent (set before the shim runs).
|
||||
val imeFlag = if (imeProxy) "<script>window.__nappletImeProxy=true;</script>" else ""
|
||||
val script = "$style$nip07Flag$imeFlag<script>$shimJs</script>"
|
||||
|
||||
+7
-7
@@ -110,8 +110,8 @@ class NappletHostActivity : ComponentActivity() {
|
||||
// The sandbox never carries its own coordinate, so a compromised :napplet process can't forge one.
|
||||
private var launchToken: String = ""
|
||||
|
||||
// nSite "website mode": a normal web app (NIP-07 window.nostr + normal network), vs a locked napplet.
|
||||
private var websiteMode: Boolean = false
|
||||
// The host posture: a WEBSITE nSite (NIP-07 window.nostr + normal network) vs a locked NAPPLET.
|
||||
private var profile: HostProfile = HostProfile.NAPPLET
|
||||
|
||||
// Per-site network choice: route this site's traffic through Tor (default) or over the open web.
|
||||
// Applied to both the WebView proxy and the blob-fetch client; toggled from the top-bar onion.
|
||||
@@ -224,7 +224,7 @@ class NappletHostActivity : ComponentActivity() {
|
||||
// "Open web" for a site makes everything direct — both its blob fetches (here) and its live
|
||||
// web traffic (the WebView proxy, below). Tor (the default) routes both through the SOCKS port.
|
||||
val effectiveProxy = if (useTor) proxyPort else -1
|
||||
contentServer = NappletContentServer(paths, servers, effectiveProxy, cacheDir, shellHtml, shim, appOrigin, websiteMode)
|
||||
contentServer = NappletContentServer(paths, servers, effectiveProxy, cacheDir, shellHtml, shim, appOrigin, profile)
|
||||
|
||||
// Create + warm the WebView NOW so its (slow, first-in-process) Chromium init runs on the main
|
||||
// thread concurrently with the index probe below (which runs on IO) — instead of serially after
|
||||
@@ -237,7 +237,7 @@ class NappletHostActivity : ComponentActivity() {
|
||||
// Route the WebView's own (off-origin) traffic through Tor for an nSite, unless this site was
|
||||
// opted out to the open web. Set process-wide before any page navigation; the shell + blobs are
|
||||
// served from cache via shouldInterceptRequest, so only the site's external requests hit this.
|
||||
if (websiteMode) applyWebViewProxy(effectiveProxy)
|
||||
if (profile.exposesNetwork) applyWebViewProxy(effectiveProxy)
|
||||
// Origin-restricted bridge: only the trusted shell page (main frame) can reach native.
|
||||
WebViewCompat.addWebMessageListener(
|
||||
webView,
|
||||
@@ -411,7 +411,7 @@ class NappletHostActivity : ComponentActivity() {
|
||||
servers.addAll(intent.getStringArrayListExtra(NappletHostContract.EXTRA_SERVERS) ?: emptyList())
|
||||
author = intent.getStringExtra(NappletHostContract.EXTRA_AUTHOR).orEmpty()
|
||||
identifier = intent.getStringExtra(NappletHostContract.EXTRA_IDENTIFIER).orEmpty()
|
||||
websiteMode = intent.getBooleanExtra(NappletHostContract.EXTRA_WEBSITE_MODE, false)
|
||||
profile = HostProfile.fromName(intent.getStringExtra(NappletHostContract.EXTRA_HOST_PROFILE))
|
||||
useTor = intent.getBooleanExtra(NappletHostContract.EXTRA_USE_TOR, true)
|
||||
title = intent.getStringExtra(NappletHostContract.EXTRA_TITLE).orEmpty()
|
||||
proxyPort = intent.getIntExtra(NappletHostContract.EXTRA_PROXY_PORT, -1)
|
||||
@@ -731,8 +731,8 @@ class NappletHostActivity : ComponentActivity() {
|
||||
onReload = { if (this::webView.isInitialized) webView.reload() },
|
||||
// Website-mode nSites can re-route over Tor; switching rebuilds the session via a confirm
|
||||
// dialog, so the row taps through rather than toggling inline.
|
||||
torInitiallyOn = if (websiteMode && proxyPort > 0) useTor else null,
|
||||
onNetworkTap = if (websiteMode && proxyPort > 0) ({ showNetworkDialog() }) else null,
|
||||
torInitiallyOn = if (profile.exposesNetwork && proxyPort > 0) useTor else null,
|
||||
onNetworkTap = if (profile.exposesNetwork && proxyPort > 0) ({ showNetworkDialog() }) else null,
|
||||
onInfo = { showAccessDialog() },
|
||||
)
|
||||
|
||||
|
||||
+4
-3
@@ -61,10 +61,11 @@ object NappletHostContract {
|
||||
const val EXTRA_BG_COLOR = "napplet_bg_color"
|
||||
|
||||
/**
|
||||
* nSite "website mode": treat the content as a normal web app — install the NIP-07 `window.nostr`
|
||||
* provider and allow normal network (no app CSP). Off for locked napplets.
|
||||
* The host posture this launch renders under — a [HostProfile] name. WEBSITE treats the content as
|
||||
* a normal web app (NIP-07 `window.nostr` provider, normal network, no app CSP); NAPPLET is the
|
||||
* locked sandbox. Resolved in the trusted main process; the sandbox derives all coupled policy from it.
|
||||
*/
|
||||
const val EXTRA_WEBSITE_MODE = "napplet_website_mode"
|
||||
const val EXTRA_HOST_PROFILE = "napplet_host_profile"
|
||||
|
||||
/**
|
||||
* Whether this site's traffic routes through Tor (true, the default when Tor is active) or over the
|
||||
|
||||
+4
-4
@@ -89,7 +89,7 @@ class NappletHostService : Service() {
|
||||
val author: String,
|
||||
val identifier: String,
|
||||
val launchToken: String,
|
||||
val websiteMode: Boolean,
|
||||
val profile: HostProfile,
|
||||
val useTor: Boolean,
|
||||
val proxyPort: Int,
|
||||
val bgColor: Int,
|
||||
@@ -218,7 +218,7 @@ class NappletHostService : Service() {
|
||||
author = author,
|
||||
identifier = data.getString(NappletHostContract.EXTRA_IDENTIFIER).orEmpty(),
|
||||
launchToken = launchToken,
|
||||
websiteMode = data.getBoolean(NappletHostContract.EXTRA_WEBSITE_MODE, false),
|
||||
profile = HostProfile.fromName(data.getString(NappletHostContract.EXTRA_HOST_PROFILE)),
|
||||
useTor = data.getBoolean(NappletHostContract.EXTRA_USE_TOR, true),
|
||||
proxyPort = data.getInt(NappletHostContract.EXTRA_PROXY_PORT, -1),
|
||||
bgColor = data.getInt(NappletHostContract.EXTRA_BG_COLOR, android.graphics.Color.WHITE),
|
||||
@@ -255,13 +255,13 @@ class NappletHostService : Service() {
|
||||
val wv = WebView(context)
|
||||
val appOrigin = NappletWebContract.appOrigin(deriveAppId(tab.author, tab.identifier))
|
||||
val effectiveProxy = if (tab.useTor) tab.proxyPort else -1
|
||||
tab.contentServer = NappletContentServer(tab.paths, tab.servers, effectiveProxy, cacheDir, shellHtml, shimJs, appOrigin, tab.websiteMode, imeProxy = true)
|
||||
tab.contentServer = NappletContentServer(tab.paths, tab.servers, effectiveProxy, cacheDir, shellHtml, shimJs, appOrigin, tab.profile, imeProxy = true)
|
||||
|
||||
hardenWebView(wv, tab)
|
||||
// Theme the pre-load background so the shell/app loading shows Amethyst's background, not white.
|
||||
wv.setBackgroundColor(tab.bgColor)
|
||||
wv.dropSystemBarInsets()
|
||||
if (tab.websiteMode) applyWebViewProxy(effectiveProxy)
|
||||
if (tab.profile.exposesNetwork) applyWebViewProxy(effectiveProxy)
|
||||
WebViewCompat.addWebMessageListener(wv, NappletWebContract.BRIDGE_NAME, setOf(NappletWebContract.ORIGIN), ::onShellMessage)
|
||||
tab.webView = wv
|
||||
wv.loadUrl(NappletWebContract.SHELL_URL)
|
||||
|
||||
Reference in New Issue
Block a user