Nostr Agent Onboarding · start here · index · source:
nostr-dev/docs/known-issues.md· snapshot 2026-10-10Paths such as
~/Documents/…,~/Production Environment/…,repos/…and services onlocalhostrefer 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:
- Mirror to a supported forge. Push the source to
github.com/<you>/<repo>(or Codeberg, GitLab, Gitea, Forgejo) with the samezapstore.yamlat 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. - Vertex reputation. Accumulate Nostr social activity under the publisher pubkey until their reputation system admits it. Passive; not directly actionable.
- 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 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_atorsig), so nak fills it in; --force-signis 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:
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:
nak --versionprintsnak version debugregardless of the version. The release ldflags are not set bygo install, so the version string is a lie. The truth isgo version -m $(command -v nak) | grep fiatjaf/nak.- The vanity import path is broken.
go install fiatjaf.com/nak@latestfails —fiatjaf.com/nak?go-get=1advertisesgit@github.com:fiatjaf/nak.git, an scp-style SSH URL the Go tool rejects ("first path segment in URL cannot contain colon"). Install it asgo install github.com/fiatjaf/nak@latest. (fiatjaf.com/nostr, the library, resolves fine — onlynakis 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.