> **Nostr Agent Onboarding** · [start here](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/start.md) · [index](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/llms.txt) · source: `nostr-dev/docs/known-issues.md` · snapshot 2026-10-10
>
> Paths such as `~/Documents/…`, `~/Production Environment/…`, `repos/…` and services on `localhost` refer to the author's workstation and are **not available to you** — read them as worked examples of a setup you can recreate.

# Known issues — local stack quirks

> Issues a sibling agent encountered while building `~/work/nostr-archive/`
> against this workspace's local stack. Documented here so future sessions
> don't re-discover them.

Each entry: what breaks, why, the workaround on this machine, and (if
applicable) the upstream fix to track.

## 1. ngit-relay nginx emits CORS headers twice

**Symptom.** A browser fetching anything from `http://localhost:8081`
fails with:

```
Access-Control-Allow-Origin: * (multiple)
The 'Access-Control-Allow-Origin' header contains multiple values...
```

The browser blocks the response. `curl -i` shows two
`Access-Control-Allow-Origin: *` headers in the response.

**Cause.** The container's `src/nginx.conf` adds CORS headers via
`add_header` inside `location @proxy { ... }`, *and* the upstream Khatru
server (proxied to `localhost:3334`) also emits the same headers. Nginx
passes both through — the response ends up with duplicate CORS headers,
which all major browsers reject per the CORS spec.

**Affects.** Any browser-based client hitting the local GRASP / Blossom
server. The TS examples in [`sdk/examples/`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/examples/README.md) don't trigger it because
they run in Node (no browser CORS check). It surfaces the moment you
serve a static page from one origin and hit `localhost:8081` from the
page's JS.

**Workaround (runtime, applied per container start).** Add five
`proxy_hide_header` lines inside `location @proxy` so nginx strips
Khatru's headers and keeps only its own:

```bash
docker exec ngit-relay-ngit-relay-1 sh -c '
  sed -i "/location @proxy {/a\\
        proxy_hide_header Access-Control-Allow-Origin;\\
        proxy_hide_header Access-Control-Allow-Methods;\\
        proxy_hide_header Access-Control-Allow-Headers;\\
        proxy_hide_header Access-Control-Expose-Headers;\\
        proxy_hide_header Access-Control-Max-Age;
  " /etc/nginx/http.d/default.conf && nginx -s reload
'
```

Verify with:

```bash
curl -sI -H 'Origin: http://localhost:9000' http://localhost:8081/list/0000000000000000000000000000000000000000000000000000000000000000 \
  | grep -i access-control-allow-origin
# expected: exactly one line
```

**Note.** This patch is **runtime only**. If you `sudo docker compose
down` and `up`, the patch is gone — re-apply. To make it persistent,
edit `repos/ngit-relay/src/nginx.conf` and `docker compose up -d
--build`. We deliberately don't patch the source in-place because
`repos/ngit-relay` is a clone of the upstream reference implementation
and we want it to track upstream cleanly.

**Discovered by.** The sibling agent building `~/work/nostr-archive/`'s
browser client. See `~/work/nostr-archive/client/README.md` § "Known
nginx CORS quirk in ngit-relay" for the original report.

**Upstream.** Worth filing on the ngit-relay project. The fix is one
config block; the regression is browser-only so the test matrix didn't
catch it.

## 2. Khatru evicts kind:30617 events by `(pubkey, kind)` ignoring `d`-tag

**Symptom.** Re-publishing a kind:30617 GRASP repo announcement under
one `d`-tag silently evicts a different repo's kind:30617 by the same
pubkey. Querying for the *previous* repo returns nothing, even though
the announcement was previously visible.

**Cause.** Per NIP-01, kinds 30000–39999 are *addressable*: relays must
keep the latest event per `(pubkey, kind, d-tag)`. The Khatru
implementation packaged in the local ngit-relay appears to evict on
`(pubkey, kind)` only — treating addressable events as if they were
plain replaceable.

**Affects.** Any pubkey publishing more than one GRASP repo to the local
relay. Single-repo workflows don't see this.

**Reproducer (informal).**

