Files
archy/docs/app-launch-trust-boundary.md
T

7.6 KiB

App launch and signer trust boundary

This note describes the current safe launch candidate. It preserves the existing Android Companion app overlay for ordinary apps and the dashboard's embedded AppSession iframe in a browser. A manifest's requires_host_frame flag is a deliberate exception, not a blanket migration of phone apps into dashboard frames. The mobile node-signer experience still needs physical acceptance before that policy changes.

Where an app runs

  • neode-ui/src/stores/appLauncher.ts selects the launch surface. In Companion, an ordinary app uses openInAppOrNewTab and the native openInAppEx/openInApp overlay. In a browser, an embeddable app stays in AppSession and its iframe; apps whose manifest says opens_in_new_tab use a separate tab. A deep link and the app's visible back/close controls must survive either path.
  • scripts/generate-app-catalog.py derives launch ports, new-tab status, and requires_host_frame from app manifests. generatedAppSessionConfig.ts is generated output; change the manifest and regenerate it rather than adding a broad frontend override. In the current safe candidate, justworks and kithmoot require the host frame. GitWorkshop, Blossom, Gashboard, IndeeHub, and podsteadr retain the existing Companion native overlay while mobile signer parity is unresolved.
  • The browser iframe lives in AppSessionFrame.vue. A cross-port app keeps its own origin and storage. A same-origin /app/... fallback is sandboxed without allow-same-origin; it cannot read dashboard cookies, DOM, or CSRF state. The sandbox is tied to the current iframe URL and is recalculated when navigation changes origin. allow grants clipboard write to the frame and only catalog-verified extra permissions; fullscreen is not implicit.

Signer and app origin

  • The dashboard's useNostrBridge.ts accepts a signer request only from the exact mounted frame window and the expected app origin. For a same-origin sandboxed frame, the sender origin is null; frame-window equality and the expected same-origin URL are both required. Every sign request still requires the existing identity and consent checks. Do not replace these checks with a wildcard origin, relaxed CORS, or automatic signing.
  • The known node-provider consumers are GitWorkshop (archipelago-source), Blossom, Gashboard, IndeeHub, Kithmoot, and podsteadr. Some also use media, registration, or payment bridges. Their provider script and session must be tested on the surface that actually launches them; the presence of nostr-provider.js alone does not prove phone signer behavior.
  • The safe phone candidate keeps those apps in the native overlay. Its proposed ArchipelagoSigner transport belongs only to the verified app main frame at the exact app origin. A separate, visible node-origin /nostr-signer?native=1 broker uses ArchipelagoSignerHost; the app cannot inspect that broker's DOM or receive its node credentials. The native listener must bind each request to the app ID, origin, current URL, navigation generation, and one session. It must deliver INIT, await ACK, and cancel pending work on navigation or close. These are requirements for the Companion implementation and physical acceptance, not proof that the installed Android app already enforces them.
  • The broker's source allowlist maps the six provider consumers to their declared app ports: IndeeHub 7778, Blossom 8191, GitWorkshop 8337, Gashboard 1337, podsteadr 8095, and Kithmoot 8189. The browser-tab provider keeps its existing narrow identity hints. The ordinary browser iframe continues to use exact parent-window and origin checks; native messages do not weaken that path or bypass visible identity/consent decisions.
  • GitWorkshop's prior same-origin /app/archipelago-source iframe had an opaque sandbox origin (null) while its NIP-07 provider needed an app-bound signer context. The current browser URL uses the app-gate origin on port 8337 while retaining the compiled /app/archipelago-source router base. The gate owns only that app's provider route. Do not repair the opaque-origin failure by adding allow-same-origin to dashboard-origin frames, exposing dashboard credentials to app JS, or broadening app-gate routes. The separate Companion overlay can lose the dashboard parent postMessage bridge; that mobile signer path remains an acceptance gate, not a source-test pass.
  • GitWorkshop's provider script URL is versioned in its built HTML. A past SPA fallback at the old URL can remain in a phone's HTTP cache as text/html and silently suppress window.nostr under nosniff. Bump the URL when repairing that state; the app gate must serve the exact root provider route as JavaScript with Cache-Control: no-store. Qualify an upgrade in the same browser profile without clearing storage or cache.
  • Native signer INIT only binds the verified app origin and acknowledges the session; it must not send a remembered identity before queued requests, because that can complete an explicit account choice without user selection. An app that offers login on launch should issue one ordinary NIP-07 login request after its UI mounts, guarded against duplicate mounts and skipped when its own session is active. A remembered node selection can then satisfy the public-key request; otherwise the broker shows the visible chooser. Signing still follows the normal consent decision. Browser-tab and dashboard-iframe startup keep their own existing paths. Qualify a fresh native launch, remembered-session reopen, cancellation, account switch, and actual signed return before accepting an app's automatic login UX.

Pine's two-port rule

Pine launches through its gated HTTP entry port 10380. That entry redirects to its HTTPS UI on 10381 so Web Bluetooth has a secure context. The second port is admitted into FIPS only when the manifest declares a matching HTTPS UI interface and a public TCP auth: none mapping for the same host port. It is not a general exception for arbitrary service ports or CORS. scripts/tests/test_generate_app_catalog_ports.py checks the generated FIPS port list and rejects a UDP, local-only, mismatched, or non-HTTPS declaration. Preserve the redirect and certificate behavior in real browsers.

Regression gates before changing launch policy

  1. Desktop browser: verify direct per-app origin and router paths, signer identity/consent, back/forward/close, initial loading/error/success, and no extra iframe reload on panel/overlay changes. Check the same-origin fallback remains opaque. Run at phone and desktop widths for any UI change.
  2. Android Companion: verify the existing native overlay opens and closes with its phone controls, no speed or visual regression, and deep links/media still work. On both phones test exact-origin INIT/ACK, visible identity and sign consent, response delivery, navigation/close cancellation, and rejection of a different app origin or stale session. Do not infer success from desktop iframe tests or source-only tests.
  3. IPv6 and HTTPS: verify bracketed IPv6 host URLs keep the app port and expected scheme, app-gate TLS/certificate trust, GitWorkshop's /app/archipelago-source base, and Pine's 10380→10381 redirect. A certificate error inside an iframe is a failure, not an invitation to switch to mixed content.
  4. Provider and security: verify exact frame source/origin binding, explicit consent, app-gate route scope, manifest-generated launch policy, and no broad sandbox/CORS changes. Run the focused AppSessionFrameAuthority, appSessionConfig, app launcher, and catalog-port tests after changes.

Source tests protect routing and policy invariants. They do not replace installed desktop, mobile, or physical signer acceptance.