Conversation
Full design for the v0.8 client-library outbox work: four-layer model (app defaults, shared growing pool, per-session config, per-target directory), role taxonomy, routing table, hydration, query semantics, streaming boundary, AUTH policy, reusable importable API surface, and a phased plan (0-F). Source of truth for the #56/#77 implementation. Co-Authored-By: Claude Opus 4.8 <[email protected]>
GetUserRelays waited on sub.Done -- which EOSE never closes -- so every mailbox fetch burned its full 5s timeout, and the login cache-fill ran the mailbox then the metadata query serially (~10s worst case). The /api/v1/events/query handler had the same sub.Done bug and always ran to its 8s timeout. - Add collectLatestReplaceable(): one EOSE-aware drain loop shared by GetUserProfile and GetUserRelays (removes ~50 lines of duplicated logic). - GetUserRelays / GetUserProfile now resolve the moment a relay sends EOSE. - QueryEventsHandler counts per-relay EOSE and finishes when all relays have sent their stored events. - Run the mailbox + profile fetches concurrently in the cache-fill path. - Make subscription IDs collision-proof with an atomic counter so the two now-concurrent REQs cannot cross-wire in the message router. - Add native tests for the collector, including an EOSE-promptness regression guard. Phase 0 of the outbox relay pool work. Refs #56, #77 Co-Authored-By: Claude Opus 4.8 <[email protected]>
Introduce the connection substrate the outbox pool needs: one shared pool that can hold many users' relays at once, dialed on demand and reclaimed when idle, instead of the flat "connect a set, query all" model. - RelayConnection gains lease counting (leases, idleAt); RelayPool gains pool-level pinned/backoff/failCount/dialing maps and a dial semaphore. - Acquire/Release: connect-on-demand ref-counting, with an underflow guard so double-release or unknown urls are safe no-ops. - Dialing runs off the pool lock with single-flight (concurrent acquires of one url share a dial) and bounded concurrency. - Exponential, capped dial backoff so a dead relay is not re-hammered. - Soft-cap LRU eviction on Acquire plus a ctx-bound idle sweeper that only touches unpinned, lease-free, idle connections. - Pin() keeps index/seed relays from being evicted. - core.Config gains dial/idle/backoff knobs (defaults only; MaxConnections 10 -> 256). Two unexported test seams make Acquire testable without sockets. Nothing calls these yet -- the pool is dormant, so login and queries are unchanged. Wiring follows in the next slice. Phase A (slice 1) of the outbox relay pool. Refs #56 Co-Authored-By: Claude Opus 4.8 <[email protected]>
) Activate the Phase A pool primitives and remove the destructive single-user teardown. Logging in no longer Close()s the entire relay pool to reconnect to one user's relays -- the shared pool now grows additively and reclaims idle connections on its own. - Subscribe takes a lease on each target relay (connect-on-demand) and releases it on Close, so an active subscription's connections are never idle-evicted, and idle ones are reclaimed afterward. AddRelay/RemoveRelay kept lease-consistent. - ReplaceRelayConnections / SwitchToUserRelays / SwitchToIndexRelays are now additive: they release the previous session's leases and acquire the new set, holding one lease per relay for the session. Index relays untouched, so auth.go's login path picks this up unchanged. - The manager pins the index/seed relays so the sweeper never evicts them, and init.go starts the ctx-bound idle-eviction sweeper (#93). - White-box test asserts the switch is additive: index survives a login, the previous user's relays are released, the new user's are leased. Phase A (slice 2) of the outbox relay pool. Refs #56 Co-Authored-By: Claude Opus 4.8 <[email protected]>
Pin down the relay-list event kinds for the outbox role model: - DMInbox = NIP-17 kind 10050. - PrivateInbox dropped (no such standard); private relays = NIP-37 kind 10013 (NIP-44 encrypted), mapped to PrivateHome, owner-only. - Search/Blocked/Favorite are NIP-51 kinds 10007/10006/10012 -- real user-published lists, so self-only event-derived, not local-only. - Directory persistence: no (cold-resolve from indexers + TTL cache). Refines the role taxonomy into per-target / self-only / local-only classes. Refs #56, #77 Co-Authored-By: Claude Opus 4.8 <[email protected]>
Resolve any user's event-derived relay roles (outbox / inbox / DM inbox) from their published kind 10002 (NIP-65) and 10050 (NIP-17) events, cached with a TTL. This is the per-target half of the role model -- routing needs to send each operation to the right relays for the user it concerns. - RelayDirectory: TTL cache keyed by pubkey, single-flight per pubkey, and short negative-caching so a flood of unknown pubkeys can't hammer the indexers and a user who later publishes a list is picked up soon. - Client.ResolveRelays(pubkey) + a resolver that queries the index/seed relays for kinds 10002 and 10050 concurrently and maps them to roles. - parseDMRelays (NIP-17 relay tags); appendUnique for write+both / read+both. - core.Config gains relay_list_ttl / relay_list_neg_ttl (1h / 1m). - Tests: tag parsing, dedup, TTL re-resolution, single-flight, negative TTL. Dormant until routing is wired to it. Phase B (slice 1). Refs #56, #77 Co-Authored-By: Claude Opus 4.8 <[email protected]>
…#56) Wire the per-target directory into the real operations so the outbox model finally takes effect: - Reads (GetUserProfile with no explicit relays) route to the author's outbox relays (index fallback) instead of blasting every connected relay. - Publishes go to the author's outbox PLUS every p-tagged recipient's inbox -- a reply now reaches both my audience and the relays the person I am replying to actually reads. NIP-17 gift wraps (kind 1059) route to the recipient's DM inbox (10050). Previously a publish only hit the author's own write relays, so replies never reached their target. - RouteFetch / RoutePublish use the cached directory (B1), keyed on the event's own author pubkey, so no session state is needed yet. Tests cover reply routing, gift-wrap DM routing, and index fallback. Phase B (slice 2). Refs #56, #77 Co-Authored-By: Claude Opus 4.8 <[email protected]>
The "only write to X / read from Y" session knob is not a routine narrowing of routing -- it pins the client to fixed relays and turns the outbox model OFF (replies stop reaching other users inboxes). Reframe it as an explicit, default-off, discouraged opt-out. The default is always full outbox: when interacting with another user, write to their relays in addition to your own (already how B2 RoutePublish behaves). Refs #56 Co-Authored-By: Claude Opus 4.8 <[email protected]>
The optional fixed-relay override pins every read to a chosen read set and every write to a chosen write set, bypassing outbox routing. It turns the outbox model OFF -- replies no longer reach other users inboxes -- so it is default-off and intended only for users who explicitly want a fixed- or single-relay client. - Client.SetFixedRelays / ClearFixedRelays / FixedRelaysEnabled. - RouteFetch / RoutePublish short-circuit to the pinned read/write set when enabled (an empty pinned set falls back to index relays, never to outbox). - Off by default, so the outbox model (B2) is unchanged unless explicitly enabled. Tests cover the override on/off and the default. Phase B (slice 3a). Refs #56 Co-Authored-By: Claude Opus 4.8 <[email protected]>
…ut (#77) After B2 routed queries to real per-user relays, Subscribe dialed them one at a time (A2), so a slow or dead relay in someone outbox stalled the others behind it. Now: - Subscribe acquires its target relays concurrently (bounded by the pool dial semaphore), so N relays come up in ~one dial time, not the sum. - Acquire uses a shorter per-dial OpenTimeout (4s, configurable) instead of the 10s ConnectionTimeout, so a dead relay fails fast; dial backoff then skips it on subsequent queries. - Close sends CLOSE to and releases only the relays actually acquired. Meets #77 "one dead relay does not add more than its own per-relay timeout". Test asserts concurrent dialing (4x150ms dials finish in ~150ms) and balanced leases across Subscribe/Close. Phase C (slice 1). Refs #77, #56 Co-Authored-By: Claude Opus 4.8 <[email protected]>
Foundation for the dashboard "x / y relays" indicator: surface how many relays
the outbox pool is tracking versus how many are connected.
- RelayPool.Stats() / Client.PoolStats(): {total, connected, pinned, leased}.
- GetCoreClientStatus() now includes the pool counts.
- New GET /api/v1/client/status handler returns them as JSON.
Phase C (slice 2a). Refs #56
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Small header indicator showing how many relays the outbox pool has connected out of the total it is tracking, polled every 10s from the new /api/v1/client/status endpoint. Best-effort and guarded -- if the relay is unreachable it keeps its last value and never breaks the header. Frontend only; needs a Docker/dashboard smoke-test to confirm rendering. Phase C (slice 2b). Refs #56 Co-Authored-By: Claude Opus 4.8 <[email protected]>
…#56) Fixes the brutal dashboard slowdown during a mutelist sync once the outbox pool had grown to dozens of relays. Root cause: GetUserRelays queried GetConnectedRelays() -- the ENTIRE pool -- so every per-author relay-list lookup (the mutelist refresh does many in parallel) fanned a REQ out to all ~80 connected relays. And B2 made GetUserProfile synchronously resolve+fetch from each author outbox on every profile view. Intended flow: indexer relays resolve mailboxes + profile; a user own mailboxes serve their other data. - GetUserRelays resolves through the directory (index-relay query, TTL-cached, single-flight), reconstructing the read/write/both split. Repeated authors become cache hits; fan-out drops from ~80 relays to 5. - GetUserProfile uses RouteMetadata: index/profile-indexer relays plus the author outbox only when ALREADY cached -- never a blocking resolve. - RelayDirectory.Cached() for non-blocking peeks; Lookup() stamps FetchedAt itself so a resolver cannot silently disable caching. Refs #56, #77 Co-Authored-By: Claude Opus 4.8 <[email protected]>
…known (#56) The relay indicator denominator is now "known" relays -- everything the client is aware of -- not just live connections. This matches how the outbox pool is meant to feel: known climbs fast as the indexers seed mailbox lists, while connections grow on demand. - RelayDirectory.KnownRelays() + Client.PoolStats().Known = configured index relays + every resolved outbox/inbox/DM relay + pooled connections. - WarmRelays(): GetUserProfile now background-resolves the viewed user's mailboxes into the directory (and thus "known") without blocking and without dialing -- resolution only queries the already-connected indexers, so it populates "known" but does not re-trigger the dial storm. - /api/v1/client/status gains pool_known. - Header indicator: connected/known with a relay icon, moved between the login button and the theme swapper; click toggles a pool breakdown panel. Full relay manager is a later slice. Refs #56, #77 Co-Authored-By: Claude Opus 4.8 <[email protected]>
…y URLs (#56) So the known-relays set starts broad and stays clean. Seeding: at startup, after the index relays connect, bulk-fetch recent relay- list events (NIP-65 kind 10002 + NIP-17 kind 10050, limit 500) from the indexers and fold every advertised relay into the directory. "Known" now starts in the hundreds instead of climbing only as the user browses; connections still grow on demand (seeding records relays, it does not dial them). - FetchEvents: bulk multi-relay collector (dedupe by id, EOSE/limit/timeout). - RelayDirectory.Store: merge-aware insert (10002 + 10050 for one author merge). - SeedKnownRelays wired into InitializeCoreClient after index relays connect. Normalization: every relay URL entering the directory or pool goes through normalizeRelayURL -- repairs scheme (bare host->wss, http(s)->ws(s), "ws:/" typo), lowercases host, strips trailing slash; ws/wss kept distinct. Applied in parseMailboxEvent, parseDMRelays, Acquire, Connect, and the index seed list, so near-duplicates (wss://X, wss://X/, X) collapse to one. Refs #56, #77 Co-Authored-By: Claude Opus 4.8 <[email protected]>
…d events (#90) Backend for editing your own profile from the dashboard: - core.AssembleProfileEvent: merge field edits over an existing kind-0, preserving every other content field AND tag; dual-write each edited field to both the content JSON (interoperable source of truth) and a [key,value] tag. Errors rather than clobbering if existing content is not valid JSON. - POST /api/v1/user/profile/build: session-gated merge -> returns the UNSIGNED event for the browser to sign. - POST /api/v1/events/publish: accepts a client-signed event (NIP-07/46/Amber), verifies pubkey==session + signature, routes via the outbox (RoutePublish), broadcasts, returns per-relay results. (The existing /api/v1/publish only signs server-side from a private key.) Tests cover preservation of unknown fields/tags, tag upsert (replace not duplicate), new-profile, and invalid-content rejection. Frontend (edit UI + toast) follows. Refs #90, #56 Co-Authored-By: Claude Opus 4.8 <[email protected]>
…90) Frontend for in-place profile editing: - Reveals an Edit button only when the logged-in user views their own profile (session pubkey == profile pubkey). - Edit panel with the common kind-0 fields (name, display_name, about, picture, banner, nip05, lud16, website); lud06 added as a lightning-display fallback. - Save sends ONLY changed fields to /api/v1/user/profile/build, signs the returned unsigned event with the user signer (window.grainSigner), and publishes via /api/v1/events/publish (outbox-routed). Unknown fields/tags are preserved server-side. - A publish toast lists the relays it went to with per-relay send status. Per-relay NIP-20 OK accept/reject is the next slice. Refs #90, #56, #77 Co-Authored-By: Claude Opus 4.8 <[email protected]>
… kind-0 (#90) - Publish now collects each relay OK verdict: BroadcastResult gains Accepted/Reason, the message router routes OK by event id (resolving the long-standing TODO in the OK handler), and BroadcastEvent waits briefly for responses from the relays it sent to. - The profile toast shows per-relay accepted / rejected+reason instead of just send status, and the save hydrates the UI in place once at least one relay accepts -- no page reload. - AssembleProfileEvent strips any "client" tag (e.g. a stale client:amethyst from another client); kind-0 should not advertise the writing app by default. Tests: OK routing, OK collection, client-tag strip. Refs #90, #56 Co-Authored-By: Claude Opus 4.8 <[email protected]>
An Advanced mode (own profile) giving full control before signing: - Content shown as individual key/value rows (every content key, incl. custom ones), editable / add / remove; re-serialized into the event content. - Tags as rows you can edit, add an element to, remove, and drag to reorder via a grip handle (HTML5 drag-and-drop). Order is preserved as published. - kind (0) and pubkey are read-only; created_at is stamped at sign time. - Assembles the event directly (full control, no server merge), then reuses the shared sign -> outbox-publish -> OK toast -> in-place hydrate path the simple editor now also uses. Refs #90, #56 Co-Authored-By: Claude Opus 4.8 <[email protected]>
- RoutePublish now sends metadata/relay-list kinds (0, 10002, 10050) to the index relays in addition to the author outbox -- a profile update has to reach the indexers where clients fetch it for arbitrary users, not just the author own write relays. (Test added; kind-1 notes unaffected.) - Publish toast rebuilt: small, bottom-right, spinner while sending, then an "accepted by x/N relays" header; click to expand per-relay accept / reject+reason / send-failure detail; dismiss with the x. Shared by the simple and advanced editors. Drag-reorder animation in the advanced editor is deferred to the profile restyle. Refs #90, #56 Co-Authored-By: Claude Opus 4.8 <[email protected]>
Redesign per the approved mockup: - Cleaner card layout: banner, centered rounded-square avatar, identity, about, a responsive contact grid, social links -- all on grain design tokens so it adapts across the 7 themes. - In-place editing: clicking Edit swaps the display fields for inputs RIGHT WHERE THEY SIT (name, about, nip-05, lightning, website, plus picture/banner URL + display name) instead of a separate panel. A .pf-view / .pf-edit toggle drives it; the avatar gets a change-photo button; Save/Cancel replace Edit. - Reuses the build -> sign -> outbox-publish -> OK toast -> in-place hydrate path, plus the Advanced editor and view-event-json. The smooth drag-reorder animation in the Advanced editor is the next follow-up. Refs #74, #90, #56 Co-Authored-By: Claude Opus 4.8 <[email protected]>
The restyle moved the `hidden` class onto #profile-banner-img directly and dropped the old #profile-banner wrapper, but updateProfileImages still un-hid the (now-nonexistent) wrapper -- so the banner loaded but stayed hidden behind the gradient placeholder. Reveal the image element itself. Refs #74 Co-Authored-By: Claude Opus 4.8 <[email protected]>
The Advanced panel renders below the profile card; opening it now scrolls it into view so the inputs are visible immediately. Refs #90 Co-Authored-By: Claude Opus 4.8 <[email protected]>
The toast was created AFTER the signer check, so a save that failed at the signer step (no signer / not restored) threw before any toast appeared and the user saw nothing. Create the toast up front and run the signer + publish inside its try, surfacing any error (including "no signer") in the toast itself. Adds a toast.status() for the in-progress states. Refs #90 Co-Authored-By: Claude Opus 4.8 <[email protected]>
Backend for the media-server upload feature: resolve a user's published media-server lists and expose grain's quick-add suggestions. - client/core/media.go: MediaServers type + MediaDirectory (TTL cache, single-flight), resolves kinds 10063/10096 from the index relays plus the user's cached outbox (these lists often live only on the user's own write relays, not the metadata indexers). server-tag parse + HTTP(S) URL normalize. - client/core/media_servers.go: static capability table (cost / retention / BUD-04 mirror support) + curated suggestions, Blossom-preferred with NIP-96 as the legacy fallback. Includes the Happy Tavern offer. - client/api: GET /api/v1/user/media-servers (resolved + annotated) and GET /api/v1/media-servers/suggested. Native-tested (CGO_ENABLED=0); resolver + cache mirror the relay directory. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Settings -> Media servers: the user's own lists (or an empty state) on top, grain's recommended quick-adds below in the same row style, add-by-URL, and a collapsed legacy NIP-96 section that nudges toward Blossom. Save & sign rebuilds each changed list and publishes it through the outbox. Backend: - core.AssembleMediaServerEvent rewrites the server tags in order while preserving content + every non-server tag (same "don't drop data" rule as the kind-0 editor); core.FetchMediaServerList pulls the latest list (indexers + cached outbox) so the rewrite keeps any extra tags. - POST /api/v1/user/media-servers/build (session-gated, returns unsigned). - PublishSignedHandler invalidates the media cache on a 10063/10096 publish so the new list shows immediately. - Recommended order: blossom.band, 0x0.happytavern.co, blossom.happytavern.co, blossom.primal.net. Frontend: - www/static/js/nostr-publish.js: shared sign -> outbox-publish -> per-relay OK toast (profile editor, this section, and the coming uploader share one toast). - www/static/js/media-servers.js: render your-list + recommended rows, add/remove/drag-reorder, Save & sign (build -> grainSigner -> publish). - settings.html: the Media servers section + script includes. Native-tested (CGO_ENABLED=0). Frontend pending Docker smoke-test. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…rride (#98) Slice 1 backend for the full relay manager, reusing the media build flow: - core.AssembleRelayListEvent builds an unsigned relay-list event for kind 10002 (NIP-65 r-tags with read/write markers; both is unmarked) and kinds 10050/10006/10007/10012 (relay tags), preserving content + non-relay tags. - Client.FetchRelayList (indexers + cached outbox) + Client.InvalidateUserRelays. - POST /api/v1/user/relay-list/build (session-gated, returns unsigned). - POST /api/v1/client/fixed-relays sets/clears the outbox override (proxy) via SetFixedRelays / ClearFixedRelays. - PublishSignedHandler invalidates the directory cache on a 10002/10050 publish. Native-tested (CGO_ENABLED=0). Co-Authored-By: Claude Opus 4.8 <[email protected]>
Settings -> Media servers polish from live testing: - Separate "Save & sign" per list (Blossom 10063 / NIP-96 10096) instead of one grouped button, each with its own status line. - Server names are hyperlinks (open the server in a new tab); the display still strips the scheme, and http:// / localhost are preserved as typed. - Fix the inherited center + airy rows: left-align the section, compact 3-line rows (URL -> chips -> info), and an outlined neutral chip so "Permanent" reads as a tag alongside the filled Free/Paid/Ephemeral chips. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…no-cache (#83) The publish toast now streams: each relay's result lands the moment it resolves and the x/N pill counts up live, expandable to per-relay accept/reject/ no-response, and it persists until dismissed. - core.broadcastEventStream + Client.PublishEventStream: a channel that emits each BroadcastResult as it resolves (send-fail now, accept/reject on the OK, no-response at the 5s deadline). - POST /api/v1/events/publish/stream: NDJSON (start{relays} -> result per relay -> done), Flusher-based; same cache invalidation as the non-stream handler. - nostr-publish.js: rewritten shared toast (incremental start/addResult/done, live count-up, expandable, persistent, stacked). - server/startup.go: Cache-Control no-cache on /static, /style, /views so a rebuilt binary's assets load fresh without a manual hard-refresh. Native-tested (CGO_ENABLED=0); verified live in Docker. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Migrate the profile editor onto grainPublish.signAndPublish so it gets the same live count-up streaming toast as the media/relay flows, and load nostr-publish.js on the profile page. Removes ~85 lines of duplicated toast (makePublishToast / relayLine) from profile-page.js; hydrate-in-place on accept is preserved via the onAccepted callback. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add an optional Encrypter capability (NIP44Encrypt / NIP44Decrypt) a Signer may also implement, mirroring the nip44 methods a NIP-07/NIP-46 browser signer exposes — so the same calling code works whether the key lives in the browser or in a downstream Go EventSigner. EventSigner implements it over the v2 primitives; decrypt version-dispatches on the payload's version byte (v3 slots into the switch later) and encrypt defaults to v2. Covered by a round-trip test: self-encryption (NIP-51 lists), two-party, and wrong-key rejection. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…#100) Implements the nostr-land NIP-44 v3 draft: per-message HKDF salted with 'nip44-v3\x00' || nonce (so ChaCha20 runs with a fixed zero nonce), a u32 length prefix for binary plaintext, and the event kind + scope bound into the MAC's authenticated data for cross-context replay protection. Validated against the proposal's official test-vectors.json — padded lengths (176), PRK + both message keys, encrypt/decrypt, long messages, decrypt-only, and the invalid-decryption suite (incl. non-UTF-8 scope rejection). Exposed as an optional EncrypterV3 capability on the Signer (EventSigner implements it); the generic v2 decrypt path explicitly refuses v3 payloads since it carries no kind/scope context. Encryption elsewhere still defaults to v2 for interop — v3 is opt-in until it has deployed peers. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…101) The pool now records each relay's NIP-42 AUTH challenge as it arrives in the connection read loop (MessageRouter.authStates, session-scoped). A new challenge resets that relay's authed flag so the manager re-prompts; an identical one leaves it. Exposes the surface the relay manager + browser signer will use: AuthRequests (relays that challenged us, with authed status), AuthChallenge, SendAuth (relays a browser-signed kind-22242 event on the same connection and marks the relay authed), and RemoveAuth (revoke for the session). The send path reuses the existing RelayPool.SendMessage. API + UI slices next. Co-Authored-By: Claude Opus 4.8 <[email protected]>
GET /api/v1/client/auth-requests lists relays that have challenged us this session (with authed status); POST /api/v1/client/auth relays a browser-signed kind-22242 event to a relay; POST /api/v1/client/auth/remove revokes a relay's session AUTH. The browser builds + signs the event (it holds the key); grain only forwards it on the challenged connection, gating each route on the session. UI slice next. Co-Authored-By: Claude Opus 4.8 <[email protected]>
The old stored-inert "Trusted" app-relay role becomes a live "Relays requesting AUTH · NIP-42" list: relays that challenged grain this session, each with an Authenticate button (signs a kind-22242 event with the browser signer, POSTs it for grain to relay on the challenged connection) and a x to forget the session trust. Answered relays show "trusted · session"; the list polls on the existing 5s timer since challenges arrive during normal pool activity. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…99/#80 slice 1) grain now always strips any foreign `client` tag from events it builds (profile kind-0, media lists 10063/10096, relay lists 10002/10050/10006/10007/10012) and, when enabled, stamps its own ["client", name]. On by default. - config: ClientConfig gains json tags (#80) + a ClientTag {enabled, name} block; ValidateAndApplyDefaults treats an unset block (empty name) as "on, grain". - core: ApplyClientTag(event, enabled, name) — strip foreign, conditional add — unit-tested. AssembleProfileEvent no longer strips `client` itself; the build handler owns the policy so the library assembler stays policy-free. - api: the three build handlers resolve the configured default vs a per-build override (client_tag in the build request — the user slider, slice 2) and apply. Next: user settings slider (slice 2); admin Client section + grain_updateclient (slice 3). Co-Authored-By: Claude Opus 4.8 <[email protected]>
Settings → "Client tag" toggle lets a logged-in user opt out of grain's client:grain stamp on their own events. Persisted client-side (localStorage via window.grainClientTag, default on) and sent as `client_tag` with each build request — profile, relay-list, and media-server builds — overriding the server default. grain still strips foreign client tags regardless. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…e 3) Adds the dashboard "Client" section so the built-in Nostr client's settings are editable: index_relays (ws_url list widget), connection/read/write timeouts, max connections, retry attempts/delay, keep-alive, user agent — plus the #99 client-tag knobs (default on/off + name). Round-trips through a new grain_updateclient NIP-86 method (config.UpdateClientConfig, same atomic-write pipeline; validates index relays as ws/wss, fills a blank tag name with "grain"). Wired into the admin section list + the admin.html dispatch chain. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…80) `swag init` was aborting on `@Success {object} nostr.Event`: server/types is declared `package relay`, and swag resolves by real package name (not import alias), so it couldn't find the type — leaving the committed spec stranded at its May-20 state and breaking the release build's spec the same way. Use `relay.Event` in the three build handlers' annotations. Also register grain_updateclient in the NIP-86 supported_methods list + the swagger @description (it was wired but unadvertised/undocumented), and regenerate the spec. It now includes this cycle's endpoints: known-relays, relay-info, relays/ping, app-relays, the NIP-42 auth routes, and the build endpoints' client_tag field. Co-Authored-By: Claude Opus 4.8 <[email protected]>
/api/docs rendered blank in dev: the docs shell loads swagger-ui-dist from /static/swagger/, but Dockerfile.dev never fetched those assets (only release.yml + tests/docker did) — so the UI's JS/CSS 404'd and nothing bootstrapped — and it overwrote docs/openapi/swagger.json with an empty stub. The dev image now pulls the pinned swagger-ui-dist (matching the release/test images) and keeps a real spec when the build context carries one (`make generate` output — gitignored but copied in), stubbing only when absent. Verified: the three UI assets serve 200 and /api/docs/openapi.json is the full spec. Co-Authored-By: Claude Opus 4.8 <[email protected]>
The Swagger Authorize button was a NIP-98 apiKey field — asking you to paste a base64 kind-27235 event by hand, which only authorizes one request (NIP-98 binds the signature to the URL + payload, so a pasted token is stale immediately). The one protected operation (POST / — the NIP-86 RPC) now signs on the fly: preauthorizeApiKey arms the lock and a requestInterceptor mints the Authorization header via window.grainSigner per request (same kind-27235 u/method/payload flow as nip86-submit.js). No token to paste — Try it out uses your signer, like the rest of the app. Session-cookie endpoints already ride the browser session. Updated the NostrAuth description to match. Co-Authored-By: Claude Opus 4.8 <[email protected]>
With "Try it out" auto-signing via the connected signer, the manual NIP-98 Authorize modal is pointless — and swagger's default dialog isn't themed, so it read badly against grain's dark palette. Hide the Authorize bar + auth dialog (.scheme-container / .dialog-ux); the per-operation lock icons stay as inert "needs owner auth" indicators. Also version-stamp the swagger CSS links so the service worker serves the override fresh after this change. Co-Authored-By: Claude Opus 4.8 <[email protected]>
www/static/js/blossom-upload.js uploads a File to the user's chosen media server — Blossom (BUD-01/02 kind-24242 PUT) or NIP-96 (kind-27235 NIP-98 POST) — signing the auth event with window.grainSigner. Every upload pops a modal: pick the primary server (default), a per-upload mirror-to-others checkbox, an ephemeral-server warning, and a "no media server set up" prompt when the user has none. XHR upload progress. Public API: grainUpload.pick(onUrl, opts) / grainUpload.open(file, onUrl, opts). Wiring into profile + admin fields next. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Load blossom-upload.js globally and add declarative [data-upload-target] wiring to the module — a button writes the uploaded URL into its target field (firing input/change so the profile editor + admin pending-tracker pick it up). Buttons added: - Profile editor: Picture URL + Banner URL (image/*). - Admin Ops: Icon + Banner (image/*), and Privacy/Terms/Posting policy URLs (pdf/md/txt). Click → pre-upload modal → URL fills the field → Save/Apply ships it via the existing changerelay* / build flows. Co-Authored-By: Claude Opus 4.8 <[email protected]>
The upload modal showed "no media server set up" when the resolve transiently
returned empty (a negative result that then sticks in cache until TTL), even
though the user has servers. The "no servers" prompt now offers a Re-check that
re-resolves with a cache-bust (GET /api/v1/user/media-servers?refresh=1 →
InvalidateMediaServers first), and distinguishes a failed fetch ("make sure
you're signed in") from a genuine empty list.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
… top + default-open The old Operations section bundled live stats, the relay-identity fields, and maintenance. Split the identity fields (name/description/icon/banner/contact/ policies + their upload buttons + the per-field changerelay* save) into a new "Relay information" section. Operations now holds just live stats + cache refresh, sits first in the section list with Relay information right under it, and is expanded by default on page load. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…connect - Section order: Operations, Relay information, Server, Rate limit, Resource limits, Whitelist, Blacklist, then the rest — via orderSections() so the definition block stays grouped by concern while display order is curated. - Relay information icon 📛 → 🪪. - Signer reconnect after claim: surface the 🔑 Reconnect signer pill proactively on load (after a silent restore attempt) instead of only after an action fails; reword the error to name the actual button; and re-pull Operations stats once the signer reconnects so the operator needn't hit ↻ themselves. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Root of the "no media servers" false negative: the resolve added the user's outbox relays (where kind-10063/10096 are usually published) only when the relay list was already cached — a non-blocking peek. On a cold or expired directory it queried the index relays alone, missed the list, and cached a *negative*, so a user who has servers saw "none set up" (Settings still showed a stale JS copy from when it last resolved). Resolve the relay list blocking instead — in both the resolve path and the build path (FetchMediaServerList) — so the outbox is always queried. Co-Authored-By: Claude Opus 4.8 <[email protected]>
#83) - Card padding p-5 → p-6 so the buttons/content aren't tight against the border. - Mirror: the checkbox now reveals the *other* servers as individual checkboxes (default all on) with a Select all toggle, so you can mirror to a chosen subset instead of all-or-nothing. The list re-derives when the primary changes. - Image uploads show a thumbnail preview above the filename + size.
Dockerfile.dev emitted the local Tailwind stylesheet as /style/tailwind.min.css with no ?v= — but the service worker caches /style/*, so the dev container served a STALE CSS across rebuilds. Any class added since the first cached fetch (the upload modal's padding, the mirror list, the relay-manager card, etc.) was missing until a manual hard-refresh. tests/docker + release.yml already version-stamp this link; match them so each rebuild's assetVersion busts the SW cache. Co-Authored-By: Claude Opus 4.8 <[email protected]>
… new users A brand-new identity has no kind-0, no relay list, no media list: - Profile page no longer errors when there's no profile — it shows an empty profile the owner can edit to create one (checkOwnProfile reveals Edit; AssembleProfileEvent already builds from nothing), and drops the owner straight into the editor when the content is empty. Only a real 404 triggers this; other fetch failures still error, so a transient failure can't lead to overwriting an existing profile. - A persistent "set up your relays" header banner for a logged-in user with no NIP-65 list, linking to the relay settings. Hidden once a list exists / logged out.
…section (#83) The no-media-server prompt's "Open settings" was a plain /settings link landing at the top of the page. It now navigates (HTMX swap, mirroring openRelaySettings) and scrolls to #media-servers-section via window.__grainScrollTarget, so the operator lands exactly where they add a server. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…dable NIPs; auth says "Required"
Homepage feed (feed.js):
- Render the author's display name next to the avatar (short hex until the
profile resolves); name + avatar both link to the profile.
- Sort newest-first deterministically by created_at — live events land at the
top, lazy-loaded older events at the bottom, regardless of arrival order
(the old prepend-on-arrival reversed the stored backfill).
- Keep the list short: backfill one small page (15) and lazy-load older events
on scroll-to-bottom via a `{until,limit}` REQ; trim only while viewing the
newest so reading older rows never yanks them away.
Relay dashboard (dashboard.js):
- "+N more" on the supported-NIPs list now expands the rest in place instead
of being a dead label.
- Authentication status reads "Required / Not required" (it gates AUTH), not
the misleading "Enabled / Disabled".
Co-Authored-By: Claude Opus 4.8 <[email protected]>
…mpile-checked examples (#101) Extend the client-library guide from the "essentials" surface to the full, feature-complete 0.8.0 surface, and refresh the stale status callout (NIP-44 and NIP-42 both landed; the remaining publish-path ctx + optional seams are 1.0 polish, not new surface). New sections, each with a usage example: - Relay lists & mailboxes — FetchUserRelayLists, ParseNIP65Entries, AssembleRelayListEvent; the NIP-51/37 lists + encrypted-content pass-through. - Media servers (Blossom + NIP-96) — ResolveMediaServers/HasAny, assemble + suggestions; note the signed upload stays client-side. - Encryption (NIP-44) — EventSigner v2 (default) + v3 (draft) methods. - AUTH (NIP-42) — AuthRequests/SendAuth/RemoveAuth; the session/"trusted" model. - Known-relays browser — KnownRelays, FetchRelayInfo (NIP-11), Ping(s). - Client tag (NIP-89) — ApplyClientTag. - The HTTP API — endpoint groups + the OpenAPI spec as the authoritative ref. Plus a contents/TOC and an expanded API-reference section. example_test.go: add compile-checked examples for media, relay-list read/write, NIP-44 encryption, NIP-42 AUTH, the known-relays browser, and the client tag, so the new surface can't drift without breaking `go test ./...`. Co-Authored-By: Claude Opus 4.8 <[email protected]>
#101) The read/fetch path already took a ctx (E4); the publish path carried only its own internal deadlines. Add ctx as the leading parameter across the publish surface, for API consistency and caller-set deadlines/cancellation: - BroadcastEvent, broadcastEventStream / PublishEventStream, broadcastToSingleRelay, BroadcastToUserRelays, BroadcastWithRetry, PublishEvent (pkg + Client method), PublishEventWithRetry (pkg + Client method) - UserContext.Publish / SignAndPublish / Reply ctx is honoured where it matters: the NIP-20 OK-collection select (collectOKResponses + the streaming variant), the retry back-off sleep, and the per-relay connect timeout (bounded by the caller's deadline when sooner, via a new effectiveTimeout helper). Callers pass r.Context() (events + profile handlers); broadcast_test, the compile-checked examples, and the client-library guide are updated to match — the guide's status callout now states both read and publish paths take ctx. Co-Authored-By: Claude Opus 4.8 <[email protected]>
client/core logged exclusively through server/utils/log.ClientCore(), forcing any consumer of the importable library into grain's logging. Introduce a Logger seam: - Logger interface = the four *slog.Logger methods (Debug/Info/Warn/Error), so the standard library logger and grain's own log.ClientCore() satisfy it as-is. - Package-global injectable default (atomic.Value, race-free) initialized to log.ClientCore(), so behaviour is byte-for-byte unchanged until overridden. - SetLogger(Logger) is the primary injection point (the library logs from many package-level functions with no Client in scope); Config.Logger is honoured at NewClient for config-driven setup. - All 146 log.ClientCore() call sites routed through clog(); the now-unused log import dropped from those files (logger.go keeps it for the default). Adds a compile-checked ExampleSetLogger and documents the seam in the guide. Co-Authored-By: Claude Opus 4.8 <[email protected]>
The relay directory cached per-user resolutions in a private in-memory map, forcing that one strategy on every consumer. Introduce a RelayListStore seam: - RelayListStore interface (Get / Set / Delete / Range over pubkey -> *UserRelays); the directory keeps the TTL + single-flight logic, so a store is just a KV map. - Default in-memory implementation (memRelayListStore) — behaviour unchanged. - Inject via Config.RelayListStore (e.g. a database for persistence across restarts or shared across instances). - newRelayDirectory keeps its signature (6 test callers); a new newRelayDirectoryWithStore variant takes the store and is used by NewClient. Adds a compile-checked example (doubling as a reference implementation) and documents the seam in the guide; the status callout now lists all three pluggable seams (Signer / Logger / RelayListStore) as in place. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…nAPI spec swag (the OpenAPI generator run in the CI/release docker build) can't resolve a struct with an anonymous embedded field, so `swag init` failed with "cannot find type definition: core.KnownRelayStatus" and broke the test-image build. Inline RelayLiveStatus's three fields into KnownRelayStatus — identical JSON, but a flat struct swag can parse. The dev image stubs the spec instead of generating it, which is why this only surfaced once CI ran swag on the branch. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…t chicken-and-egg) `swag init` runs `go list` over the module, which fails on main.go's `//go:embed docs/openapi/swagger.json` before swag can generate that very file. With go list failing, swag can't load client/core and aborts on the first referenced type — "cannot find type definition: core.KnownRelayStatus" — which broke the CI test-image build (and would break release the same way). Seed a minimal placeholder so go list succeeds; swag then overwrites it with the real spec. Applied everywhere swag runs: tests/docker/Dockerfile (CI), release.yml (Unix + Windows), and the Makefile `generate` target. Verified by building the test image locally — swag generates the spec and the binary builds. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The accumulated v0.8 work, held off
mainunder the RC hold and opened as a PR so CI validates the whole batch (build + integration tests, including the cgo-dependent suites) before it lands. 85 commits on the v0.8 theme: the importable client library plus the frontend that exposes it as the implementation reference.Highlights
Client library (
client/core) — outbox-model engine (#56)Rolevocabulary + routing,UserContextfacade, multi-relay streaming-fetch primitive.context.Contextthreaded through the read and publish paths.Signer,Logger(SetLogger),RelayListStore— each with compile-checked examples.Protocol
Frontend reference
Docs
client/core/example_test.go.CI
This PR runs the
CI / Integration testsworkflow (docker-compose integration suite + host-side nostrdb build +go test ./...). Merging tomainis the 0.8 RC.