diff --git a/docker/archipelago-source/nginx-main.conf b/docker/archipelago-source/nginx-main.conf
new file mode 100644
index 00000000..1760fafb
--- /dev/null
+++ b/docker/archipelago-source/nginx-main.conf
@@ -0,0 +1,23 @@
+worker_processes auto;
+pid /tmp/nginx.pid;
+error_log /dev/stderr notice;
+
+events {
+ worker_connections 256;
+}
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+ access_log /dev/stdout;
+ sendfile on;
+ keepalive_timeout 65;
+
+ client_body_temp_path /tmp/client_temp;
+ proxy_temp_path /tmp/proxy_temp;
+ fastcgi_temp_path /tmp/fastcgi_temp;
+ uwsgi_temp_path /tmp/uwsgi_temp;
+ scgi_temp_path /tmp/scgi_temp;
+
+ include /etc/nginx/conf.d/*.conf;
+}
diff --git a/docker/archipelago-source/nginx.conf b/docker/archipelago-source/nginx.conf
new file mode 100644
index 00000000..9679cb48
--- /dev/null
+++ b/docker/archipelago-source/nginx.conf
@@ -0,0 +1,42 @@
+server {
+ # Host networking is required for the loopback-only Archipelago RPC.
+ # Keep nginx itself on loopback so the authenticated app gate owns every
+ # externally reachable listener.
+ listen 127.0.0.1:8337;
+ server_name _;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ location = /healthz {
+ access_log off;
+ default_type text/plain;
+ return 200 "ok\n";
+ }
+
+ location = /manifest.webmanifest {
+ default_type application/manifest+json;
+ try_files $uri =404;
+ }
+
+ location = /app/archipelago-source/manifest.webmanifest {
+ default_type application/manifest+json;
+ rewrite ^/app/archipelago-source/(.*)$ /$1 break;
+ try_files $uri =404;
+ }
+
+ # The normal dashboard proxy strips this prefix before forwarding, while
+ # direct app-gate access preserves it. Supporting both keeps health/debug
+ # access useful without making launch depend on any particular interface.
+ location ^~ /app/archipelago-source/ {
+ rewrite ^/app/archipelago-source/(.*)$ /$1 break;
+ try_files $uri $uri/ /index.html;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+ }
+
+ location / {
+ try_files $uri $uri/ /index.html;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+ }
+}
diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md
index 7f7e2179..741de1bf 100644
--- a/docs/COMMANDS.md
+++ b/docs/COMMANDS.md
@@ -132,7 +132,7 @@ curl -s http:///rpc/v1 -b jar.txt -H 'Content-Type: application/json' \
Login returns a `session` cookie. State-changing calls also need the `X-CSRF-Token` header. Exactly twelve read-only methods are CSRF-exempt, so for those the cookie alone is enough:
-`node-messages-received` · `server.echo` · `server.get-state` · `system.stats` · `system.get-settings` · `system.get-node-key` · `system.get-metrics` · `system.get-version` · `tor.status` · `tor.onion-addresses` · `bitcoin.relay-status` · `federation.list-nodes`
+`node-messages-received` · `server.echo` · `server.get-state` · `system.stats` · `system.get-settings` · `system.get-node-key` · `system.get-metrics` · `system.get-hostname` · `tor.status` · `tor.onion-addresses` · `bitcoin.relay-status` · `federation.list-nodes`
Anything not on that list — including `bitcoin.getinfo` and `monitoring.current` — needs the CSRF header. If TOTP is enabled, follow the login with `auth.login.totp`.
diff --git a/docs/app-developer-guide.md b/docs/app-developer-guide.md
index 42e00f07..76f6b7f1 100644
--- a/docs/app-developer-guide.md
+++ b/docs/app-developer-guide.md
@@ -31,9 +31,6 @@ app:
entrypoint: ["sh", "-lc"]
custom_args:
- /app/start.sh
- derived_env:
- - key: PUBLIC_URL
- template: https://{{HOST_MDNS}}:8180
secret_env:
- key: APP_PASSWORD
secret_file: my-app-password
@@ -55,6 +52,8 @@ app:
- host: 8180
container: 8080
protocol: tcp
+ bind: 127.0.0.1
+ auth: gated
volumes:
- type: bind
@@ -125,13 +124,59 @@ app:
| `app.environment` | Static `KEY=value` environment entries |
| `app.health_check` | HTTP or TCP health check settings |
| `app.devices` | Explicit device paths |
-| `app.metadata` | Catalog-facing presentation metadata such as icon, category, tier, repo/source, author, feature bullets, and launch hints |
+| `app.metadata` | Catalog-facing presentation metadata such as icon, category, tier, repo/source, author, feature bullets, and [launch hints](#browser-iframe-and-companion-launch-modes) |
| `app.interfaces.main` | Optional primary UI launch surface with `port`, `protocol`, and `path` |
Additional extension keys may exist for current integrations, for example Bitcoin, Lightning, or app-specific launch/interface metadata. Treat extension keys as transitional unless they are documented as reusable platform primitives.
### Iframe embedding — the rules
+#### What Archipelago decides, and what the app must declare
+
+Archipelago works out the reachable hostname and browser scheme at launch
+time. An app must not bake a LAN IP, Tailscale IP, FIPS address, `.local`
+name, or the dashboard's current `http`/`https` scheme into its UI URL.
+Declare the UI once in `interfaces.main`, put the matching port behind the
+app gate, and use relative URLs for the app's own assets and links.
+
+| Concern | App author | Archipelago |
+|---|---|---|
+| UI location | Declare `interfaces.main.port`, `protocol`, and `path` | Uses the address through which this browser reached the node |
+| Exposure | Bind a `gated`/`open` port to `127.0.0.1` | Publishes it on supported LAN, Tailscale, FIPS, and Tor ingress |
+| HTTP/HTTPS | Serve the declared upstream protocol locally | Keeps HTTP pages on HTTP; on an HTTPS dashboard, gate-fronted app ports use HTTPS on the same port |
+| Embedded or top-level | Default to iframe; declare an exception when required | Chooses iframe, browser tab, or companion-native view from generated launch metadata |
+| Navigation | Use relative same-app URLs and normal absolute external URLs | Preserves the selected node address and routes external links out of the companion app view |
+
+`interfaces.main.protocol` describes the service behind the gate. It does
+not tell application code to hard-code that scheme into browser links: the
+gate can terminate TLS in front of a locally plain-HTTP container.
+
+There are two important limits:
+
+- `auth: none` bypasses the gate, so Archipelago cannot add TLS or make that
+ port safe to embed from an HTTPS dashboard. Use it for protocols, not
+ ordinary web UIs.
+- Same-origin mounts such as `/app/archipelago-source/` are platform-owned
+ integrations. A normal app cannot request an arbitrary dashboard path in
+ its manifest; use `interfaces.main` and a gated port.
+
+When the platform does provide one of those same-origin mounts, the nginx
+location must pass its exact mount as `X-Forwarded-Prefix` to the app gate:
+
+```nginx
+location /app/example/ {
+ proxy_pass http://127.0.0.2:8123/;
+ proxy_set_header X-Forwarded-Prefix /app/example;
+}
+```
+
+The trailing slash on `proxy_pass` strips the mount from the upstream request;
+the header lets the gate put it back into its login form, login-page assets,
+and successful redirect. Omitting it makes a fresh mobile-browser session post
+to the dashboard's root `/__archipelago-gate/login`, which is not an app-gate
+endpoint and will normally return 405. This header is host integration config,
+not app-controlled manifest metadata, and must be a fixed literal path.
+
The dashboard opens apps in an **embedded frame** (My Apps → app session) by
default. Whether that works is decided by HTTP headers, not by wishes, so
know the mechanics:
@@ -145,7 +190,7 @@ know the mechanics:
behind Archipelago's app gate the clickjacking threat those headers address
is already handled — every proxied request is authenticated by the gate
first.
-- Therefore **the gate neutralizes frame blocking on proxied responses**: it
+- Therefore **the gate neutralizes frame blocking on gate-fronted responses**: it
removes `X-Frame-Options` and strips only the `frame-ancestors` directive
from the app's CSP. The rest of the app's CSP (script-src, connect-src, …)
passes through untouched — the gate never weakens the app's own content
@@ -214,6 +259,37 @@ underscores. Supported interface types are `ui`, `api`, and `metrics`; only
`type: ui` is treated as a launchable app surface. Supported protocols are
`http` and `https`, and `path` must start with `/`.
+### Browser, iframe, and companion launch modes
+
+Launch behavior is generated from the manifest. Application code should not
+sniff for a particular node IP or companion user-agent.
+
+```yaml
+metadata:
+ launch:
+ # Use only for OAuth/WebAuthn, JS frame-busting, or another top-level
+ # browser requirement that the gate cannot repair.
+ open_in_new_tab: false
+
+ # Keep a different, app-specific parent-frame integration alive in the
+ # Android companion. Standard Archipelago NIP-07 no longer needs this.
+ requires_host_frame: false
+```
+
+- Desktop/PWA: iframeable apps stay in the dashboard.
+ `open_in_new_tab: true` apps open in a browser tab.
+- Android companion: ordinary apps open in the native in-app browser with its
+ own navigation controls. `requires_host_frame: true` apps stay in the
+ dashboard iframe so `window.parent.postMessage` integrations remain alive.
+- Never set both flags. A top-level page cannot simultaneously require its
+ parent frame.
+- Relative app paths are resolved against the active dashboard origin before
+ a native launch, so the same package works through LAN, Tailscale, and FIPS.
+
+Test all four relevant paths before submission: HTTP dashboard iframe, HTTPS
+dashboard iframe with the node CA installed, companion launch, and every
+external link or login redirect that leaves the app.
+
### Nostr Signer Bridge (NIP-07)
Apps embedded in the Archipelago iframe can use the node's Nostr identity to sign
@@ -221,43 +297,149 @@ events without managing their own keys. Archipelago injects a **NIP-07 provider*
(`window.nostr` with `getPublicKey()` / `signEvent()` / `nip04` / `nip44`) that bridges
to the host. Your app code uses standard NIP-07 — no Archipelago-specific API.
-**How injection works.** After install, the host copies `nostr-provider.js` into the
-app container and patches the app's web server so every page loads it and the app is
-iframe-embeddable. This is **best-effort** and depends on your server config exposing
-the right hooks. For an **nginx-served SPA** (the supported reference shape, e.g.
-IndeeHub) your `nginx.conf` must satisfy this contract:
+**How injection works.** The dashboard owns the consent UI and postMessage
+host, and ships the canonical `nostr-provider.js`, but generic containers are
+not silently rewritten. Package the provider explicitly with a manifest
+`copy_from_host` hook (or bake the same provider into the image) and inject it
+into every HTML document your app serves. IndeeHub's manifest is the
+hook-based reference; Archipelago Source's outer same-origin nginx mount is a
+platform-owned reference.
-1. **Be iframe-embeddable.** Do not send a hard `X-Frame-Options: DENY`. The host
- strips a `SAMEORIGIN`/`DENY` `X-Frame-Options` header line if present; restrictive
- CSP `frame-ancestors` will still block embedding.
-2. **Keep an exact-match `location = /sw.js {` block.** The provider's no-cache
- `location = /nostr-provider.js` block is inserted immediately before it.
-3. **Keep an SPA fallback line `try_files $uri $uri/ /index.html;`.** A
- `sub_filter` that injects `` before
- `` is inserted right after it. (nginx must have `ngx_http_sub_module` —
- stock `nginx:alpine` does.)
+For an **nginx-served SPA**, use this contract:
+
+1. **Be iframe-embeddable.** The app gate removes `X-Frame-Options` and only
+ the CSP `frame-ancestors` directive from responses, but your own config
+ should still express the intended embedded deployment rather than relying
+ on repair.
+2. Serve `/nostr-provider.js` with `Cache-Control: no-cache, no-store`. Never
+ precache the provider or the dashboard `/nostr-signer` navigation in an app
+ service worker; signing protocol updates must reach existing installations.
+3. Inject a versioned provider URL such as
+ `` before
+ `` in every SPA document. The token prevents an older iframe-only
+ provider from surviving a dashboard update in the browser's asset cache.
+ `sub_filter` is suitable when nginx has
+ `ngx_http_sub_module` (stock `nginx:alpine` does).
4. **If you proxy an API that does NIP-98 URL verification**, expose
`proxy_set_header X-Forwarded-Prefix /api;`; the host rewrites it to honor the
outer reverse proxy's prefix.
-The patch is **idempotent** (it checks for an existing `nostr-provider` reference
-before editing) and re-runs on reinstall. If you rename or remove any of the anchor
-strings above, injection silently no-ops and `window.nostr` will be undefined in your
-app — so guard those lines in your config (see the contract comment block at the top of
-IndeeHub's `nginx.conf` for a template).
+Make the hook **idempotent** and fail its verification step if the provider is
+not present after install. A silent no-op leaves `window.nostr` undefined and
+is not release-ready.
-> Non-nginx servers (Next.js `node server.js`, etc.) are not auto-patched today. Either
-> serve via nginx, or ship `nostr-provider.js` yourself and reference it in your HTML;
-> the canonical script lives at `/opt/archipelago/web-ui/nostr-provider.js` on the node.
+> Non-nginx servers (Next.js `node server.js`, etc.) should ship the provider
+> themselves and reference it in their HTML; the canonical host copy is
+> `/opt/archipelago/web-ui/nostr-provider.js`.
-Declare iframe intent in the manifest so the launcher embeds (vs. opens a new tab):
+Choose the launch mode for the app itself; the signer works in either shape:
```yaml
metadata:
launch:
- open_in_new_tab: false # default; set true only if the app cannot be iframed
+ open_in_new_tab: false
+ requires_host_frame: false
```
+The provider supports both launch shapes. In a dashboard iframe it talks to
+the dashboard parent directly. In a browser tab or the companion's standalone
+WebView it creates a dashboard-origin signer frame, which renders the same
+identity chooser and consent card over the app and relays NIP-07 requests to
+the authenticated node session. It deliberately does not depend on
+`window.opener`, so `noopener` tab launches remain safe and functional.
+The app gate's successful login supplies the host-wide session and CSRF cookie
+pair in a fresh external browser; the signer broker validates that session
+directly and does not require the browser to have visited or logged into the
+dashboard first. Existing session-only browser tabs are repaired on their next
+gate-fronted app response. Do not add a second dashboard-login prerequisite in
+application code.
+
+For that reason a NIP-07 app does **not** need `requires_host_frame: true`.
+Use the flag only if the app has some other parent-frame protocol. If a
+top-level app sends its own `Content-Security-Policy`, its `frame-src` must
+permit the dashboard origin; apps intended to work over every node address can
+allow `http:` and `https:` while relying on the provider's strict same-host
+parent validation. A policy limited to `frame-src 'self'` will block the
+broker when the app is running on a different port.
+
+**Consent UI belongs to the platform.** Do not build a second signer modal,
+request a top-level window, or overlay the entire dashboard. A standard NIP-07
+call pauses while Archipelago shows its contained consent card inside the
+active app surface. After approval, the shared Nostr identity ring provides a
+short signing loader and completion state. The same host-owned flow renders in
+desktop browsers, installed PWAs, and the Android companion WebView.
+Silent background requests and remembered approvals deliberately keep the
+broker frame hidden; only an identity choice or an actual consent prompt may
+reveal it. If an app performs NIP-98 bootstrap and then navigates, it must wait
+for the provider Promise to finish rather than independently reloading while
+the consent result is still visible. The canonical provider coordinates its
+automatic IndeeHub-style session reload with the broker's hide notification.
+For top-level apps, that broker document must remain transparent. When hidden,
+its iframe must stay loaded but be reduced to a non-interactive 1px surface and
+parked physically off-screen. Removing/display-hiding the full-viewport iframe,
+or leaving it full-size with only `visibility:hidden`, can make Android WebView
+and mobile Chromium retain its last black/grey compositor surface above a
+healthy app until refresh. Keeping one parked broker also prevents a visible
+hide/recreate flash between `getPublicKey` and `signEvent`. The canonical
+provider owns this lifecycle; apps must not copy or manipulate its iframe.
+
+Apps should treat the NIP-07 Promise as an ordinary asynchronous operation:
+disable only the initiating control, preserve the user's draft, handle a user
+denial as a normal rejected request, and render the returned result when it
+resolves. Never infer approval from elapsed time and never ask the user for an
+`nsec` as a fallback.
+
+Archipelago recognizes a synchronous, user-triggered `getPublicKey()` as an
+account-selection action. An Archipelago-packaged app should still ask the host
+to show the identity chooser explicitly before login, especially when other
+asynchronous work happens between the click and the NIP-07 call. This prevents
+a returning user from being silently locked to the identity chosen on first use:
+
+```js
+await window.archipelagoNostr?.selectIdentity?.()
+const pubkey = await window.nostr.getPublicKey()
+```
+
+`archipelagoNostr.selectIdentity()` is an optional host enhancement, not part of
+NIP-07. Apps must continue to work when it is absent (for example with a normal
+browser extension). Invoke it only from a deliberate login/account-switch
+action; routine signing calls should continue using the remembered identity.
+
+If a first-launch choice should create the app account automatically, use the
+provider's sticky identity subscription and call the ordinary extension-login
+action from it:
+
+```js
+const unsubscribe = window.archipelagoNostr?.onIdentitySelected?.(() => {
+ if (!alreadyLoggedIn()) loginWithNip07()
+})
+```
+
+The callback runs immediately when an identity was selected just before the
+React/Vue component mounted, closing the load-event race seen in browser tabs
+and Companion WebViews. Call `unsubscribe()` when the component unmounts. The
+selected public key remains available to the immediately following
+`getPublicKey()` call; do not add a timeout, reload, or second lookup between
+those operations. A plain `archipelago:identity` message remains available for
+backward compatibility, but it is not a reliable framework lifecycle API.
+
+Submission testing for a Nostr-signed app must include:
+
+1. `getPublicKey` allow, deny, and remembered consent;
+2. `signEvent` with a readable event-kind/content preview;
+3. the contained review → identity-ring loader → completion sequence;
+4. changing the selected identity and confirming remembered consent does not
+ cross identity boundaries;
+5. HTTP and HTTPS dashboard frames, a `noopener` browser-tab launch, and the
+ Android companion's standalone WebView;
+6. choosing an identity immediately when the first-launch picker appears, to
+ prove the app's account store is ready before the result arrives; and
+7. Companion → **Open in browser** in a browser with no prior dashboard
+ localStorage: complete the app gate, then prove the contained signer can
+ choose an identity and sign without asking for a second node login; and
+8. after the first identity choice and after NIP-98 authentication, confirm the
+ underlying app paints immediately—no black frame and no manual reload.
+
## Security Requirements
Two different things enforce these, and it's worth knowing which is which:
diff --git a/docs/app-manifest-spec.md b/docs/app-manifest-spec.md
index ed29741f..d740d07a 100644
--- a/docs/app-manifest-spec.md
+++ b/docs/app-manifest-spec.md
@@ -173,6 +173,37 @@ override wins over the manifest in both directions and applies on the next
request — your app cannot assume the gate is or isn't in front of it, so it
must always enforce its own authorization for sensitive operations.
+## Launch metadata
+
+`metadata.launch` is consumed by catalog generation and the dashboard
+launcher. It is currently an extension rather than a Rust-validated field:
+
+```yaml
+metadata:
+ launch:
+ open_in_new_tab: false
+ requires_host_frame: false
+```
+
+| Field | Default | Meaning |
+|---|---|---|
+| `open_in_new_tab` | `false` | The app must be top-level because header repair cannot solve its OAuth/WebAuthn flow, JavaScript frame-busting, or strict cookies. Desktop opens a browser tab; Android uses its native in-app browser. |
+| `requires_host_frame` | `false` | Keep the app in the dashboard iframe even in the Android companion because it consumes an app-specific parent-frame integration. Standard Archipelago NIP-07 works in iframes, tabs, and the companion WebView without this flag; the platform renders consent inside the active app surface. |
+
+Do not set both fields to `true`. The generated TypeScript launch tables are
+the runtime source used by the dashboard, so run
+`python3 scripts/generate-app-catalog.py` after changing either value. See
+[`app-developer-guide.md`](app-developer-guide.md#browser-iframe-and-companion-launch-modes)
+for the HTTP/HTTPS and test matrix.
+
+Platform-owned same-origin mounts are not manifest features. If Archipelago
+adds one, its nginx location must send a fixed
+`X-Forwarded-Prefix: /app/` header to the app gate whenever `proxy_pass`
+strips that prefix. The gate uses it for challenge form/assets and the
+post-login redirect; without it, a fresh external browser posts to the
+dashboard root and receives 405. Ordinary registry apps should declare a
+gated `interfaces.main` port instead of requesting such a mount.
+
## Volumes
```yaml
diff --git a/docs/nostr-git-source-hosting.md b/docs/nostr-git-source-hosting.md
index 908ad300..e154f68b 100644
--- a/docs/nostr-git-source-hosting.md
+++ b/docs/nostr-git-source-hosting.md
@@ -1,252 +1,278 @@
# Nostr Git Source Hosting Plan
-This plan describes how Archipelago can publish and accept contributions to its
-source code through `ngit`, NIP-34, and GRASP while keeping the developer
-experience inside Archipelago.
+**Reviewed:** 2026-09-08
-## Goals
+**Status:** GitWorkshop integration is deployed and engineering-tested on the
+development node, ready for owner UAT. Canonical repository publication and
+release work remain separate gates. No app-registry, OTA, ISO, or production
+artifact may be published until the owner accepts the node deployment.
-- Publish Archipelago source from a sanitized, fresh-history repository.
-- Make the in-app registry the primary onboarding path for contributors.
-- Let contributors clone, branch, push PR branches, open PRs, and discuss issues
- with a Nostr identity from their Archipelago node.
-- Follow the Bitcoin Core development model: broad public review and easy forks,
- with canonical merge authority held by a small maintainer set.
-- Give contributors full read, fork, and proposal rights, but no direct merge
- rights on the canonical repository.
-- Keep the official maintainer identity and merge authority separate from user
- node identities.
+The Android companion opens Source as a top-level page in its native in-app
+WebView. GitWorkshop's injected NIP-07 provider creates a small authenticated
+dashboard-origin signer broker within that page, so the app itself is never
+kept in a dashboard iframe. The App Store carries the upstream GitWorkshop
+icon, source-focused copy, and a dedicated contribution banner. Popular ordering and promotional
+placement are registry-owned `storefront` metadata rather than node-OS UI
+policy; these are also part of owner UAT.
-## Current Building Blocks
+## Goal
-Archipelago already has most of the primitives needed for this:
+Archipelago users can install a Source app from the app registry, obtain the
+Archipelago source, browse it, and contribute through the established Nostr Git
+ecosystem. Git remains the version-control engine, Nostr NIP-34 carries
+repository identity and collaboration events, and GRASP transports Git objects.
-- App manifests and the app registry already install developer tooling as
- rootless Podman apps.
-- The `gitea` app provides a conventional fallback Git UI and package registry.
-- The app launcher already exposes a consent-gated NIP-07 bridge for launched
- apps using `getPublicKey`, `signEvent`, NIP-04, and NIP-44 requests.
-- The backend exposes node and identity Nostr signing RPC methods.
-- FIPS gives nodes a stable mesh identity and private transport path, but repo
- announcements and PRs should remain NIP-34 compatible on normal Nostr relays.
-- DWN protocol registration exists and can be used later for local contribution
- metadata/cache, but should not be required for the first public workflow.
+The app must make public contribution easy without giving contributors direct
+merge or release authority. Canonical refs, merge status, release tags, and
+catalog signatures remain controlled by explicitly configured Archipelago
+maintainers.
-## Protocol Basis
+## Product Decision
-Use existing Nostr Git conventions rather than inventing an Archipelago-only
-protocol:
+Archipelago will package the upstream GitWorkshop web client instead of
+building another NIP-34 repository interface.
-- NIP-34 repository announcement events identify repositories with kind `30617`.
-- NIP-34 repository state events publish branch/tag refs with kind `30618`.
-- NIP-34 patches, pull requests, PR updates, issues, and status events use kinds
- `1617`, `1618`, `1619`, `1621`, and `1630`-`1633`.
-- `ngit` provides the `git-remote-nostr` helper for `nostr://` clone URLs and PR
- branches.
-- GRASP servers provide Git Smart HTTP storage while Nostr events remain the
- authority for repository identity, refs, PRs, issues, and maintainer state.
+GitWorkshop already provides repository discovery, a sparse Git explorer,
+issues, pull requests, and review workflows. Archipelago owns only the node
+integration around it:
+
+- installable app metadata and a pinned upstream build;
+- a same-origin `/app/archipelago-source/` launch path that works through the
+ dashboard address the user already opened, whether that is LAN, Tailscale,
+ FIPS, DNS, IPv4, or IPv6;
+- authenticated routing through the existing app gate;
+- an injected, consent-gated NIP-07 provider so GitWorkshop can use a selected
+ node identity without receiving its private key;
+- source provenance, security validation, upgrades, and rollback.
+
+Archipelago will not duplicate GitWorkshop's repository browser, issue/PR,
+fork, diff, relay, or GRASP behavior in private `source.*` RPC methods.
Primary references:
+- https://ngit.dev/how-it-works
+- https://github.com/DanConwayDev/gitworkshop
+- https://gitworkshop.dev/
- https://nips.nostr.com/34
-- https://docs.rs/crate/ngit/latest/source/README.md
-- https://ngit.dev/grasp/
-## Recommended Architecture
+## Trust And Permissions
-### Apps
+- GitWorkshop runs as a static, read-only container behind the app gate.
+- The iframe may request NIP-07 operations through `postMessage`; only the
+ exact launched frame and expected origin are accepted.
+- `getPublicKey`, event signing, encryption, and decryption require explicit
+ dashboard consent. A remembered decision is scoped to node origin, app,
+ selected identity, and method.
+- Contributor private keys never enter the GitWorkshop container.
+- Browser-origin signing calls from direct high-port app origins are rejected;
+ they must pass through the dashboard consent bridge.
+- Maintainer and release keys must not be placed on ordinary user nodes.
+- Relay and GRASP data is untrusted. Canonical status is derived only from the
+ signed repository announcement and configured maintainer identities.
-Create two first-party apps:
+## Upstream Pin And Redistribution Gate
-- `ngit`: CLI/runtime package containing `ngit` and `git-remote-nostr`.
-- `archipelago-source`: web UI for cloning Archipelago source, viewing NIP-34
- issues/PRs, opening branches, and submitting PR events.
+The development image currently pins GitWorkshop commit
+`dc36db64f6a2cca29d109829eabaf0a49d4bf4da` (2026-07-28). The integration patch
+only adds base-path support and the Archipelago NIP-07 provider.
-The `archipelago-source` app should depend on `ngit`. It can also recommend
-Gitea for users who want a conventional local web Git UI, but Gitea should not
-be the source of truth for public contribution permissions.
+The pinned revision and current upstream `main` have no license file, the npm
+package metadata declares no license, and GitHub reports no detected license.
+An earlier project-site description of “MIT” is not a license grant bundled
+with the code. Local engineering and owner evaluation may continue, but the
+compiled image must not be published to the production app registry until its
+redistribution terms are unambiguous.
-### Contributor Onboarding
+Preferred resolution: ask upstream to add an SPDX-recognized license file
+(MIT if that remains their intent), then re-pin at or after that commit and add
+GitWorkshop plus its copyright/license notice to Archipelago's `NOTICE` and
+generated image inventory. A written grant that explicitly permits compiling,
+modifying, and redistributing this app is an alternative, but is harder for
+downstream users to audit. A public GitHub repository or permission to fork is
+not sufficient redistribution permission. Production dependency-audit findings
+must also be resolved or explicitly accepted before release.
-When the user installs `archipelago-source` from the registry:
+The release-preparation audit on 2026-09-09 ran `npm audit --omit=dev` against
+the exact pinned commit and reported 4 high and 6 moderate advisories, with
+fixes available for every affected package. The same commit remains upstream
+`main`, so repinning alone does not resolve them. The final runtime image is
+static nginx rather than Node, which makes the Hono server findings unlikely to
+be runtime-reachable, but browser/runtime dependencies such as `fflate` and
+React Router still require an upstream dependency update or an explicit,
+written risk acceptance before registry publication.
-1. Show a modal before first launch: "Contribute to Archipelago".
-2. Explain that the app will use their Archipelago Nostr identity to clone and
- sign contribution events.
-3. Display the maintainer repository announcement, clone URL, maintainer npub,
- and relay/GRASP endpoints.
-4. Ask for consent to:
- - fetch repository metadata from configured relays,
- - clone source through `nostr://`,
- - create local branches,
- - sign NIP-34 issue/PR/comment events,
- - push PR branches to approved GRASP servers.
-5. Store approval per app origin, identity id, repository id, and relay set.
+## Canonical Archipelago Repository
-This should build on the existing NIP-07 app-launcher bridge, but use a more
-specific permission scope than the generic sign-event approval.
+The canonical announcement maintainer is
+`npub1w3sqdkrhn0gyuvsex32effzgnfpyde6qrrc4u467flg5e9txh4wsfn5vjg`.
+The repository already contains a root MIT `LICENSE` and `CONTRIBUTING.md`;
+contributors agree to license their contributions under that MIT License.
-### Identity And Permissions
+The user-facing Source app can ship for local evaluation before the canonical
+Archipelago Nostr repository exists, but it must not pretend a placeholder is
+canonical. Canonical launch requires:
-Use four identity classes:
+1. A sanitized public `archy` source repository.
+2. An offline or tightly controlled maintainer identity.
+3. A signed NIP-34 kind `30617` repository announcement.
+4. At least one Archipelago-operated relay/GRASP endpoint and one independent
+ compatible mirror.
+5. Tested `nostr://` clone, proposal, update, review, merge-status, server-loss,
+ and recovery flows.
+6. A GitWorkshop link/configuration that opens the verified `archy` repository.
-- `archipelago-maintainer`: an offline or tightly controlled Nostr key that
- signs the canonical kind `30617` repo announcement and status/merge events.
-- `archipelago-merge-maintainer`: one of the small set of maintainer npubs
- allowed to advance canonical refs and publish valid merged/applied status.
-- `archipelago-build`: release automation key for signed release artifacts and
- CI status events. It must not have merge authority.
-- `contributor`: user node or app-specific identity used for PRs, issues, and
- comments.
+The existing HTTP Git remote remains a fallback until those drills pass.
-Contributor rights:
+## Delivery Milestones
-- Clone the repository.
-- Open issues.
-- Push proposal branches using `pr//` or `pr/`.
-- Publish NIP-34 PR/update/comment events.
-- Rebase and update their own PR branch.
-- Run local validation and attach status evidence.
+### 1. Plan And Protocol Review — complete
-Contributor restrictions:
+- Confirmed NIP-34/ngit/GRASP as the interoperability layer.
+- Defined contributor, maintainer, build, and release trust boundaries.
+- Confirmed that installation must ultimately come from the Archipelago app
+ registry and include a path to the upstream/source code.
-- Cannot update `refs/heads/main` or release branches in canonical state.
-- Cannot publish maintainer-valid merge/applied status.
-- Cannot alter the canonical repository announcement.
-- Cannot publish release catalog signatures.
+### 2. Runtime Feasibility — complete
-Maintainer rights:
+- Validated pinned `ngit` and `git-remote-nostr` binaries on supported node
+ architectures.
+- Exercised public `nostr://` discovery/clone behavior.
+- Established that app lifecycle dependencies do not share executables or
+ filesystems, avoiding an invalid two-container CLI design.
-- Publish/update the canonical repo announcement.
-- Publish canonical `refs/heads/main` state.
-- Mark PRs merged/closed/draft via NIP-34 status events.
-- Sign release tags and catalog updates.
+These CLI checks remain useful for canonical repository operations and release
+validation; they are not a reason to build a second browser client.
-Fork rights:
+### 3. Node Integration Foundation — complete
-- Any contributor can create their own NIP-34 kind `30617` repository
- announcement for a fork.
-- Fork announcements should use the NIP-34 `u` tag to point back to the
- canonical `archy` repository.
-- The source app should make forking a first-class path: "Fork on Nostr", clone
- the fork locally, push branches to the contributor's GRASP list, and open PRs
- back to canonical Archipelago when they want review.
-- Forks can have their own maintainer npubs, relays, policies, and release
- cadence, but the app should clearly label them as forks unless signed by the
- canonical maintainer set.
+- Added the installable app manifest, catalog metadata, icon, and port
+ reservation.
+- Added identity selection and a generic consent-gated NIP-07 bridge.
+- Kept signing secrets out of the app container.
-The GRASP server policy should enforce this by accepting pushes to maintainer
-refs only when backed by signed maintainer state, while allowing contributor PR
-refs from their own npubs.
+### 4. GitWorkshop Pivot — complete on the development node
-## Repository Layout
+- Replace the prototype Source UI and all private `source.*` APIs with the
+ pinned upstream GitWorkshop build.
+- Mount it below `/app/archipelago-source/` and proxy to the authenticated app
+ gate, eliminating hard-coded address and high-port launch behavior.
+- Validate upstream base-path routing, static assets, browser refresh/deep
+ links, NIP-07 requests, container hardening, and install/restart behavior.
+- Deploy the resulting daemon, dashboard, and app only on this development
+ node, then hand it to the owner for UAT.
-Canonical repo announcement:
+The pinned integration patch applies cleanly to a fresh upstream checkout. The
+upstream unit suite passes 152 tests, Archipelago's full frontend suite passes
+1,091 tests across 137 files, the production dashboard build and Android UAT
+lint/build pass, and the manifest passes all 16 validators. The read-only,
+capability-free container passes health, asset, manifest, and base-path checks.
+The live same-origin route reaches the authenticated app gate through the
+node's loopback, LAN, Tailscale, and FIPS addresses. A rollback snapshot is at
+`/var/backups/archipelago/pre-uat-fixes-20260908-1140` on the development node.
-- repo id: `archy`
-- display name: `Archipelago`
-- clone URLs:
- - `nostr:////archy`
- - `https:////archy.git`
-- relays:
- - Archipelago-operated relay
- - at least two public Nostr relays that support the event load
-- GRASP servers:
- - Archipelago-operated GRASP instance
- - one public GRASP-compatible mirror
+### 5. Owner UAT — pending owner action
-Keep the existing HTTP Git remote as a mirror during launch. The docs can
-present `nostr://` as the preferred contribution path once the workflow is
-proven.
+The owner validates install, launch, navigation, repository discovery, identity
+selection, consent prompts, source browsing, and available contribution flows.
+Engineering fixes UAT findings on this node and repeats the gate. Owner UAT is
+not inferred from automated tests.
-## UI Requirements
+For companion testing, the node hosts a local-only Archipelago Companion
+`0.5.32-uat` at `/packages/archipelago-companion-0.5.32-uat.apk`. It uses the
+separate package ID `com.archipelago.app.uat`, installs beside the existing
+companion, and includes the native WebView launch plus Android's native node-CA
+installer. Its SHA-256 is
+`8924d7ba3a013e0db09a5f1e72c21de7886e5e21ed1b2e31d585495183191fe7`.
+The production companion download remains unchanged.
-The source app should provide:
+Owner UAT should cover:
-- A first-run contribution modal with a real Archipelago source graphic, not a
- generic text-only dialog.
-- Current clone status and local path.
-- Branch list, changed files, commit form, and push/open-PR flow.
-- PR inbox, issue list, maintainer status, and relay health.
-- Explicit identity indicator showing which npub will sign events.
-- A merge rights indicator that clearly says contributors can propose changes
- but cannot merge them.
-- A fork flow that creates a user-owned NIP-34 repo announcement and remote,
- then offers "Open PR to Archipelago" from any fork branch.
-- Maintainer badges based only on pinned canonical maintainer npubs, not relay
- metadata or server-side account names.
-- Links to container docs, deployment docs, manifest spec, and open-source
- readiness tasks.
+1. Install/reinstall GitWorkshop from the local App Store and open it from the
+ App Store, Apps screen, and Source banner. Confirm Discover shows Popular
+ Apps first, the banner after two desktop rows, and the remaining catalog
+ under All Apps; confirm the GitWorkshop mark is no longer the old icon.
+2. Confirm it opens as a top-level page in Companion's native in-app browser,
+ not a dashboard iframe, and loads without a blank or "webpage unavailable"
+ screen. Confirm Back and Close return through the Companion UI correctly.
+3. Select a node identity, exercise `getPublicKey` and signing prompts, verify
+ the contained consent surface, short identity-circle loader, success/error,
+ allow/deny/remember behavior, then change identity and confirm consent is
+ requested again. Repeat this flow inside the Companion WebView.
+4. Edit a Nostr identity and confirm the identity-specific success screen shows
+ the saved identity, relay coverage, event ID, copy action, and honest partial
+ publish warning when a relay does not accept the update.
+5. Browse a known NIP-34 repository and exercise the contribution actions that
+ GitWorkshop exposes without granting direct merge or release authority.
+6. From Companion settings, choose **Download this node's certificate** and
+ confirm Android opens the system CA-install prompt for this node. Confirm
+ the ordinary browser link still downloads the `.crt` file.
+7. Repeat launch through whichever of LAN, Tailscale, FIPS, DNS, IPv4, or IPv6
+ is available; the app must follow the dashboard origin rather than a stored
+ address. A raw numeric address works over HTTP; for HTTPS over Tailscale use
+ the node's MagicDNS hostname because the certificate is issued to that name,
+ not to the numeric Tailscale address.
-## Backend Work
+UAT follow-up on 2026-09-08 found three integration defects: the mounted gate
+used root-relative form/assets and returned nginx 405 in a fresh mobile
+browser; silent signer requests flashed the full-screen broker frame in the
+Companion WebView; and IndeeHub reloaded while the signer's success surface was
+still closing, leaving Android WebView blank. The fixes are implemented with a
+validated forwarded mount, consent-driven broker visibility, and a coordinated
+post-auth reload plus native page-commit fallback. These items remain pending
+owner retest on the development node; their implementation is not UAT
+acceptance. The fixes were deployed locally on 2026-09-08. Live engineering
+checks confirm that mounted gate pages and assets retain the app prefix, gate
+POSTs return the application's 401 response instead of nginx 405 over HTTP and
+LAN HTTPS, both apps are healthy, and the served provider and UAT APK match
+their build hashes.
-Add an RPC module for source contribution workflow:
+A further Companion retest showed a black surface immediately after the first
+identity selection even though authentication, reload, application data, and
+`/api/auth/me` all completed successfully. The common cause was the Android
+Chromium compositor retaining the hidden broker iframe's last full-screen black
+canvas. The broker route now has a genuinely transparent document, and hidden
+brokers stay loaded as a non-interactive 1px surface parked off-screen so the
+identity choice and immediately following sign request share one broker.
+Companion covers an expected authentication navigation with its branded loader
+until the app commits a new frame. This is deployed in `0.5.32-uat` and
+remains pending owner visual retest.
-- `source.repo-info`: returns canonical announcement, clone URL, relay set,
- maintainer npubs, and local clone state.
-- `source.ensure-ngit`: verifies the `ngit` app/runtime is installed.
-- `source.clone`: clones or updates the local source checkout.
-- `source.status`: returns branch, dirty files, ahead/behind, and PR state.
-- `source.commit`: creates a local commit from selected files.
-- `source.fork`: creates a contributor-owned NIP-34 fork announcement and local
- remote.
-- `source.open-pr`: pushes a PR branch and publishes a kind `1618` event.
-- `source.update-pr`: updates the branch and publishes kind `1619`.
-- `source.issue`: publishes a kind `1621` event.
+### 6. Canonical Nostr Launch — pending
-Backend must shell out through a narrow command wrapper, never arbitrary user
-commands. The wrapper should set an isolated working tree under
-`/var/lib/archipelago/source/archy`, run as the Archipelago service user, and
-deny operations outside that path.
+- Publish and configure the signed `archy` kind `30617` announcement.
+- Bring up and test the chosen relays and GRASP servers.
+- Deep-link/configure GitWorkshop to the verified repository.
+- Run the real-node proposal and recovery drills listed above.
-## Security Model
+### 7. Release — explicitly blocked pending prior gates
-- Never expose maintainer private keys to an Archipelago node.
-- Prefer app-specific contributor identities over the node's default identity.
-- Require per-action consent for first PR push, issue creation, and signing any
- event that tags the canonical repository.
-- Pin the canonical maintainer npub in the app manifest and backend config.
-- Keep the canonical merge-maintainer allow list signed by the
- `archipelago-maintainer` key; never infer merge rights from GRASP server
- accounts.
-- Verify the canonical kind `30617` event signature before displaying clone
- instructions.
-- Treat GRASP servers as untrusted storage; verify Git refs against signed
- Nostr state.
-- Do not use destructive git operations from the UI without an explicit modal.
-- Store local clones and generated patches outside app container writable roots
- unless the user exports them.
+Only after engineering tests, owner UAT acceptance, canonical launch tests,
+license confirmation, and dependency review may the team:
-## MVP
+- build and publish a production multi-architecture app image;
+- sign/update the production app-registry entry;
+- include the integration in an OTA or ISO;
+- add release notes and migration/rollback instructions.
-1. Package `ngit` as a first-party app.
-2. Stand up one Archipelago-operated GRASP server and one Nostr relay.
-3. Publish sanitized fresh-history `archy` through `ngit init`.
-4. Add a simple `archipelago-source` app that clones source and links out to the
- preferred Nostr Git browser.
-5. Add app-launcher consent scopes for repository-specific NIP-34 signing.
-6. Allow issues and PR branch submission from contributor npubs.
-7. Add a one-click fork flow that publishes a contributor-owned fork
- announcement referencing canonical Archipelago.
-8. Keep maintainer merge/status publication manual.
+The production companion signing path also needs an explicit release decision.
+The current branch omits the shared debug keystore expected by
+`scripts/publish-companion-apk.sh` (an older repository revision contains it),
+while the local UAT key is intentionally unsuitable for public artifacts.
+Before publishing, verify upgrade compatibility against the already-distributed
+companion's signing certificate and stage only the intended production-signed
+APK.
-## Later
+## Completed Next-OTA Follow-ups
-- Native PR review UI with file diffs and inline comments.
-- CI status events signed by the build identity.
-- FIPS-first source sync between trusted Archipelago nodes.
-- Private prerelease repositories using NIP-42 allow lists and/or protected
- events if the ecosystem support is mature enough.
-- Multi-maintainer policy with threshold signatures or explicit maintainer-list
- rotation events.
+- The container doctor detects a missing rootless Podman `pasta` listener and
+ restarts only the affected container, including the intermittent Nginx Proxy
+ Manager port 8081 case. TCP and UDP bindings are checked independently.
+- The node-certificate UI contains the approved macOS, iOS/iPadOS, Windows,
+ Android, Linux, browser restart, DNS, and symptom/cause guidance, while the
+ Companion hands the downloaded node CA to Android's system installer.
-## Open Questions
+## Open Decisions Before Canonical Launch
-- Which maintainer npub should become canonical for `archy`?
-- Should contributor identities be node-default or app-specific by default?
-- Which GRASP implementation should be deployed first: `ngit-grasp` or another
- NIP-34/GRASP-compatible relay?
-- Should the source app include a full web Git UI in v1, or launch Gitea/ngit
- browser links for review while keeping signing/submission native?
-- What exact license and contribution certificate should contributors accept
- before submitting PR events?
+- Which Archipelago-operated and independent relay/GRASP endpoints are used?
+- Will upstream add an explicit GitWorkshop license file, or provide another
+ written redistribution grant suitable for registry publication?
diff --git a/image-recipe/_archived/build-auto-installer-iso.sh b/image-recipe/_archived/build-auto-installer-iso.sh
index c03c62a4..be1b67b2 100755
--- a/image-recipe/_archived/build-auto-installer-iso.sh
+++ b/image-recipe/_archived/build-auto-installer-iso.sh
@@ -262,7 +262,12 @@ ROOTFS_STAMP="$WORK_DIR/archipelago-rootfs.recipe.sha256"
# were added to the Dockerfile below — the cache condition never looked at
# the recipe. Hash the rootfs-defining region of this script; any edit to it
# forces a rebuild. `--rebuild` still forces one unconditionally.
-RECIPE_HASH=$(sed -n '/^# STEP 1: Build complete root filesystem/,/^# STEP 2: Build minimal installer/p' "$0" | sha256sum | cut -d' ' -f1)
+RECIPE_HASH=$(
+ {
+ sed -n '/^# STEP 1: Build complete root filesystem/,/^# STEP 2: Build minimal installer/p' "$0"
+ cat "$SCRIPT_DIR/../configs/install-ngit.sh"
+ } | sha256sum | cut -d' ' -f1
+)
if [ ! -f "$ROOTFS_TAR" ] || [ "${1:-}" == "--rebuild" ] || [ "$(cat "$ROOTFS_STAMP" 2>/dev/null)" != "$RECIPE_HASH" ]; then
echo " Using Docker to create Debian root filesystem..."
@@ -451,6 +456,13 @@ COPY --from=fips-builder /tmp/fips.deb /tmp/fips.deb
RUN apt-get update && apt-get -y full-upgrade && apt-get install -y --no-install-recommends /tmp/fips.deb && \
apt-get clean && rm -rf /var/lib/apt/lists/* && rm /tmp/fips.deb
+# Install the pinned Nostr Git runtime. The installer verifies the release
+# archive before copying ngit and git-remote-nostr into /usr/bin.
+COPY install-ngit.sh /tmp/install-ngit.sh
+RUN chmod 0755 /tmp/install-ngit.sh && \
+ /tmp/install-ngit.sh && \
+ rm /tmp/install-ngit.sh
+
# Configure locale
RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen
@@ -796,6 +808,11 @@ NGINXCONF
echo " Using nostr-relay-config.toml from configs/"
fi
+ if [ -f "$SCRIPT_DIR/../configs/install-ngit.sh" ]; then
+ cp "$SCRIPT_DIR/../configs/install-ngit.sh" "$WORK_DIR/install-ngit.sh"
+ echo " Using pinned ngit installer from configs/"
+ fi
+
# Copy WireGuard helper script (privileged peer management)
if [ -f "$SCRIPT_DIR/../../scripts/archipelago-wg" ]; then
cp "$SCRIPT_DIR/../../scripts/archipelago-wg" "$WORK_DIR/archipelago-wg"
diff --git a/image-recipe/configs/archipelago-doctor.service b/image-recipe/configs/archipelago-doctor.service
index 6112febb..6caa33b7 100644
--- a/image-recipe/configs/archipelago-doctor.service
+++ b/image-recipe/configs/archipelago-doctor.service
@@ -6,7 +6,7 @@ After=archipelago.service
Type=oneshot
# Runs as root: needs to kill orphaned conmon processes, fix permissions
User=root
-ExecStart=/home/archipelago/archy/scripts/container-doctor.sh --local
+ExecStart=/opt/archipelago/scripts/container-doctor.sh --local
TimeoutStartSec=300
StandardOutput=journal
StandardError=journal
diff --git a/image-recipe/configs/install-ngit.sh b/image-recipe/configs/install-ngit.sh
new file mode 100755
index 00000000..54689701
--- /dev/null
+++ b/image-recipe/configs/install-ngit.sh
@@ -0,0 +1,82 @@
+#!/bin/sh
+# Install the exact ngit runtime validated for Archipelago source hosting.
+#
+# The GitHub release archive contains both `ngit` and `git-remote-nostr`.
+# Keep version, filenames, and SHA-256 values together so image builds and
+# OTA updates cannot silently resolve a newer upstream release.
+
+set -eu
+
+NGIT_VERSION="2.6.3"
+NGIT_RELEASE_BASE="https://github.com/DanConwayDev/ngit-cli/releases/download/v${NGIT_VERSION}"
+X86_64_ASSET="ngit-v${NGIT_VERSION}-x86_64-unknown-linux-gnu.2.17.tar.gz"
+X86_64_SHA256="81dd9b6a11a4a0feb946e56f55d557dc24075f1dcdda00ac35f9fd01920b9779"
+AARCH64_ASSET="ngit-v${NGIT_VERSION}-aarch64-unknown-linux-gnu.2.17.tar.gz"
+AARCH64_SHA256="e9d9437b7574e729b5a5d5cd800ebd668b73e6eb5c5859d52f83114c2f4b08b8"
+
+install_root="${ARCHIPELAGO_NGIT_INSTALL_ROOT:-}"
+case "$install_root" in
+ ""|/*) ;;
+ *)
+ echo "ARCHIPELAGO_NGIT_INSTALL_ROOT must be empty or absolute" >&2
+ exit 2
+ ;;
+esac
+
+install_dir="${install_root}/usr/bin"
+ngit_bin="${install_dir}/ngit"
+helper_bin="${install_dir}/git-remote-nostr"
+
+if [ -x "$ngit_bin" ] && [ -x "$helper_bin" ] && \
+ [ "$($ngit_bin --version 2>/dev/null || true)" = "ngit ${NGIT_VERSION}" ] && \
+ [ "$($helper_bin --version 2>/dev/null || true)" = "v${NGIT_VERSION}" ]; then
+ echo "ngit ${NGIT_VERSION} already installed"
+ exit 0
+fi
+
+machine="${ARCHIPELAGO_NGIT_ARCH:-$(uname -m)}"
+case "$machine" in
+ x86_64|amd64)
+ asset="$X86_64_ASSET"
+ expected_sha256="$X86_64_SHA256"
+ ;;
+ aarch64|arm64)
+ asset="$AARCH64_ASSET"
+ expected_sha256="$AARCH64_SHA256"
+ ;;
+ *)
+ echo "Unsupported ngit architecture: $machine" >&2
+ exit 2
+ ;;
+esac
+
+download_dir=$(mktemp -d -t archipelago-ngit.XXXXXX)
+cleanup() {
+ rm -rf -- "$download_dir"
+}
+trap cleanup EXIT HUP INT TERM
+
+archive="$download_dir/$asset"
+curl --fail --silent --show-error --location \
+ --proto '=https' --tlsv1.2 \
+ --retry 3 --connect-timeout 20 \
+ --output "$archive" "$NGIT_RELEASE_BASE/$asset"
+
+actual_sha256=$(sha256sum "$archive" | awk '{print $1}')
+if [ "$actual_sha256" != "$expected_sha256" ]; then
+ echo "ngit archive checksum mismatch for $asset" >&2
+ echo "expected: $expected_sha256" >&2
+ echo "actual: $actual_sha256" >&2
+ exit 1
+fi
+
+# Extract only the two expected top-level files. Unexpected archive content is
+# never copied into the host filesystem.
+tar -xzf "$archive" -C "$download_dir" ngit git-remote-nostr
+mkdir -p "$install_dir"
+install -m 0755 "$download_dir/ngit" "$ngit_bin"
+install -m 0755 "$download_dir/git-remote-nostr" "$helper_bin"
+
+[ "$($ngit_bin --version)" = "ngit ${NGIT_VERSION}" ]
+[ "$($helper_bin --version)" = "v${NGIT_VERSION}" ]
+echo "installed ngit ${NGIT_VERSION} for $machine"
diff --git a/image-recipe/configs/nginx-archipelago.conf b/image-recipe/configs/nginx-archipelago.conf
index 052d932e..7fb83ff4 100644
--- a/image-recipe/configs/nginx-archipelago.conf
+++ b/image-recipe/configs/nginx-archipelago.conf
@@ -52,6 +52,17 @@ server {
try_files $uri =404;
}
+ # Dashboard-origin Nostr signer for apps opened as their own browser tab or
+ # companion WebView. This document alone may be framed by another port on
+ # the same node; signing RPCs still require an authenticated node session.
+ location = /nostr-signer {
+ try_files /index.html =404;
+ add_header Cache-Control "no-store" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header Referrer-Policy "no-referrer" always;
+ add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self' http://$host:* https://$host:*; base-uri 'none'; form-action 'none';" always;
+ }
+
# AIUI SPA (Chat mode iframe) — SPA fallback for client-side routing
#
# /aiui/-scoped CSP (AIUI-04, D-19 unaffected — this is a build-time/
@@ -691,13 +702,32 @@ server {
sub_filter "src='/" "src='/app/botfights/";
sub_filter '' '';
}
+ # GitWorkshop follows the dashboard origin so every supported ingress
+ # works without separately publishing an app port. The app gate on
+ # 127.0.0.2 preserves session authentication before forwarding to the
+ # loopback-only container.
+ location /app/archipelago-source/ {
+ proxy_pass http://127.0.0.2:8337/;
+ proxy_http_version 1.1;
+ proxy_set_header Host $http_host;
+ proxy_set_header Cookie $http_cookie;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Prefix /app/archipelago-source;
+ proxy_hide_header X-Frame-Options;
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ proxy_read_timeout 300s;
+ }
location /app/gitea/ {
proxy_pass http://127.0.0.1:3001/;
+ proxy_request_buffering off;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
- client_max_body_size 1G;
+ client_max_body_size 10G;
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
# Override parent add_header to allow iframe embedding
@@ -1037,6 +1067,16 @@ server {
return 504 '{"error":{"code":"BACKEND_TIMEOUT","message":"Service did not respond in time"}}';
}
+ # Dashboard-origin Nostr signer for apps opened as their own browser tab or
+ # companion WebView. Keep this aligned with the HTTP server block.
+ location = /nostr-signer {
+ try_files /index.html =404;
+ add_header Cache-Control "no-store" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header Referrer-Policy "no-referrer" always;
+ add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self' http://$host:* https://$host:*; base-uri 'none'; form-action 'none';" always;
+ }
+
# AIUI SPA (Chat mode iframe) — SPA fallback for client-side routing
#
# /aiui/-scoped CSP — see the HTTP server block above for the full
@@ -1479,4 +1519,3 @@ server {
proxy_read_timeout 86400s;
}
}
-
diff --git a/image-recipe/configs/snippets/archipelago-https-app-proxies.conf b/image-recipe/configs/snippets/archipelago-https-app-proxies.conf
index 83f86cac..db8dc03e 100644
--- a/image-recipe/configs/snippets/archipelago-https-app-proxies.conf
+++ b/image-recipe/configs/snippets/archipelago-https-app-proxies.conf
@@ -33,9 +33,27 @@ location /app/uptime-kuma/ {
sub_filter_once on;
sub_filter '' '';
}
+# GitWorkshop follows the dashboard origin; the app gate keeps the route
+# session-authenticated before it reaches the loopback-only container.
+location /app/archipelago-source/ {
+ proxy_pass http://127.0.0.2:8337/;
+ proxy_http_version 1.1;
+ proxy_set_header Host $http_host;
+ proxy_set_header Cookie $http_cookie;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Prefix /app/archipelago-source;
+ proxy_hide_header X-Frame-Options;
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ proxy_read_timeout 300s;
+}
location /app/gitea/ {
proxy_pass http://127.0.0.1:3001/;
proxy_http_version 1.1;
+ proxy_request_buffering off;
+ client_max_body_size 10G;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
diff --git a/neode-ui/e2e/nostr-provider-handoff.spec.ts b/neode-ui/e2e/nostr-provider-handoff.spec.ts
new file mode 100644
index 00000000..49156533
--- /dev/null
+++ b/neode-ui/e2e/nostr-provider-handoff.spec.ts
@@ -0,0 +1,71 @@
+import { readFileSync } from 'node:fs'
+import { resolve } from 'node:path'
+import { expect, test } from '@playwright/test'
+
+const providerSource = readFileSync(
+ resolve(process.cwd(), 'public/nostr-provider.js'),
+ 'utf8',
+)
+
+test('mobile Chromium returns to the live app after the signer is hidden', async ({ context, page }) => {
+ await page.setViewportSize({ width: 390, height: 844 })
+
+ await context.route('**/*', async (route) => {
+ const url = new URL(route.request().url())
+ if (url.pathname === '/nostr-provider.js') {
+ await route.fulfill({ contentType: 'application/javascript', body: providerSource })
+ return
+ }
+ if (url.port === '' && url.pathname === '/nostr-signer') {
+ await route.fulfill({
+ contentType: 'text/html',
+ body: `
+
+ `,
+ })
+ return
+ }
+ if (url.port === '7778') {
+ await route.fulfill({
+ contentType: 'text/html',
+ body: `IndeedHub
+
+
+ app-ready`,
+ })
+ return
+ }
+ await route.abort()
+ })
+
+ await page.goto('http://app.test:7778/')
+ await expect(page.locator('#app')).toHaveText('signed-in:browser-handoff-key')
+
+ const broker = page.locator('#archipelago-nostr-signer')
+ await expect(broker).toHaveCount(1)
+ await expect(broker).toHaveCSS('width', '1px')
+ await expect(broker).toHaveCSS('height', '1px')
+ await expect(broker).toHaveCSS('opacity', '0')
+ await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(22, 101, 52)')
+ expect(await page.evaluate(() => document.elementFromPoint(195, 422)?.id)).toBe('app')
+})
diff --git a/neode-ui/playwright.config.ts b/neode-ui/playwright.config.ts
index dc416849..39fca94c 100644
--- a/neode-ui/playwright.config.ts
+++ b/neode-ui/playwright.config.ts
@@ -13,6 +13,9 @@ export default defineConfig({
screenshot: 'only-on-failure',
trace: 'off',
ignoreHTTPSErrors: true,
+ launchOptions: process.env.ARCHY_CHROMIUM_EXECUTABLE
+ ? { executablePath: process.env.ARCHY_CHROMIUM_EXECUTABLE }
+ : undefined,
},
projects: [
{
diff --git a/neode-ui/public/assets/img/app-icons/gitworkshop-dc36db6.svg b/neode-ui/public/assets/img/app-icons/gitworkshop-dc36db6.svg
new file mode 100644
index 00000000..ae2c2d0d
--- /dev/null
+++ b/neode-ui/public/assets/img/app-icons/gitworkshop-dc36db6.svg
@@ -0,0 +1,20 @@
+
diff --git a/neode-ui/public/assets/img/featured/archipelago-source-banner.webp b/neode-ui/public/assets/img/featured/archipelago-source-banner.webp
new file mode 100644
index 0000000000000000000000000000000000000000..2a44767787ef46a8d5fd4e80f587b4f43422c2b7
GIT binary patch
literal 145292
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