feat(release): stage GitWorkshop and next node updates

This commit is contained in:
archipelago
2026-09-09 18:15:21 -04:00
parent 973356df16
commit f5c0ba85cd
97 changed files with 5716 additions and 1327 deletions
+1 -1
View File
@@ -132,7 +132,7 @@ curl -s http://<node>/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`.
+211 -29
View File
@@ -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 `<script src="/nostr-provider.js"></script>` before
`</head>` 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
`<script src="/nostr-provider.js?v=tab-signer-v4"></script>` before
`</head>` 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:
+31
View File
@@ -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/<id>` 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
+224 -198
View File
@@ -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/<npub>/<short-topic>` or `pr/<event-id>`.
- 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://<maintainer-npub>/<relay-hint>/archy`
- `https://<grasp-host>/<maintainer-npub>/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?