Markdown source for agents: https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/design/known-issues.md

Nostr Agent Onboarding · start here · index · 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/ 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:

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:

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:

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: 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 (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 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:

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):

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

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.

# 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:

# 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 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 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:

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.