```
nak event --sec $NSEC -k 30617 -t d=alpha ws://localhost:8081
nak event --sec $NSEC -k 30617 -t d=beta  ws://localhost:8081
# both queryable
nak event --sec $NSEC -k 30617 -t d=alpha ws://localhost:8081
# 'beta' is now gone in the buggy version
```

**Workaround.** **Also publish each kind:30617 to a second relay that
handles addressable events correctly.** A standalone `nostr-rs-relay`
on `:8080` (already part of this workspace, see
`configs/local-services.json` → `available_on_demand.nostr_rs_relay`) is
a clean choice. Or use a public relay (`wss://relay.ngit.dev`,
`wss://relay.damus.io`).

The public relay you replicate to acts as the source of truth for
multi-repo announcements; the local relay still serves git pushes for
whatever repos *it* still has. If the bug is in your specific Khatru
build, mirroring is also the diagnostic — when the local query disagrees
with the public query, you've hit it.

**Upstream.** Track via the Khatru codebase
(`github.com/fiatjaf/khatru`) and the eventstore module the local
container uses (`fiatjaf/eventstore` — see
`relays/khatru-example/go.mod` for the version pinned in our example
relay). The eviction logic for addressable events lives in the
eventstore backend (`slicestore`, `lmdb`, etc.) — different backends
may have different behavior.

**Discovered by.** The sibling agent building `~/work/nostr-archive/`,
which publishes one kind:30617 per archive and one per archive's
GRASP-as-structural-layer (so two per archiver, same pubkey, same
kind, different d-tag).

## 3. `relay.zapstore.dev` whitelist verifier does not follow NIP-34 / GRASP repos

**Symptom.** Publishing kind 32267 / 30063 / 3063 events to
`wss://relay.zapstore.dev` is rejected with:

```
event pubkey is not allowed. Visit https://zapstore.dev/docs/publish for more information.
```

Committing a `zapstore.yaml` containing `pubkey: <publisher-npub>` at the
root of the linked source repo — the official path documented at
<https://zapstore.dev/docs/publish> — does **not** lift the rejection when
the source repo is a NIP-34 GRASP repo. The relay continues to refuse.
Kind 30509 (NIP-C1 cert link) and kind 0 (profile) under the same pubkey are
accepted; only the app-event trio is gated.

**Cause.** The relay's auto-whitelist verifier only resolves repository
references against a fixed set of forge hosts — confirmed against
[the publish docs](https://zapstore.dev/docs/publish):

> auto-whitelisting works for repositories hosted on GitHub, GitLab,
> Codeberg, and self-hosted Gitea/Forgejo instances.

Even though kind 32267's `repository` tag is a valid NIP-34 `naddr1…`, the
verifier does not (yet) resolve it to a kind 30617, follow the announced
`clone` URLs, and fetch `zapstore.yaml` from the resulting git tree.
GRASP-hosted projects therefore can never auto-whitelist via the
yaml-in-repo flow.

**Affects.** Any publisher whose canonical source repo is a GRASP repo —
i.e., the canonical configuration of *this* workspace. Three options
remain:

1. **Mirror to a supported forge.** Push the source to
   `github.com/<you>/<repo>` (or Codeberg, GitLab, Gitea, Forgejo) with the
   same `zapstore.yaml` at root. Re-issue the app events; the verifier
   reads the yaml from the forge, matches pubkey↔repo, and admits future
   publishes from that pubkey. Requires keeping the forge mirror in sync
   on every release.
2. **Vertex reputation.** Accumulate Nostr social activity under the
   publisher pubkey until their reputation system admits it. Passive;
   not directly actionable.
3. **Skip `relay.zapstore.dev`.** Three general-purpose relays
   (`relay.damus.io`, `nos.lol`, `relay.primal.net`) accept the same
   events with no whitelist gate. Any outbox-model-aware Zapstore client
   not pinned exclusively to the Zapstore relay will find the app from
   those.

**Discovered by.** This workspace, 2026-05-12, publishing OTSuite Mobile
v2.0.0 under `npub1zkd4h6zshlh2a3yuzaxqk2wgk2kv2za5trz76rd5upwy06fscjrq8mk5ta`.
The repo had its kind 30617 / 30618 on `relay.ngit.dev`, `zapstore.yaml`
with `pubkey: …` was committed (`6444c9d…`), and the public clone URL was
reachable — the relay still rejected. Cert-link (30509) and profile (0)
under the same pubkey were accepted.

**Upstream.** Worth filing on the `zapstore` org — implementing
naddr-aware verification would close the gap. Track at
<https://github.com/zapstore> (there is no dedicated relay repo public at
the time of writing; the `zsp` CLI is the closest published artifact).

