You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
v1 is shipped on afterparty. Do not re-implement plan/sign/publish. Do not treat the numbered spec below as a todo list. This parent stays open so remaining work has a home.
First test (2026-08-16)
#591 merged. That is the first-tester live-smoke record (#583). Classification partial: nos.lol, nostr.mom, and relay.primal.net accepted event 0ef0b100…; relay.damus.io failed WebSocket upgrade (BadStatus) twice. Fixture: docs/contracts/fixtures/nostr-live-smoke/. No secret in git. Not a CI gate. Not a verified target.
Tester review: the testing client said they do not need more testing. Do not ask for another live smoke, another first-person try, or more first-tester evidence unless someone asks. This does not make Nostr a verified target and does not start #584.
Do not redo: plan / sign / publish / DNS / wss:// handshake / NIP-19 npub/naddr / HTML nostr:naddr / the live-smoke fixture. Those are on afterparty via #468#544#551#562#568#573#591.
Product sentence: CLI shipped, not a verified target
First-person try (comment on this issue, 2026-08-16)
How to re-run if someone asks. Tester said no more testing is needed.
Constraints for the next session
Integration line is afterparty (main is frozen). Fresh topic branch from up-to-date afterparty. Do not commit or push to main. Do not force-push.
Secret never on argv, in the profile, in the environment, or in git. publish never sees the secret. Ordinary CI never needs a real key or a public relay.
Nostr is a shipped CLI family, not a verified target. Do not invent a parallel pipeline or treat Pages / Standard.site as “not real.”
The original specification from §1 down is historical. §17 acceptance is checked. NIPs pin 656cecc7.
Publication: add Nostr NIP-23 long-form target
Type: Product/protocol architecture Status: v1 CLI shipped on afterparty (2026-08-16). Remaining work is verified-target extras and §20 future items, not the original three slices. Target: Nostr long-form articles (NIP-23 kind 30023) Research date: 2026-08-14 Living contract:docs/contracts/nostr-publication.md NIPs authority:nostr-protocol/nips at 656cecc7c0a815b6a2b218d3b5d6f078b3f4dbab, committed 2026-08-08
The original specification below is historical. v1 plan/sign/publish, NIP-19 naddr/npub, HTML nostr:naddr, and the first-tester live-smoke fixture (#591 / #583) are on afterparty. This issue stays open as the parent. Remaining card: #584 (verified-target extras, only if authorized). §20 stays parked.
1. Opportunity
Boris should be able to syndicate selected Boris-authored articles to Nostr without weakening the ordinary static-site pipeline:
The canonical Boris website remains canonical. Nostr is another opt-in publication surface, alongside—not instead of—the website, RSS, sitemap, search, IR, RAG, Context Bundle, and llms.txt.
Boris must not become a full Nostr client, relay, social feed, wallet, or messaging application.
2. Current protocol research
Protocol authority for this issue is the official NIPs repository at the exact revision above:
NIP-01: event serialization, BIP-340 signing, addressable-event replacement, client/relay messages, and OK results.
NIP-23: long-form kind 30023, Markdown profile, metadata, update time, and references.
NIP-19: naddr encoding and human-facing key/address forms.
NIP-21 and NIP-27: nostr: URI references in content.
NIP-24: generic r web-reference tags and lowercase t topics.
NIP-09: deletion requests and their non-erasure limitations.
NIP-11, NIP-42, and NIP-65: relay information, relay authentication, and user relay lists.
NIP-07, NIP-46, NIP-49, and NIP-55: browser, remote, encrypted-key, and Android signer boundaries.
NIP-73: external-content identifiers; relevant to, but not automatically selected for, canonical web identity.
Material protocol facts at that revision:
A NIP-01 event ID is SHA-256 over the exact compact JSON serialization [0, pubkey, created_at, kind, tags, content]. It cannot be computed until the public key and created_at are fixed. The signature is BIP-340 Schnorr over secp256k1.
Kind 30023 is addressable. Its address is (kind, author pubkey, d); relays replace versions according to NIP-01's addressable-event rules. Relays may retain different versions and resolve equal timestamps by event ID.
NIP-23 says long-form content should be Markdown. Creating clients must not hard-line-break paragraphs and must not support adding HTML to the Markdown.
NIP-23 defines created_at as the last-update time and defines optional title, image, summary, published_at, and topic t tags. Editable articles should carry d.
Relay publication is ["EVENT", event]; the meaningful response is a matching ["OK", event_id, accepted, message]. Duplicate acceptance is still success. Rejection messages can carry machine-readable prefixes such as auth-required:, restricted:, rate-limited:, and invalid:.
NIP-09 deletion is a signed deletion request, not an erasure guarantee.
NIP-42 authentication is a separate, ephemeral signed event flow and may be required by individual relays.
The implementation must re-check the official NIPs head before landing. Protocol drift after the pinned revision must be recorded in the contract/PR.
3. Architectural fit
Nostr is an independent content projection followed by explicit mutation stages. It is not a static hosting adapter and should not be forced into the current verified-target seam described by docs/contracts/publication-platforms.md.
The conductor rule remains:
validated Boris model + source body
├── canonical website
├── RSS / sitemap / search / IR / RAG / Context / llms.txt
└── Nostr event intentions
└── signer
└── relay delivery
A bare Boris build must retain current behavior and must never need a key, contact a relay, publish, delete, or otherwise mutate remote state. A failed Nostr plan/sign/publish operation must not invalidate, corrupt, or roll back a successful web build.
Publication facts belong in the strict publication profile. Document facts stay in the closed frontmatter contract. Do not add Nostr-specific frontmatter merely for convenience.
Coordinate with #452 because both proposals may touch publication-profile, CLI, and observed-network evidence contracts. Do not use that overlap to create a giant generic “federated publishing framework.”
4. Eligibility and selection
Required by protocol
Event content conforms to the NIP-23 Markdown profile.
An editable kind-30023 article has a stable d.
Topics emitted as t conform to NIP-24's lowercase convention.
Nostr references in content use the NIP-21/NIP-27 nostr: form.
Recommended and required by Boris v1
A page selected for full-content publication must:
be selected by an explicit, exact entity-ID allowlist in a Nostr publication-profile section;
come from the Markdown source family (.md or currently accepted .mdx source in Markdown mode);
not have draft status;
have an explicit frontmatter id rather than only a path-derived ID;
have explicit title, summary, and published_at;
pass the publication-safe Markdown validation below; and
have a canonical public web URL derived from the same normalized site.url/publication-location machinery used by current URL-bearing projections.
Textile and Cooklang entities are not eligible for NIP-23 full-content publication in v1, even though Boris has bounded internal adapters for their HTML rendering. They must produce an actionable eligibility diagnostic, never silent conversion.
Requiring an explicit entity ID is a Boris stability rule, not a NIP requirement. It makes ordinary source-path renames preserve the Nostr article address without adding a side database.
Optional/future
Cross-dialect publication may be revisited only if Oliver gains a reviewed canonical Markdown serialization that preserves semantics and satisfies NIP-23. That is future work, not an implicit promise of the current Textile/Cooklang adapters.
5. Publication-safe Markdown
The smallest honest v1 mapping is neither raw source bytes nor rendered HTML:
remove/skip Boris frontmatter using the existing parsed body boundary;
start from the original Markdown body;
apply only the existing deterministic Boris-owned source transformations needed to preserve the page's published meaning:
expand validated includes;
rewrite doc/wiki links to ordinary canonical Boris web URLs;
rewrite content-local image references to absolute canonical asset URLs;
validate the resulting source structurally through the Oliver parsing seam; and
emit those transformed Markdown bytes without a general normalize/reserialize pass.
This is a bounded “publication-safe Markdown view,” not HTML-to-Markdown and not a new Markdown engine.
The plan must fail closed when the selected page contains:
any raw HTML node;
Boris-only <Aside> or <Details> components, or any unconsumed Boris directive;
a hard-wrapped paragraph that would violate NIP-23's no-hard-line-break requirement;
an unresolved relative URL or content-local asset;
an unsupported source feature that cannot be represented as interoperable Markdown without semantic loss.
Validation must be syntax-aware so code spans and fenced code containing HTML-like text are not false positives. Do not silently strip content, emit rendered HTML, or claim compliance when validation cannot establish it.
Suggested diagnostic:
ENOSTRMARKDOWN <source>:<line>: selected NIP-23 article contains raw HTML;
remove/replace it with interoperable Markdown or opt this entity out of Nostr publication
Future canonical Markdown serialization from Oliver may remove the conservative hard-wrap restriction. It is not part of v1.
6. NIP-23 mapping
Use this fixed mapping and deterministic tag-group order:
Event field/tag
Boris v1 source
Classification
kind
constant 30023
Protocol required for NIP-23
content
publication-safe Markdown view
Protocol profile required; Boris validation required
d
exact explicit canonical Boris entity ID
Addressability recommended by protocol; required by Boris
title
explicit frontmatter title
Protocol optional; required by Boris
summary
explicit frontmatter summary
Protocol optional; required by Boris
published_at
frontmatter RFC3339 UTC converted to decimal Unix seconds string
Protocol optional; required by Boris as first-publication provenance
t
validated lowercase frontmatter tags, preserving source order after duplicate rejection
Protocol topic convention; optional per article
image
omitted in v1
Protocol optional; Boris has no owned document-image fact
r
canonical Boris page URL
NIP-24 interoperable web reference; Boris recommendation
created_at
supplied at signing, recorded as last-update/signing time
NIP-01 event field; NIP-23 update convention
authored Nostr refs
preserved/validated NIP-21 nostr: URIs
Protocol convention
Boris relations
no automatic a, e, or q tags
Deliberate non-mapping
Tag order is d, title, summary, published_at, zero or more t tags in validated source order, then r. The plan contract must define JSON string escaping and byte ordering exactly.
Do not infer image from the first Markdown image. Adding a document-level image later requires an explicit publication-model/frontmatter decision.
The NIP-24 r tag says the event refers to the canonical page; it does not establish protocol-wide canonical precedence. Whether v1 should additionally use NIP-73 ["i", <normalized-url>] plus ["k", "web"] remains an open review question. Do not invent a custom canonical tag.
7. Stable identity and updates
The Nostr article address is:
30023 : configured author pubkey : explicit Boris entity ID
Use the exact UTF-8 entity ID as d; do not hash it or derive it anew from the pathname. Boris already bounds entity IDs to 255 UTF-8 bytes, which also fits NIP-19's one-byte TLV length for naddr's identifier value. Validate collisions through the existing graph rules.
Consequences:
ordinary content edits keep the same address;
metadata-only edits keep the same address and produce a new version;
a source-path rename with the same explicit id keeps the same address, though a changed canonical URL is a metadata update;
changing explicit id or author pubkey creates a new address and must be reported as an identity migration;
path-derived IDs are rejected for selected v1 articles rather than accidentally creating new articles after rename.
No side database is required.
A newer signed event for the same address is an update. Boris cannot promise every relay sees or retains the same version.
8. Deterministic unsigned publication plan
Define a versioned artifact such as boris-nostr-publication-plan. It is an event-intention document, not a syntactically incomplete NIP-01 event.
The root records:
format/schema version;
Boris compiler/version;
pinned NIPs repository/commit/research date;
normalized public site identity;
expected author public key (lowercase 64-hex, public—not secret);
normalized ordered relay targets;
deterministic source/config provenance;
ordered article intentions.
Each article records at least:
entity ID and source path;
canonical web URL;
kind 30023;
stable d;
exact ordered tags;
exact event content;
SHA-256 content digest;
first-publication timestamp source and value;
created_at policy placeholder (signing-time in v1);
an event-intention digest that excludes relay destinations and signing time;
a delivery-config digest for relay targets;
eligibility/disposition where knowable.
Identical eligible source and configuration must produce byte-identical plan output. Planning performs no network requests, reads no private key, uses no wall-clock “now,” and does not claim an event ID or signature.
A source Git commit may be captured in a separate provenance wrapper or supplied explicitly; do not silently read a changing Git worktree and place it inside the byte-deterministic core plan. This preserves the current Boris publication-plan rule against ambient Git/time inputs.
9. Time semantics
Keep three concepts separate:
First publication time: source-owned published_at, deterministic, mapped to the NIP-23 tag.
Article-update time: NIP-23's created_at, supplied at signing.
Relay-observation time: when Boris observed an OK, rejection, or timeout; publish evidence only.
The offline plan must contain no wall-clock time. The signer uses current Unix seconds by default or an explicit test/recovery override, validates created_at >= published_at, and records the chosen value in the signed-event bundle. Because created_at participates in the event ID, signing is a controlled mutation of the plan intention, not a deterministic build phase.
10. Signing boundary and security
Define a narrow signer interface conceptually equivalent to:
verify that the signer pubkey matches the plan's expected author pubkey;
serialize the NIP-01 preimage exactly;
recompute the event ID;
obtain a BIP-340 Schnorr signature;
verify the signature before writing a signed-event bundle; and
bind that bundle to the exact plan/event-intention digest.
Recommended v1 signer: one explicit local signer that reads a hex secret or nsec once from standard input or a dedicated inherited file descriptor/secret-manager pipe. Do not accept the secret in a command-line argument, publication profile, checked-in file, public environment dump, or retained evidence. Environment-secret support is not the default because process environments are frequently copied into diagnostics.
NIP-46 remote signers and NIP-07 browser signers are future adapters behind the same boundary. NIP-49 describes an encrypted key representation but does not authorize Boris to become a key store. Hardware/remote signers are future work.
The key must never enter:
site output;
the unsigned plan;
normal or error logs;
proof/evidence artifacts;
crash diagnostics where Boris controls formatting;
shell history;
Git history.
Secret buffers should be bounded and zeroed best-effort after use.
Dependency decision required before implementation: current Boris/Zig dependencies do not expose a reviewed BIP-340 signing path. A separate security review must choose between a narrowly pinned/audited native libsecp256k1 boundary and a reviewed pure-Zig implementation. Do not improvise signature code inside the feature.
11. Relay configuration and publication
The publication profile may contain public Nostr configuration only:
enabled;
expected author pubkey;
exact entity-ID allowlist;
explicit non-empty relay list;
publication-safe timeouts/retry limits.
There is no hard-coded public relay list and no implicit network discovery. Production relays must use wss://. ws:// is permitted only for explicit loopback/mock-test configuration.
Validate URL syntax, host, optional port/path, and reject userinfo, query, fragment, credentials, invalid schemes, and duplicate normalized relay URLs. Normalize scheme/host/default port/root path and order normalized relays bytewise in the plan/evidence.
Publishing sends the exact already-signed event and waits a bounded time for a matching NIP-01 OK. A NOTICE is not success. A malformed or wrong-event reply is an error. Preserve the relay's acceptance boolean and bounded message without treating arbitrary relay text as trusted markup.
NIP-11 relay information is optional publish-time observation; it must not make planning network-dependent or be treated as authoritative acceptance. NIP-65 discovery is future convenience, not v1 configuration truth.
If a relay requires NIP-42 authentication, v1 should fail that relay with an actionable auth-required/unsupported outcome unless the review explicitly promotes NIP-42 into the first slice. Authentication would require another ephemeral signing flow and must not be smuggled into ordinary event publication.
12. Relay result semantics
The publish report must record per relay:
normalized target;
connection result;
exact event ID sent;
whether a matching OK arrived;
accepted/rejected boolean;
normalized machine-prefix classification and bounded relay message;
timeout/unavailable/malformed-reply state;
observation timestamp and duration.
Overall classification must preserve distributed truth:
complete: every selected relay accepted the event, including accepted duplicates;
partial: at least one accepted and at least one rejected, timed out, or was unavailable;
failed: none accepted and all relays returned explicit rejection;
incomplete: none accepted and at least one outcome is unknown because of timeout/unavailability.
Do not collapse these outcomes into one “Published” boolean.
13. Idempotence, retries, and removal
Compare the current event-intention digest with an explicitly supplied prior signed/publish-evidence bundle:
no prior evidence: disposition is unknown/needs-signature, not a claim of “first publish”;
unchanged content and metadata: reuse the exact signed event; do not re-sign merely to change created_at;
changed content or NIP-23 metadata: create a new signed event with the same address and a newer created_at;
relay-list-only change: reuse the exact signed event and deliver it to the requested relays;
failed/timed-out relay retry: resend the exact signed event, preserving event ID and signature;
identity change: report an explicit new-address migration.
Local evidence can describe what Boris attempted and observed. It is not a remote consistency database and cannot prove that all relays retain the same latest version.
Omitting or deleting a local Boris file must do nothing remotely. V1 does not automatically emit a NIP-09 deletion request. A future, separately confirmed unpublish command may plan and sign kind 5 with the same author, an a coordinate and corresponding k tag, but must say clearly that relays and downstream clients may retain the article. Deleting a deletion request does not restore or erase history.
14. Evidence
Keep deterministic intent evidence separate from temporal signing/network evidence.
These artifacts may adopt Boris's existing “configured / attempted / emitted / checked / passed / verified / failed / limited” vocabulary, but they must not pretend Nostr relays are target-local static hosts. The private key is never evidence.
15. Failure behavior
Provide stable, actionable diagnostics at the correct phase:
ENOSTRPLAN: invalid schema, digest mismatch, or plan/signed-bundle mismatch.
Diagnostics identify the entity/relay, cause, and remediation without printing secret input or the complete process environment. Nostr failure never retroactively invalidates a successfully committed website.
16. CLI shape
Preserve the current truth that boris plan --profile PATH is a deterministic configuration declaration and does not scan/build content. Do not silently redefine it.
The contract review should settle one of these explicit shapes:
boris plan nostr --profile PATH --out nostr-plan.json
boris sign nostr --plan nostr-plan.json --key-stdin --out nostr-events.json
boris publish nostr --events nostr-events.json --report nostr-publish.json
or a bounded boris nostr plan|sign|publish command family.
Whichever spelling wins:
plan is offline and secret-free;
sign is explicit and never publishes;
publish is the only relay-mutating step;
deletion/unpublish, if ever added, is a separate confirmed command;
machine-readable output, quiet behavior, usage errors, and exit classes follow current Boris CLI conventions;
per-relay partial success is represented in the report and exit contract.
Boris can produce a versioned NIP-23 publication plan offline without private keys, relay access, DNS, wall-clock time, or ambient Git data.
Identical eligible source and configuration produce byte-identical unsigned plan bytes.
A selected article requires an explicit Boris entity ID and retains the same (30023, pubkey, d) address across ordinary content edits and source-path renames that preserve that ID.
Kind, content, d, title, summary, published_at, topics, canonical web reference, and deterministic tag ordering follow the pinned protocol mapping above.
Frontmatter never appears in event content.
Markdown containing raw HTML, hard-wrapped prose, unresolved Boris-only constructs, or unsafe relative resources is rejected with an actionable diagnostic rather than silently transformed.
Textile and Cooklang are not silently converted to Markdown.
No rendered HTML or HTML-to-Markdown conversion enters the NIP-23 path.
Planning does not claim an event ID/signature; signing supplies created_at, creates the exact NIP-01 ID, produces BIP-340 signature bytes, and verifies them.
The signer public key must match the planned public identity.
Private key material never enters site output, profiles, plans, logs, retained proof/evidence, or Git history.
Relay destinations are explicit, normalized wss:// URLs with no protocol-truth default list.
Relay publication reports accepted, rejected, timed-out, unavailable, and malformed outcomes per relay and preserves partial success honestly.
Retrying an unchanged event resends the exact signed event rather than creating a needless update.
Removing a local article performs no automatic deletion/unpublish action.
A failed Nostr operation never corrupts or invalidates the canonical Boris website build.
Ordinary CI requires neither a real secret key nor public relay access.
Normative contracts, focused fixtures, diagnostics, CLI docs, and a uniquely named changelog fragment ship with implementation.
18. Test plan
Unit
NIP-23 field mapping and fixed tag-group ordering;
explicit-ID stability, collision rejection, rename behavior, and 255-byte boundary;
Markdown/Textile/Cooklang eligibility;
AST-aware raw-HTML/component detection;
hard-wrapped paragraph rejection without code-fence false positives;
deterministic include/link/wiki/asset projection;
deterministic unsigned plan bytes and digests;
RFC3339-to-Unix conversion and signing-time validation;
Unicode in title, summary, body, entity ID, and links;
canonical URL mapping under root/subpath publication;
relay URL normalization/deduplication;
prior-evidence disposition and retry behavior.
Fixtures
simple Markdown article;
topics;
summary and confirmation that image is omitted in v1;
long Unicode Markdown;
authored NIP-21 reference;
raw HTML;
<Aside>/<Details>;
hard-wrapped paragraph;
Textile rejection;
Cooklang rejection;
source rename with stable explicit ID;
unchanged article;
changed body;
metadata-only change;
content-local asset and rewritten canonical link.
Signing
Use deterministic, clearly test-only keys/fixtures. Verify exact NIP-01 serialization, SHA-256 event ID, BIP-340 signature, public-key match, and signature verification against authoritative upstream vectors where possible. Test malformed key, wrong signer, changed-plan digest, and bad signature. Never use a production key.
Relay
Use a local mock WebSocket relay for acceptance, accepted duplicate, rejection/prefix classification, timeout, unavailable connection, malformed/wrong-ID reply, NIP-42-required response, and mixed multi-relay results. Verify that retries preserve the exact event ID.
An optional bounded manual test may publish a disposable test-identity article to explicitly selected real relays. It is never required by ordinary CI and must not use a personal/production identity.
19. Non-goals
implementing a Nostr relay;
implementing a full Nostr client;
timelines or feed reading;
DMs;
wallets or zaps;
reactions;
comments/replies in v1;
account/profile management;
Textile-to-Markdown conversion;
Cooklang-to-Markdown conversion;
HTML-to-Markdown conversion;
replacing RSS;
replacing the canonical Boris website;
automatic private-key storage;
automatic deletion from relays;
automatic relay discovery in v1;
a giant generic federated-publishing framework.
Do NIP-23 well first.
20. Future work
cross-dialect publication if Oliver gains canonical Markdown serialization;
The first implementable slice should stop at an offline, byte-deterministic boris-nostr-publication-plan for an explicit allowlist of Markdown articles with explicit IDs, title, summary, and published_at. It should include publication-safe Markdown validation, canonical URL rewriting, fixed NIP-23 mapping, relay configuration validation, and fixtures—but no private-key ingestion, cryptography, WebSocket client, relay authentication, or remote mutation.
A second reviewed slice can add signing after the crypto dependency decision. A third can add explicit relay publication and per-relay evidence. This sequencing proves the content/identity contract before the security- and network-sensitive layers arrive.
Read this first (session handoff, 2026-08-16)
v1 is shipped on
afterparty. Do not re-implement plan/sign/publish. Do not treat the numbered spec below as a todo list. This parent stays open so remaining work has a home.First test (2026-08-16)
#591 merged. That is the first-tester live-smoke record (#583). Classification
partial:nos.lol,nostr.mom, andrelay.primal.netaccepted event0ef0b100…;relay.damus.iofailed WebSocket upgrade (BadStatus) twice. Fixture:docs/contracts/fixtures/nostr-live-smoke/. No secret in git. Not a CI gate. Not a verified target.Tester review: the testing client said they do not need more testing. Do not ask for another live smoke, another first-person try, or more first-tester evidence unless someone asks. This does not make Nostr a verified target and does not start #584.
Next session — start here
partial).BadStatuson this run is relay weather, not a reason to start #584.There is no default next Nostr product card. Next session starts only if someone wants #584 or a parked item.
Do not reopen: #492 #493 #494 #495 #496 #497 #545 #552 #566 #571 #583.
Do not redo: plan / sign / publish / DNS /
wss://handshake / NIP-19npub/naddr/ HTMLnostr:naddr/ the live-smoke fixture. Those are onafterpartyvia #468 #544 #551 #562 #568 #573 #591.Authority (read before coding)
docs/contracts/nostr-publication.mdcontent/guides/nostr-publication.mddocs/contracts/fixtures/nostr-live-smoke/partial)docs/STATUS.mdNostr row + roadmap item 7Constraints for the next session
afterparty(mainis frozen). Fresh topic branch from up-to-dateafterparty. Do not commit or push tomain. Do not force-push.publishnever sees the secret. Ordinary CI never needs a real key or a public relay.worktrees/grok-nostr-hostname(naddr HTML) andworktrees/grok-nostr-live-smoke(docs(nostr): record a first-tester live-smoke fixture #591). Start a new owned branch if more work is authorized.The original specification from §1 down is historical. §17 acceptance is checked. NIPs pin
656cecc7.Publication: add Nostr NIP-23 long-form target
Type: Product/protocol architecture
Status: v1 CLI shipped on
afterparty(2026-08-16). Remaining work is verified-target extras and §20 future items, not the original three slices.Target: Nostr long-form articles (NIP-23 kind
30023)Research date: 2026-08-14
Living contract:
docs/contracts/nostr-publication.mdNIPs authority:
nostr-protocol/nipsat656cecc7c0a815b6a2b218d3b5d6f078b3f4dbab, committed 2026-08-081. Opportunity
Boris should be able to syndicate selected Boris-authored articles to Nostr without weakening the ordinary static-site pipeline:
The canonical Boris website remains canonical. Nostr is another opt-in publication surface, alongside—not instead of—the website, RSS, sitemap, search, IR, RAG, Context Bundle, and
llms.txt.Boris must not become a full Nostr client, relay, social feed, wallet, or messaging application.
2. Current protocol research
Protocol authority for this issue is the official NIPs repository at the exact revision above:
OKresults.30023, Markdown profile, metadata, update time, and references.naddrencoding and human-facing key/address forms.nostr:URI references in content.rweb-reference tags and lowercasettopics.Material protocol facts at that revision:
[0, pubkey, created_at, kind, tags, content]. It cannot be computed until the public key andcreated_atare fixed. The signature is BIP-340 Schnorr over secp256k1.30023is addressable. Its address is(kind, author pubkey, d); relays replace versions according to NIP-01's addressable-event rules. Relays may retain different versions and resolve equal timestamps by event ID.created_atas the last-update time and defines optionaltitle,image,summary,published_at, and topicttags. Editable articles should carryd.["EVENT", event]; the meaningful response is a matching["OK", event_id, accepted, message]. Duplicate acceptance is still success. Rejection messages can carry machine-readable prefixes such asauth-required:,restricted:,rate-limited:, andinvalid:.The implementation must re-check the official NIPs head before landing. Protocol drift after the pinned revision must be recorded in the contract/PR.
3. Architectural fit
Nostr is an independent content projection followed by explicit mutation stages. It is not a static hosting adapter and should not be forced into the current verified-target seam described by
docs/contracts/publication-platforms.md.The conductor rule remains:
A bare Boris build must retain current behavior and must never need a key, contact a relay, publish, delete, or otherwise mutate remote state. A failed Nostr plan/sign/publish operation must not invalidate, corrupt, or roll back a successful web build.
Publication facts belong in the strict publication profile. Document facts stay in the closed frontmatter contract. Do not add Nostr-specific frontmatter merely for convenience.
Coordinate with #452 because both proposals may touch publication-profile, CLI, and observed-network evidence contracts. Do not use that overlap to create a giant generic “federated publishing framework.”
4. Eligibility and selection
Required by protocol
30023article has a stabled.tconform to NIP-24's lowercase convention.nostr:form.Recommended and required by Boris v1
A page selected for full-content publication must:
.mdor currently accepted.mdxsource in Markdown mode);idrather than only a path-derived ID;title,summary, andpublished_at;site.url/publication-location machinery used by current URL-bearing projections.Textile and Cooklang entities are not eligible for NIP-23 full-content publication in v1, even though Boris has bounded internal adapters for their HTML rendering. They must produce an actionable eligibility diagnostic, never silent conversion.
Requiring an explicit entity ID is a Boris stability rule, not a NIP requirement. It makes ordinary source-path renames preserve the Nostr article address without adding a side database.
Optional/future
Cross-dialect publication may be revisited only if Oliver gains a reviewed canonical Markdown serialization that preserves semantics and satisfies NIP-23. That is future work, not an implicit promise of the current Textile/Cooklang adapters.
5. Publication-safe Markdown
The smallest honest v1 mapping is neither raw source bytes nor rendered HTML:
This is a bounded “publication-safe Markdown view,” not HTML-to-Markdown and not a new Markdown engine.
The plan must fail closed when the selected page contains:
<Aside>or<Details>components, or any unconsumed Boris directive;Validation must be syntax-aware so code spans and fenced code containing HTML-like text are not false positives. Do not silently strip content, emit rendered HTML, or claim compliance when validation cannot establish it.
Suggested diagnostic:
Future canonical Markdown serialization from Oliver may remove the conservative hard-wrap restriction. It is not part of v1.
6. NIP-23 mapping
Use this fixed mapping and deterministic tag-group order:
kind30023contentdtitletitlesummarysummarypublished_attimagercreated_atnostr:URIsa,e, orqtagsTag order is
d,title,summary,published_at, zero or morettags in validated source order, thenr. The plan contract must define JSON string escaping and byte ordering exactly.Do not infer
imagefrom the first Markdown image. Adding a document-level image later requires an explicit publication-model/frontmatter decision.The NIP-24
rtag says the event refers to the canonical page; it does not establish protocol-wide canonical precedence. Whether v1 should additionally use NIP-73["i", <normalized-url>]plus["k", "web"]remains an open review question. Do not invent a customcanonicaltag.7. Stable identity and updates
The Nostr article address is:
Use the exact UTF-8 entity ID as
d; do not hash it or derive it anew from the pathname. Boris already bounds entity IDs to 255 UTF-8 bytes, which also fits NIP-19's one-byte TLV length fornaddr's identifier value. Validate collisions through the existing graph rules.Consequences:
idkeeps the same address, though a changed canonical URL is a metadata update;idor author pubkey creates a new address and must be reported as an identity migration;No side database is required.
A newer signed event for the same address is an update. Boris cannot promise every relay sees or retains the same version.
8. Deterministic unsigned publication plan
Define a versioned artifact such as
boris-nostr-publication-plan. It is an event-intention document, not a syntactically incomplete NIP-01 event.The root records:
Each article records at least:
30023;d;created_atpolicy placeholder (signing-timein v1);Identical eligible source and configuration must produce byte-identical plan output. Planning performs no network requests, reads no private key, uses no wall-clock “now,” and does not claim an event ID or signature.
A source Git commit may be captured in a separate provenance wrapper or supplied explicitly; do not silently read a changing Git worktree and place it inside the byte-deterministic core plan. This preserves the current Boris publication-plan rule against ambient Git/time inputs.
9. Time semantics
Keep three concepts separate:
published_at, deterministic, mapped to the NIP-23 tag.created_at, supplied at signing.OK, rejection, or timeout; publish evidence only.The offline plan must contain no wall-clock time. The signer uses current Unix seconds by default or an explicit test/recovery override, validates
created_at >= published_at, and records the chosen value in the signed-event bundle. Becausecreated_atparticipates in the event ID, signing is a controlled mutation of the plan intention, not a deterministic build phase.10. Signing boundary and security
Define a narrow signer interface conceptually equivalent to:
The sign stage must:
Recommended v1 signer: one explicit local signer that reads a hex secret or
nseconce from standard input or a dedicated inherited file descriptor/secret-manager pipe. Do not accept the secret in a command-line argument, publication profile, checked-in file, public environment dump, or retained evidence. Environment-secret support is not the default because process environments are frequently copied into diagnostics.NIP-46 remote signers and NIP-07 browser signers are future adapters behind the same boundary. NIP-49 describes an encrypted key representation but does not authorize Boris to become a key store. Hardware/remote signers are future work.
The key must never enter:
Secret buffers should be bounded and zeroed best-effort after use.
Dependency decision required before implementation: current Boris/Zig dependencies do not expose a reviewed BIP-340 signing path. A separate security review must choose between a narrowly pinned/audited native
libsecp256k1boundary and a reviewed pure-Zig implementation. Do not improvise signature code inside the feature.11. Relay configuration and publication
The publication profile may contain public Nostr configuration only:
enabled;There is no hard-coded public relay list and no implicit network discovery. Production relays must use
wss://.ws://is permitted only for explicit loopback/mock-test configuration.Validate URL syntax, host, optional port/path, and reject userinfo, query, fragment, credentials, invalid schemes, and duplicate normalized relay URLs. Normalize scheme/host/default port/root path and order normalized relays bytewise in the plan/evidence.
Publishing sends the exact already-signed event and waits a bounded time for a matching NIP-01
OK. ANOTICEis not success. A malformed or wrong-event reply is an error. Preserve the relay's acceptance boolean and bounded message without treating arbitrary relay text as trusted markup.NIP-11 relay information is optional publish-time observation; it must not make planning network-dependent or be treated as authoritative acceptance. NIP-65 discovery is future convenience, not v1 configuration truth.
If a relay requires NIP-42 authentication, v1 should fail that relay with an actionable
auth-required/unsupportedoutcome unless the review explicitly promotes NIP-42 into the first slice. Authentication would require another ephemeral signing flow and must not be smuggled into ordinary event publication.12. Relay result semantics
The publish report must record per relay:
OKarrived;Overall classification must preserve distributed truth:
Do not collapse these outcomes into one “Published” boolean.
13. Idempotence, retries, and removal
Compare the current event-intention digest with an explicitly supplied prior signed/publish-evidence bundle:
unknown/needs-signature, not a claim of “first publish”;created_at;created_at;Local evidence can describe what Boris attempted and observed. It is not a remote consistency database and cannot prove that all relays retain the same latest version.
Omitting or deleting a local Boris file must do nothing remotely. V1 does not automatically emit a NIP-09 deletion request. A future, separately confirmed unpublish command may plan and sign kind
5with the same author, anacoordinate and correspondingktag, but must say clearly that relays and downstream clients may retain the article. Deleting a deletion request does not restore or erase history.14. Evidence
Keep deterministic intent evidence separate from temporal signing/network evidence.
Plan evidence
d;Signed-event evidence
created_at;Publish observation evidence
These artifacts may adopt Boris's existing “configured / attempted / emitted / checked / passed / verified / failed / limited” vocabulary, but they must not pretend Nostr relays are target-local static hosts. The private key is never evidence.
15. Failure behavior
Provide stable, actionable diagnostics at the correct phase:
ENOSTRELIGIBILITY: non-Markdown source, draft, missing explicit identity, missing required title/summary/published_at, unsupported selected entity.ENOSTRMARKDOWN: raw HTML, hard-wrapped paragraph, remaining Boris-only construct, unsafe/unresolved URL or asset, unsupported Markdown feature.ENOSTRTIME: invalid timestamp conversion, out-of-range value, or signingcreated_atprecedingpublished_at.ENOSTRRELAY: invalid/duplicate relay URL, connection failure, timeout, malformed reply, rejection, partial publication, or unsupported authentication requirement.ENOSTRSIGN: missing/invalid key input, public-key mismatch, serialization/hash failure, signing failure, or signature-verification failure.ENOSTRPLAN: invalid schema, digest mismatch, or plan/signed-bundle mismatch.Diagnostics identify the entity/relay, cause, and remediation without printing secret input or the complete process environment. Nostr failure never retroactively invalidates a successfully committed website.
16. CLI shape
Preserve the current truth that
boris plan --profile PATHis a deterministic configuration declaration and does not scan/build content. Do not silently redefine it.The contract review should settle one of these explicit shapes:
or a bounded
boris nostr plan|sign|publishcommand family.Whichever spelling wins:
17. Acceptance criteria
Shipped in v1 (slices #468 / #544 / #551 / #562 / #568 / #573). Boxes checked 2026-08-16.
(30023, pubkey, d)address across ordinary content edits and source-path renames that preserve that ID.d, title, summary,published_at, topics, canonical web reference, and deterministic tag ordering follow the pinned protocol mapping above.created_at, creates the exact NIP-01 ID, produces BIP-340 signature bytes, and verifies them.wss://URLs with no protocol-truth default list.18. Test plan
Unit
Fixtures
<Aside>/<Details>;Signing
Use deterministic, clearly test-only keys/fixtures. Verify exact NIP-01 serialization, SHA-256 event ID, BIP-340 signature, public-key match, and signature verification against authoritative upstream vectors where possible. Test malformed key, wrong signer, changed-plan digest, and bad signature. Never use a production key.
Relay
Use a local mock WebSocket relay for acceptance, accepted duplicate, rejection/prefix classification, timeout, unavailable connection, malformed/wrong-ID reply, NIP-42-required response, and mixed multi-relay results. Verify that retries preserve the exact event ID.
An optional bounded manual test may publish a disposable test-identity article to explicitly selected real relays. It is never required by ordinary CI and must not use a personal/production identity.
19. Non-goals
Do NIP-23 well first.
20. Future work
Nostr identity/shipped (feat(nostr): emit NIP-19 naddr and accept npub (#566) #568 plan naddr, feat(nostr): emit nostr:naddr alternate links from the HTML build (#571) #573 HTML alternate links);nostr:naddrlinks from generated pages;21. Dependencies and review gates
Before implementation:
656cecc7...;rin v1 or also NIP-73i/k;22. Recommended first slice
The first implementable slice should stop at an offline, byte-deterministic
boris-nostr-publication-planfor an explicit allowlist of Markdown articles with explicit IDs, title, summary, andpublished_at. It should include publication-safe Markdown validation, canonical URL rewriting, fixed NIP-23 mapping, relay configuration validation, and fixtures—but no private-key ingestion, cryptography, WebSocket client, relay authentication, or remote mutation.A second reviewed slice can add signing after the crypto dependency decision. A third can add explicit relay publication and per-relay evidence. This sequencing proves the content/identity contract before the security- and network-sensitive layers arrive.