## 4. `relay.ngit.dev` rejects events that don't carry an NIP-34 `a`-tag

**Symptom.** Pushing zapstore-style events (kind 30509, 30063, 3063, or
even kind 0) to `wss://relay.ngit.dev` under a pubkey that *is* the
maintainer of a stored GRASP repo returns:

```
Event event must reference an accepted repository or accepted event
```

Only kind 32267 lands, because `zsp` emits it with an `a`-tag containing
the kind 30617 coordinate of the linked repo.

**Cause.** `relay.ngit.dev` runs the same Khatru policy as the local
`ngit-relay` container (`ws://localhost:8081`): an event is only stored if
it explicitly references a stored repo by NIP-34 coordinate (`a`-tag
`30617:<pubkey>:<d>`) or by event id of an already-accepted event
(`e`-tag). Zapstore's release/asset/identity events reference the app by
`i`-tag (package id) instead, which the policy doesn't recognize. By design
— it's a *git repository* relay, not a general-purpose one — but the
selective acceptance is surprising on first contact.

**Affects.** Any flow that publishes non-NIP-34 events under a pubkey
whose primary identity is "GRASP repo maintainer." The most concrete case
is the zapstore publish documented in
[`patterns/zapstore-publishing.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/zapstore-publishing.md):
four of five event kinds bounce off `relay.ngit.dev` even though they're
all signed by the same npub that maintains the linked repo.

**Workaround.** Skip `relay.ngit.dev` for non-repo-tagged events; the
three general-purpose relays carry them fine. Hand-patching each event to
include an `a`-tag is possible but rewrites the event id (different
content → different sha256 → different signature), which then diverges
from the canonical event on every other relay — not worth it.

**Discovered by.** Same session as issue #3.

**Upstream.** Khatru policy in the `ngit-relay` reference implementation
(`repos/ngit-relay/`). Relaxing the rule to accept e.g. "any event from a
pubkey that maintains a stored repo, regardless of tag form" would close
this gap. Track via the `ngit-relay` project.

## 5. `nak event --sec ...` silently re-signed fully-formed events on stdin — RESOLVED in nak v0.19.2

**Resolved upstream.** nak commit
[`faac4d9`](https://github.com/fiatjaf/nak/commit/faac4d9440368fcad7f46c077089cc10fe37d81f)
(2026-03-17, "ensure an event is not resigned if it was already signed and
wasn't changed") first shipped in **v0.19.2**, tagged 2026-03-20. The `nak` on
`PATH` is v0.20.7 (§7.1), so it has the fix. The behaviour in §5.1 started in
v0.7.9 (commit `134d122`, 2024-10-29) and lasted **through v0.19.1**.

**The rule now.** A piped event that already carries a `sig` and that no flag
modifies is printed and published exactly as given. `--sec`,
`NOSTR_SECRET_KEY` and `--auth` do not count as modifications, and the key is
used only to answer NIP-42 AUTH. nak still re-signs when:

- a flag modifies the event: `-k`, `-c`, any tag flag (`-t`, `-e`, `-p`, `-d`,
  `-h`, `-a <naddr>`), `--ts`/`--created-at`, `--pow`, `--musig`;
- the input is incomplete (no `kind`, `created_at` or `sig`), so nak fills it
  in;
- **`--force-sign`** is given (new in v0.19.2).

A re-sign uses the `--sec` key, or nak's per-machine default key if there is
none. If that isn't the event's own key, `pubkey`, `id` and `sig` all change:
the old trap. The odd one out is `-a <pubkey>`: it overwrites `pubkey` but does
**not** re-sign, so the output is an invalid event.

**Gift wraps.** An unmodified NIP-59 wrap now goes through `nak event --auth
--sec <maintainer-key> <relay>` intact. Its `pubkey` stays the ephemeral key the
seal was encrypted to, so the recipient can decrypt it. `--force-sign`, or any
flag that modifies the event, still overwrites that ephemeral pubkey with the
`--sec` key and makes the wrap undecryptable (mechanism in §5.1). Never pass
either with a wrap.

**The reverse trap.** Anything that relied on `--sec` to re-sign now publishes
the input unchanged. That includes re-authoring an event under another key and
repairing a hand-edited signed JSON. nak checks only that a `sig` is *present*,
not that it verifies, so an edited event goes out with its stale `id` and `sig`
and relays reject it. Add `--force-sign`.

**Pre-flight.** What `nak event` prints is what it would publish. So before
publishing a pre-formed event, run the same command without relay URLs and
compare. Use `jq -cS .` on both sides, because nak re-serialises JSON that
another tool wrote:

```bash
diff <(jq -cS . wrap.json) <(nak event --auth --sec "$NSEC" < wrap.json | jq -cS .) \
  && echo "publishes as given"
```

For an older `nak`, the TypeScript helper in §5.1 remains the route.

**Verified 2026-09-26, offline.** Every command ran inside `bwrap
--unshare-net` (loopback only), with no relay argument, so nothing was
published. Throwaway keys: A signed the input, B is the other `--sec`, R is the
gift-wrap recipient. "Re-signed as B" means pubkey B with a new `id` and `sig`,
still valid. v0.18.3 is the parked `~/.local/bin/nak.v0.18.3.bak`.

| Piped input → `nak event …` | v0.18.3 | v0.20.7 |
|---|---|---|
| note signed by A → `--sec B` | re-signed as B | **byte-identical** |
| same → `NOSTR_SECRET_KEY=B`, no flag | re-signed as B | byte-identical |
| same → `--sec A` (its own key) | byte-identical | byte-identical |
| same → `--sec B --auth` | not run | byte-identical |
| same → `--sec B -t x=y` | not run | re-signed as B |
| same → `--sec B --force-sign` | (no such flag) | re-signed as B |
| same → `--sec B -a <pubkey B>` | not run | pubkey B, old `id`/`sig`: **invalid** |
| note signed by A, content then hand-edited → `--sec A` | re-signed, valid | passed through, **invalid** |
| gift wrap A→R → `--sec B` | pubkey B, **R cannot decrypt** | byte-identical (also with `--auth`), R decrypts |
| gift wrap A→R → `--sec B --force-sign` | (no such flag) | pubkey B, **R cannot decrypt** |

R's decryption was checked two ways: layer by layer with `nak decrypt`, and
with `nak gift unwrap`. A forced re-sign with the event's own key also comes out
byte-identical, because signing is deterministic.

**Not tested: a live AUTH publish.** From the source: with an event on stdin,
`publishFlow` takes its plain (non-TTY) path. On `auth-required:` it calls
`relay.Auth(ctx, kr.SignEvent)`, which signs only a kind 22242 AUTH event with
the `--sec` key, and then republishes the same, unchanged `evt`.

To re-check after a nak upgrade (no relay argument, so nothing is published):

```bash
A=$(nak key generate); B=$(nak key generate)
nak event -k 1 -c hi --sec "$A" </dev/null > /tmp/a.json
nak event --sec "$B" < /tmp/a.json | cmp - /tmp/a.json && echo unchanged   # fixed
nak event --sec "$B" --force-sign < /tmp/a.json | jq -r .pubkey            # → B's pubkey
```

### 5.1 History: the entry as logged 2026-05-12 (nak v0.7.9 – v0.19.1)

Kept as written, apart from the dated notes.

**Symptom.** You pipe a complete, signed event into `nak event` and pass
`--sec` so the relay's NIP-42 AUTH challenge can be answered. nak prints
"publishing... success" and the relay accepts the event — but the event
that lands on the relay has its `pubkey` rewritten to whatever pubkey
corresponds to `--sec`, with a new `id` and a new `sig`. The original
`pubkey` is lost.

For a NIP-59 gift wrap this is silently *catastrophic*: the encrypted
content (the kind 13 seal) was created with `ECDH(ephemeral_priv,
recipient_pub)`. The recipient decrypts by computing
`ECDH(recipient_priv, event.pubkey)`. When nak overwrites `event.pubkey`
with the maintainer's identity, `event.pubkey` no longer matches the key
the ciphertext was sealed for — NIP-44 v2 decryption fails and the message
appears as undecryptable garbage on the recipient's side.

**Cause.** `nak event` documents that a piped event "is rehashed and
resigned if modified, otherwise just returned as given." The
modification detector treats a `--sec` whose derived pubkey differs from
`event.pubkey` as a request to change `event.pubkey`, then dutifully
rehashes and re-signs. There is no warning, no `--no-rehash` flag, and
the printed output is the re-signed event (visible only with `--verbose`
during a publish).

*Correction, 2026-09-26: the source shows a wider trigger.* From `134d122`
through v0.19.1, `event.go` set `mustRehashAndResign` whenever `--sec` or
`--prompt-sec` was set at all, and `NOSTR_SECRET_KEY` in the environment
counted too. Nothing compared the key with `event.pubkey`. With the event's
own key the re-sign was invisible, because deterministic signing gives
byte-identical output. With any other key, `pubkey`, `id` and `sig` were
rewritten. The effect was as described above.

**Affects.** Any pipeline of the form

```bash
nak event --sec <nsec> <relay-with-auth> < some-event.json
```

where `some-event.json` was produced by another tool and its `pubkey` is
not the same as the one derived from `<nsec>`. NIP-59 gift wraps are the
canonical victim (ephemeral wrap key by design). Same trap applies to
NIP-46 bunker responses you proxy, decoupled-key flows, anything signed
by an ephemeral session key, or events you're republishing from a relay
on someone else's behalf.

*Note, 2026-09-26:* the same pipe with no `--sec` flag was hit too whenever
`NOSTR_SECRET_KEY` was exported (measured on v0.18.3).

**Reproducer.**

```bash
# Pre-formed event with one pubkey...
nak event -k 1 -c hi --sec <nsec-A> </dev/null > /tmp/a.json
jq -r .pubkey /tmp/a.json   # → pubkey of A

# ...piped through nak with a *different* --sec for AUTH only
nak --verbose event --sec <nsec-B> wss://relay-needing-auth < /tmp/a.json \
  | grep -m1 pubkey
# → pubkey of B (silently rewritten), id changed, content preserved
```

**Workaround.** Don't pass `--sec` to `nak event` when publishing a
pre-formed event. For relays that demand NIP-42 AUTH this means you have
to handle AUTH yourself — and this workspace ships exactly that helper:

```bash
# In sdk/examples/publish-wrap-with-auth.ts:
NOSTR_SECRET_KEY=<nsec> npx tsx examples/publish-wrap-with-auth.ts \
  /tmp/event.json wss://auth-required-relay [more-relays]
```

The helper opens a raw `ws://` connection per relay, waits ~500 ms for a
proactive `["AUTH", <challenge>]` from the relay (and also handles the
`["OK", <id>, false, "auth-required: …"]` follow-up form), signs a kind
22242 AUTH event with the supplied key, retries the EVENT once, and
publishes the event **byte-for-byte unchanged**. Reusable for any
pre-formed-event + AUTH-relay combo, not just gift wraps.

If you need to clean up after hitting this in production, a NIP-09 kind:5
deletion request signed by the *re-signed* event's `pubkey` (i.e. the
maintainer key, since that's what nak replaced into the event) referencing
the broken event's `id` is what compliant relays will honour.

**Discovered by.** This workspace, 2026-05-12, sending a NIP-17 / NIP-59
gift-wrapped DM from the OTSuite Mobile maintainer to the Zapstore admin
to request a manual whitelist. The first two attempts went through `nak
event --auth --sec ...` and produced an undecryptable wrap; the helper
above is the fix.

*Note, 2026-09-26:* by then the fix had been out for 53 days (v0.19.2,
2026-03-20). Per §7.1, though, the `nak` on `PATH` until 2026-09-21 was
v0.18.3. Before logging a nak bug, compare `go version -m $(command -v nak)`
against the latest release.

**Upstream.** Worth filing on `fiatjaf/nak`. Either an explicit
`--no-rehash` flag, or a default that refuses to silently mutate
`event.pubkey` when the input event was already validly signed, would
prevent recurrence.

*Note, 2026-09-26: already done.* v0.19.2 shipped the second option: signed,
unmodified input is no longer re-signed. It checks that a `sig` is present,
not that it is valid. `--force-sign` is the explicit opt-in. There is nothing
left to file for this issue. The `-a <pubkey>` case in the rule above is
unfiled.

## 6. Signer login (NIP-07 / NIP-46 / NIP-55): relay rot and known-bad code on this machine

**Read first:** [`patterns/signer-login.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/signer-login.md) is the
implementation guide. This section holds only the *state of this machine*, as
audited 2026-09-19.

**Relays.**
- `wss://relay.nsec.app` and `wss://relay.nostr.band` were unreachable from
  here (TLS/WS connect times out). Both are still hard-coded as defaults in
  several projects and libraries.
- `auth.nostr1.com` demands NIP-42 AUTH before accepting a 24133.
- `relay.nos.social` blocks kind 24133.
- The local ngit-relay (`:8081`) and nostr-rs-relay (`:8080`) are unsuitable
  for NIP-46 tests.
- Working set: see guide §3.6.

**Code that looks like a reference but is still wrong. Do not copy it
without the fix.** Found by reading; none of it has been fixed.

| Where | Bug | Guide § |
|---|---|---|
| Every Go app on **stock** `fiatjaf.com/nostr` nip46/keyer (CFR, icdib, Starcraft Replaystr, nostrkit, Nostr-Music-Sampler V1–V3, In-Your-Face) | Waits for EOSE from all relays; drops NIP-04 replies; 30 s hard-coded sign timeout; broken `switch_relays`; waits for the relay OK before reading replies | 3.7 |
| `In-Your-Face-try2/internal/identity/nostrconnect.go:110`, `cmd/iyf-web/main.go:299`; `I can do it better/cmd/icdib/api.go:216`, `main.go:202` | Bunker session built on a context that is cancelled when login returns, so the next sign fails with "context canceled" | 3.7 (Go lifetime rule) |
| `nostrkit/identity/identity.go:131,159` (and CFR, icdib, Replaystr) | Raw login input wrapped with `%w`, so a mistyped nsec ends up in error text; `Redact` leaves `secret=` in bunker URIs | 2.6 |
| `OTSuite-mobile` `Navigation.kt:297`, `NostrSigner.kt:117` | NIP-55 rejection (`RESULT_OK`+`rejected`) treated as success with an empty signature; missing column crashes instead of falling back | 4.3–4.5 |
| `Idle StarFighter/web/shell.js:808` | NIP-55 web: rebuilds the event with a new `created_at` after the callback, so the signature is invalid | 5.3 |
| `Nostr-Agenda` `SessionManager.kt:206` | NIP-46 `PERMS` contains bare `sign_event`, which Amber drops (the test only passed against nak) | 2.3 |
| `Desktop/Archon-Web/src/bunker.js:313` | nostrconnect accepts a bare `"ack"` (spoofable) | 3.4.5 |
| MKStack / `@nostrify/react` 0.50.2 `NUser.fromBunkerLogin` (EarthTreeMedia) | Restores with the user pubkey as the signer pubkey | 1 |
| `work/nostr-archive-app` `Publish.tsx:279`, ETM `LoginDialog.tsx:173`, grimoire `LoginDialog.tsx:393` | NIP-07 availability decided at render time | 6.1 |
| `Sessions/GRASPCONTROL` `manager.go:312` | QR flow can record the signer pubkey as the user when `get_public_key` times out; cancel doesn't stop the listener | 1, 3.4 |
| `Facebook-Museum/app/third_party/nostr/nip46/client.go:28` (`ConnectPerms`, used at `:116`, `signer/nostrconnect.go:77`) | Bare `get_public_key,sign_event` perms (Amber strips them); client key kept in session memory only (D44), so a reload makes the client a stranger again | 2.3, 3.3.2 |
| `Testimony/internal/signer/nip07.go` `SignEvent`; `third_party/nostr/FORK.md` "Known divergence" | Returned pubkey not compared with the session; the FORK note is stale (`portable.go:202` does send perms) | 2.1 |
| Stock `fiatjaf.com/nostr` `wellknownnostrjson.go` | NIP-05 bunker lookup returns an unassigned named return, so login targets the zero pubkey and looks like "signer never answered" (fixed as Testimony Delta 10) | 3.7 |
| `Sessions/Musig2_main/Archon-Web_v2/src/lib/core/identity/bunker-signer.ts:252` | Random client key per instance; nip44-only decrypt | 3.3.2, 3.5 |
| `Sessions/bunker-android` `Bunker.kt:58` (signer side) | `connect` acks without checking the secret; incoming signatures aren't verified | — |

**Secret leak to clean up.** `EarthTreeMedia-Nostr` `rip/blossom-upload.log`,
`rip/media-map.json`, `compare/new-assets.log` and `compare/new-assets-map.json`
contain a full `bunker://…secret=` URI and the client-key hex. A timed-out
`nak` call echoed its command line into the logs. Scrub the files, and
consider revoking that client in Amber.

**Discovered by.** Cross-project signer audit, 2026-09-19.

## 7. nsite publishing: the trap set, measured

**Read first:** [`patterns/nsite-publishing.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/nsite-publishing.md)
is the implementation guide. This section holds the machine-state findings that
made five separate agents each rediscover the same things.

### 7.1 Two `nak` binaries, and the one on `PATH` had no `nsite` command — RESOLVED

`nak` has an `nsite` subcommand — `nak nsite upload|download`, a complete NIP-5A
deploy in one call. Until 2026-09-21 you could not reach it:

| Binary | Version | `nsite`? | On `PATH`? |
|---|---|---|---|
| `~/.local/bin/nak` | **v0.18.3** (Feb 2026) | **no** | **yes, won** |
| `~/go/bin/nak` | build from source | **yes** | shadowed |

So `nak nsite` looked like it did not exist, and four projects
(Idle StarFighter → Nostr Workshop Board → Signer Login Tester → Cerebrate)
each hand-rolled a ~130-line Python deploy script instead.

**Resolved.** `nak` is now **v0.20.7** at `~/go/bin/nak` and is the only one on
`PATH`; the old binary is parked at `~/.local/bin/nak.v0.18.3.bak`. Two traps
survive for whoever reinstalls it:

- **`nak --version` prints `nak version debug`** regardless of the version. The
  release ldflags are not set by `go install`, so the version string is a lie.
  The truth is `go version -m $(command -v nak) | grep fiatjaf/nak`.
- **The vanity import path is broken.** `go install fiatjaf.com/nak@latest`
  fails — `fiatjaf.com/nak?go-get=1` advertises `git@github.com:fiatjaf/nak.git`,
  an scp-style SSH URL the Go tool rejects ("first path segment in URL cannot
  contain colon"). Install it as **`go install github.com/fiatjaf/nak@latest`**.
  (`fiatjaf.com/nostr`, the library, resolves fine — only `nak` is misconfigured.)

### 7.2 `blossom.primal.net` rewrites every `text/*` blob to `text/plain`

The nsite gateway serves each file with the `Content-Type` the Blossom server
recorded **at upload, keyed by sha256** — so a wrong type is only fixable by
changing the file's bytes. Measured 2026-09-21, same three files, same
uploader, fresh key:

| Server | `.css` | `.js` | `.wasm` | GET |
|---|---|---|---|---|
| `https://nostr.download` | `text/css; charset=utf-8` | `text/javascript; charset=utf-8` | `application/wasm` | 200 |
| `https://blossom.primal.net` | **`text/plain`** | **`text/plain`** | `application/wasm` | 302 → 200 |
| `https://blossom.band` | upload **400** | upload **400** | upload **415** | 404 |
| `https://azzamo.media` | upload **415** | upload **415** | upload **415** | — |
| `http://localhost:8081` | `text/css; charset=utf-8` | `text/javascript; charset=utf-8` | `application/wasm` | 200 |

**This corrects the folklore.** `nak blossom upload` was never the problem — it
sends the correct type from the file extension (Go's `mime.TypeByExtension`),
and both the local server and `nostr.download` honour it. `blossom.primal.net`
is not "unreliable" and is not content-sniffing: it deterministically flattens
`text/*` to `text/plain` and passes `application/*` through. A browser refuses
a stylesheet served as `text/plain` and refuses to register a service worker
served as `text/plain` — the page renders, completely unstyled, looking fine.

`blossom.band` now rejects fresh-key uploads outright (400 on text, 415 on
wasm), not just `.wasm` as previously recorded. Don't list it.
`cdn.satellite.earth` is still 500 on everything. `inner.sebastix.social` is
reachable again (its API is at the **root** path; `…/blossom/` is an HTML
catch-all that 200s for anything and false-positives every dedup check).

**`azzamo.media` is media-only and cannot host an nsite.** It is the owner's
server (first in their kind 10063, published by Amethyst) and it accepts
uploads from *any* key — the Blossom side is not gated the way
`relay.azzamo.net` is (`payment_required` + `restricted_writes`). But it
refuses by MIME with HTTP 415: `text/html` and `text/javascript` "not allowed
for security reasons"; `text/css`, `text/plain` and `application/wasm` "not
supported". Only `image/*` and friends get in, faithfully typed. Right server
for media and for the side channel's file drops; never a `server` tag on a
15128. Canonical list: `configs/blossom.json`.

**Live defect in the owner's 10063**: its second entry is
`cdn.satellite.earth/`, which 500s. Their phone's media has a dead fallback.
Fix from Amethyst — never with `nsyte --publish-server-list`, which would
replace the whole list.

**Workaround.** `nostr.download` first in the `server` list and abort if it
fails; primal second, as an availability mirror only. Inlining CSS/JS remains
good belt-and-braces, but it is a mitigation for server #2, not for `nak`.

### 7.3 Four divergent copies of the same deploy script

Each project copied the last one and drifted. As of 2026-09-21:

| | Idle StarFighter | Workshop Board | Signer Login Tester | Cerebrate |
|---|---|---|---|---|
| description tag | `description` | `description` | `description` | **`summary`** (not a spec tag) |
| non-spec `relay` tags | yes | yes | yes | no |
| aggregate `x` tag | no | no | no | no |
| `/404.html` | no | no | no | no |
| SPA route aliases | no | no | no | no |
| gateway verification | no | no | no | no |
| abort rule | all servers failed | all servers failed | all servers failed | first server failed |

All four sites are live, because the bugs are latent: primal is listed second,
so the wrong MIME only bites when `nostr.download` misses a blob; and none of
the sites has deep links. **Fixed at source:** `bin/nsite-deploy.py` is now the
shared implementation and does all six columns correctly. Point new projects at
it instead of copying a fifth time. The four existing scripts were deliberately
left alone — retrofitting them means redeploying four live sites.

### 7.4 A root site event with a `d` tag reports partial success and 404s

`relay.nsite.lol` rejects it outright (`"Root site events must not include a
'd' tag"`); every other relay accepts it happily. So a broken deploy prints
"2/4 relays accepted" and looks survivable, while the gateway can never find
the site. A `d` tag belongs to kind 35128. Found 2026-09-21 publishing
Cerebrate.

More generally: **counting relay OKs is not a test.** `bin/nsite-deploy.py`
fetches the gateway and checks the served content types before it claims
success.

**Discovered by.** nsite cross-project audit, 2026-09-21.

## How to log a new issue here

Format: a section like the above with **Symptom / Cause / Affects /
Workaround / Discovered by / Upstream**. Workspace-relevant only —
issues that belong upstream and don't affect this stack go straight to
the upstream tracker, not here.
