28 KiB
Publish-Pipeline für Nostr-Events — Design-Spec
Datum: 2026-04-15 (aktualisiert 2026-04-16: Blossom für alle Bilder, kein All-Inkl-rsync-Pfad mehr)
Status: Entwurf, ausstehende User-Freigabe
Scope: Toolchain, die Markdown-Posts aus content/posts/*/index.md in signierte Nostr-Events (kind:30023, NIP-23) umwandelt, zu Relays publiziert, und die zugehörigen Bilder zu Blossom hochlädt.
Designentscheidung 2026-04-16: Alle Bilder (auch die der 18 Altposts) werden zu Blossom hochgeladen. Kein rsync-Legacy-Pfad, kein image_source-Flag im Frontmatter. Die SPA rendert alle Posts über denselben Code-Pfad (Event-Text → Bild-URLs aus Blossom). Repo = Source-of-Truth für Content, Pipeline = Nostr-Export-Routine.
Diese Spec ist die Schwester-Spec zu 2026-04-15-nostr-page-design.md und teilt sich mit ihr den Event-Kontrakt für kind:30023 und die Konfiguration über kind:10002 / kind:10063.
1. Gesamtarchitektur
Auslöser
┌───────────────────┬───────────────────┐
│ │ │
▼ ▼ ▼
Lokaler CLI GitHub Action workflow_dispatch
`deno task (push auf main, (--force-all, z. B.
publish` wenn content/ für Migration oder
posts/** geändert) Reimport)
│ │ │
└───────────────────┴───────────────────┘
│
▼
┌─────────────────────────────┐
│ Publish-Pipeline (Deno) │
│ gemeinsame Library + CLI │
│ │
│ 1. Nostr-Kontext laden: │
│ • kind:10002 (Relays) │
│ • kind:10063 (Blossom) │
│ 2. Change-Detection │
│ (Git-Diff oder force) │
│ 3. Pro Post: │
│ a. Frontmatter parsen │
│ b. Bilder aus Ordner → │
│ Blossom upload │
│ c. Markdown body: bild- │
│ pfade → Blossom-URLs │
│ d. Event bauen │
│ e. Via NIP-46 signieren │
│ f. Zu Relays pushen │
└──────┬──────────────────────┘
│
┌──────────┼──────────────┐
▼ ▼ ▼
Amber Public Blossom-
(NIP-46 Nostr- Server
Signer Relays aus kind:10063
via aus (primal,
Relay) kind:10002 später eigener)
Kernprinzipien
- Deno als Runtime. Native TypeScript, Permissions-Modell, keine
node_modules. - Gemeinsame Library + CLI. Kernlogik in Modulen, sowohl von lokaler CLI als auch von CI-Workflow importiert. Keine Duplikation.
- Nostr als Source-of-Truth für Konfiguration. Relay-Liste aus
kind:10002, Blossom-Serverliste auskind:10063. Keine YAML-Config im Repo. - NIP-46 Bunker für Signaturen. Der private Schlüssel liegt nie in der Pipeline-Umgebung (nicht lokal, nicht in CI-Secrets). Bunker-Stufe Amber zum Start, Bunker-Stufe Optiplex nachrüstbar ohne Code-Change.
- Git-Diff als Change-Detection. Pipeline publisht nur geänderte Posts. Override-Flag für Migration und Reimport.
- State-los im Repo. Keine Lock-Files, kein Commit-zurück. CI ist read-only auf Repo-Content.
- Idempotenz. Wiederholte Läufe ohne inhaltliche Änderung erzeugen keine neuen Events (Git-Diff filtert).
Kostenübersicht
- Deno: 0 €.
- Amber, Public-Relays, Public-Blossom: 0 €.
- GitHub-Actions: im Free-Tier für persönliche Repos ausreichend.
- All-Inkl: unverändert, bereits Premium-Tarif für SSH.
- Zusatzkosten: keine.
Out-of-Scope
Diese Spec behandelt nicht:
- Kommentare/Reactions auf Posts. Die kommen von Besuchern über die SPA via NIP-07 (siehe SPA-Spec §3). Publish-Pipeline publisht ausschließlich Autor-eigene
kind:30023. - SPA-Deployment (SvelteKit-Bundle-Upload). Wird in einem separaten Deploy-Mechanismus behandelt oder als optionaler Subcommand nachgerüstet.
- Domain-Verwaltung, TLS-Zertifikate, All-Inkl-Paketwahl. Infrastruktur-seitig außerhalb der Pipeline.
2. Pre-Flight-Setup
Bevor der erste Publish-Lauf erfolgen kann, müssen folgende Bedingungen einmalig manuell erfüllt sein. Der Subcommand deno task check verifiziert die Punkte und gibt klare Fehlermeldungen aus, wenn etwas fehlt.
2.1 Nostr-Identität
- Pubkey des Autors: npub1f7jar3qnu269uyx5p0e4v24hqxjnxysxudvujza2ur5ehltvdeqsly2fx9
- Privater Schlüssel (
nsec) existiert nur in Amber auf dem Handy des Autors. Keine andere Instanz hält ihn.
2.2 NIP-46-Bunker-Pairing (Amber)
- Auf dem Handy: Amber öffnen, Account wählen.
- In Amber: „Generate Bunker URL" o. ä. — erzeugt eine
bunker://<hex-pubkey>?relay=wss://...&secret=...URL. - Im Handy-Amber: Permission-Regeln setzen:
kind:30023signieren → auto-approve für die Publish-Pipeline-App- alle anderen Kinds → prompt (Sicherheitsnetz, sollte nicht aufschlagen)
- Bunker-URL in die Pipeline-Umgebung einfügen:
- Lokal: in
.envalsBUNKER_URL=bunker://...(in.gitignore) - CI: als GitHub-Actions-Secret
BUNKER_URL
- Lokal: in
- Amber muss während CI-Runs online sein (WLAN oder mobile Daten). Akku-Optimierung für Amber auf dem Handy deaktivieren.
2.3 Relay-Liste (kind:10002)
Einmalig manuell publizieren via Nostr-Client (z. B. nostrudel.ninja mit Amber-Login, oder direkt aus Amber).
Schema (NIP-65):
{
"kind": 10002,
"pubkey": "<hex>",
"tags": [
["r", "wss://relay.damus.io"],
["r", "wss://nos.lol"],
["r", "wss://relay.nostr.band"],
["r", "wss://nostr.wine"]
],
"content": "",
"created_at": <unix>
}
Lese-Semantik der Pipeline (NIP-65):
["r", <url>]ohne drittes Element → Relay ist sowohl Read als auch Write.["r", <url>, "read"]→ nur Read; Pipeline ignoriert beim Publish.["r", <url>, "write"]→ nur Write; Pipeline nutzt beim Publish.
Phase 1: alle Einträge ohne drittes Element (beides). Spätere Differenzierung möglich, ohne Code-Änderung.
Replaceable Event (kein d-Tag) — bei späteren Updates (z. B. eigener Relay hinzu) wird einfach ein neues kind:10002 publiziert, das das alte ersetzt.
2.4 Blossom-Serverliste (kind:10063)
Einmalig manuell publizieren. Phase-1-Inhalt: ein Server.
Schema (BUD-03):
{
"kind": 10063,
"pubkey": "<hex>",
"tags": [
["server", "https://blossom.primal.net"]
],
"content": "",
"created_at": <unix>
}
Phase-5-Erweiterung (eigener Blossom-Server): zusätzliches ["server", "https://blossom.joerg-lohrer.de"] wird vorne in die Liste aufgenommen, neues Event publiziert.
2.5 deno task check
Dieser Subcommand verifiziert alle obigen Punkte:
BUNKER_URLgesetzt, Bunker antwortet auf Ping, Pubkey stimmt mitAUTHOR_PUBKEY_HEXüberein.kind:10002auf Bootstrap-Relay gefunden, mindestens 1 Relay eingetragen.kind:10063auf Bootstrap-Relay gefunden, mindestens 1 Server eingetragen.- Blossom-Server aus
kind:10063antwortet auf HEAD / (Healthcheck). - Deno-Version und benötigte Permissions.
Bei jedem Fehler: klare Text-Meldung, was zu tun ist (z. B. „kind:10002 fehlt — publiziere es manuell mit folgendem Schema: ...").
3. Event-Kontrakt (normativ)
3.1 kind:30023 — Blog-Post (NIP-23)
Pflicht-Tags:
["d", "<slug>"]— Slug-String, identisch mit Frontmatterslug:. Lowercase und URL-kompatibel (a–z, 0–9,-). Ist Teil des Tupels(pubkey, kind, d)für Replaceable-Semantik.["title", "<title-string>"]— aus Frontmattertitle:.["published_at", "<unix-seconds>"]— aus Frontmatterdate:, als Unix-Zeitstempel in Sekunden. Stabil über Edits hinweg — ändert sich nie.
Empfohlene Tags (wenn im Frontmatter vorhanden):
["summary", "<summary>"]— aus Frontmatterdescription:.["image", "<absolute-url>"]— aus Frontmattercover.image:(oderimage:), transformiert zur absoluten URL gemäß Abschnitt 4.["t", "<tag>"]— ein Tag-Element pro Eintrag in Frontmattertags:. Tag-Strings unverändert übernommen (Groß-/Kleinschreibung erhalten, weil Tag-Konvention im Nostr-Ökosystem case-sensitive ist).
Event-Header:
kind: 30023pubkey:AUTHOR_PUBKEY_HEXcreated_at: Unix-Zeitstempel des Signatur-Zeitpunkts (ändert sich bei jedem Edit).content: Markdown-Body nach Bild-URL-Transformation (Abschnitt 4).
Nicht gemappt (Hugo-spezifische Frontmatter-Felder ohne Nostr-Entsprechung):
layout, cover.caption, cover.alt, author, lang, dir, toc, toc_label, toc_icon, comments, weight, menus, aliases, draft.
Draft-Behandlung: draft: true im Frontmatter → Pipeline publisht diesen Post nicht (überspringt ohne Fehler, loggt Info).
3.2 kind:10002 — NIP-65 Outbox-Relays
Siehe Abschnitt 2.3. Von der Publish-Pipeline nur gelesen (Bootstrap beim Start); nicht von der Pipeline publiziert.
3.3 kind:10063 — BUD-03 Blossom-Serverliste
Siehe Abschnitt 2.4. Von der Publish-Pipeline nur gelesen (vor Blossom-Upload); nicht von der Pipeline publiziert.
3.4 Signing
Alle ausgehenden Events werden via NIP-46 Bunker signiert (nicht NIP-07 — dieser Flow ist rein browserseitig und für die CLI nicht anwendbar). Implementierung via applesauce-signers Nip46Signer:
const signer = new Nip46Signer(BUNKER_URL)
await signer.getPublicKey() // initialisiert Verbindung
const signed = await signer.signEvent(unsignedEvent)
4. Markdown- und Bild-Transformation
4.1 Frontmatter-Parsing
YAML-Frontmatter zwischen ----Trennern. Parser: jsr:@std/yaml oder npm:gray-matter.
Slug kommt als lowercase String aus dem Frontmatter-Feld slug:. Ist bereits normalisiert (siehe Commit d17410f) — Pipeline muss nichts ableiten oder lowercasen.
Validierung:
title,date,slugmüssen vorhanden sein; sonst harter Fehler für diesen Post.slugmuss regex^[a-z0-9][a-z0-9-]*$matchen; sonst harter Fehler.
4.2 Bild-URL-Transformation
Ziel: alle relativen Bild-Referenzen im Markdown-Body werden durch Blossom-URLs ersetzt. Ablauf:
- Pipeline sammelt alle Bilder aus dem Post-Ordner (Datei-Scan nach gängigen Bild-Extensions).
- Jedes Bild wird zu allen Servern aus
kind:10063hochgeladen (siehe §5). - Blossom liefert eine hash-basierte URL zurück (Format:
<server>/<sha256>oder<server>/<sha256>.<ext>). - Pipeline baut eine Mapping-Tabelle
<dateiname> → <blossom-url>. - Markdown-Body wird traversiert, alle erkannten Bild-Patterns werden ersetzt:
→[](link)→[](link)→(Größen-Suffix entfernt; SPA skaliert per CSS)
- Wenn
filenamebereits ein Schema enthält (http://,https://,//), bleibt die URL unverändert — ist schon absolut.
Konsequenz: Es gibt nur einen Upload-Pfad (Blossom). Kein Legacy-Pfad mehr. Kein image_source-Flag, keine Datum-basierten URL-Strukturen.
4.3 Cover-Image-Tag
Das image-Tag im Event (für Listen-Previews/OG-Vorschau in Nostr-Clients) kommt aus dem Frontmatter:
- Quelle:
cover.image:(Hugo-Page-Bundle-Konvention); Fallbackimage:auf Top-Level. - Ist typischerweise ein relativer Dateiname, der als Bild auch im Post-Ordner liegt und damit ohnehin zu Blossom hochgeladen wird.
- Wird nach dem Upload über die Mapping-Tabelle auf die Blossom-URL umgeschrieben.
- Wenn der Wert bereits absolut ist (http/https), bleibt er unverändert.
5. Upload-Pfad
5.1 Blossom-Upload (einheitlich für alle Posts)
Betrifft: alle Bilder aller Posts, ohne Unterscheidung zwischen Alt- und Neu-Post.
Mechanik: BUD-01 HTTP-Upload zu allen Servern aus kind:10063-Liste, parallel.
Ablauf pro Post:
- Alle Dateien im Post-Ordner mit Bild-Extensions (
.png,.jpg,.jpeg,.gif,.webp,.svg) sammeln. - Hugo-generierte Resize-Varianten (
*_hu_*.pngetc.) werden ignoriert — das sind Derivate, keine Originale. Nur die Originaldateien, wie sie im Markdown referenziert werden, zählen. - Pro Bild SHA-256 berechnen, zu allen Servern parallel hochladen.
- Mapping
<filename> → <primary-blossom-url>aufbauen (primär = erster Server aus Liste).
Schritte pro Bild (intern):
- SHA256-Hash der Datei berechnen.
- Authorization-Event (
kind:24242) bauen und via Bunker signieren (enthält Hash, Verbupload, Expiration). - HTTP
PUT /uploadgegen alle Server gleichzeitig mit Auth-HeaderNostr <base64-signed-event>. - Antworten sammeln: pro Server entweder
200 { url, sha256, ... }oder Fehler. - Erfolg: mindestens 1 Server hat die Datei akzeptiert. Optimal: alle.
- Markdown-URL nutzt die URL des ersten Servers aus der
kind:10063-Liste (deterministisch, reproduzierbar).
Failure-Modi:
- Alle Server lehnen ab → harter Fehler, Pipeline bricht für diesen Post ab.
- Manche Server OK, manche Fehler → Warnung in Log, Pipeline fährt fort mit erfolgreichem Upload.
Retry: 2 Versuche pro Server mit exponentiellem Backoff.
Idempotenz: Blossom dedupliziert per SHA-256. Ein erneuter Upload derselben Datei ist ein No-Op (Server antwortet 200 mit derselben URL). Daher ist wiederholtes --force-all unproblematisch.
5.2 Kein Legacy-Upload mehr
Frühere Versionen dieser Spec sahen einen rsync-Pfad zu All-Inkl für Altposts vor. Das ist entfallen. Begründung:
- Repo ist Source-of-Truth; alle Bilder liegen in
content/posts/<ordner>/. - Einheitlicher Render-Pfad in der SPA (keine Sonderlogik für Altposts).
- Blossom dedupliziert per Hash; wiederholter Upload ist billig.
- Nach Cutover verwaisen die alten
joerg-lohrer.de/YYYY/MM/DD/…-URLs — das ist akzeptiert, da sie nur in der weggehenden Hugo-Site referenziert sind.
6. Change-Detection und Workflow
6.1 Welche Posts werden publiziert?
Modus 1 — Git-Diff (Standard):
Pipeline vergleicht Dateiliste zwischen HEAD~1 (lokal) bzw. ${{ github.event.before }} (CI) und HEAD. Alle .md in content/posts/**/, die darin als A (added), M (modified) oder R (renamed) auftauchen, werden publiziert.
Modus 2 — --force-all (Migration / Reimport):
Alle content/posts/**/*.md werden publiziert, unabhängig von Git-Diff. Verwendet für:
- Initiale Migration der 18 Altposts (einmaliger lokaler Lauf).
- Nachträgliches Reimport nach Schema-Änderungen.
Modus 3 — --post <slug> (Einzel-Post, für Debug):
Nur der Post mit dem angegebenen Slug wird verarbeitet.
6.2 Trigger
Lokal: deno task publish [--force-all | --post <slug> | --dry-run].
GitHub Action:
on:
push:
branches: [main]
paths: ['content/posts/**']
workflow_dispatch:
inputs:
force_all:
type: boolean
default: false
- Push auf
mainmit Content-Änderung → automatischer Publish im Git-Diff-Modus. - Manual-Trigger via GitHub-UI → optional
force_all=true, dann--force-all-Lauf.
6.3 Idempotenz und Doppelpublikationen
Bei ausschließlicher Nutzung einer Variante (nur lokal ODER nur CI) ist Git-Diff-Detection präzise.
Edge Case: Wenn du lokal einen Post publisht, ohne zu pushen, und später CI läuft (z. B. für eine Content-Änderung an einem anderen Post), bekommt CI keinen Diff für den schon-lokal-publizierten Post — keine Doppelpublikation. Wenn du pushst, sieht CI im Diff die Änderung und publisht den Post erneut (dank replaceable-Semantik ist das funktional harmlos, nur etwas Relay-Bandbreite-Waste).
Akzeptable Redundanz. Spec dokumentiert es, aber keine aktive Mitigation.
6.4 Updates bestehender Posts
Ein Edit eines bereits publizierten Posts führt zu einem neuen kind:30023-Event mit:
- Selbem
d-Tag, selbempubkey, selbemkind→ ersetzt das alte Event (Replaceable-Semantik). - Selbem
published_at(Datum aus Frontmatter, unverändert). - Neuem
created_at(Signaturzeit). - Geändertem
contentund ggf. Tags.
Die Pipeline loggt explizit „UPDATE" vs. „NEU", indem sie vor dem Publish das Relay befragt, ob bereits ein Event für (pubkey, kind, dtag) existiert. Rein informativ; beide Pfade nutzen denselben Code.
7. Fehlerbehandlung und Retries
7.1 Relay-Publish
Pro Post wird das signierte Event an alle Relays aus der kind:10002-Liste parallel geschickt. Pro Relay:
- Bis zu 2 Retries mit exponentiellem Backoff (1s, 3s).
- Erfolg = Relay antwortet mit
OK true. - Timeout pro Versuch: 10 Sekunden.
Erfolgskriterium pro Post: mindestens 2 von 4 Relays haben bestätigt. Weniger → harter Fehler, Post wird als „failed" markiert, Pipeline fährt mit nächstem Post fort, am Ende Exit-Code != 0.
Log pro Relay: Status (OK / fail / timeout), Roundtrip-Zeit.
7.2 Blossom-Upload
Siehe Abschnitt 5.1. Pro Server 2 Retries, mindestens 1 Server muss akzeptieren.
7.3 Bunker-Signing
- Timeout 30 Sekunden pro Signatur-Request (Handy-Wake-up berücksichtigen).
- 1 Retry bei Timeout.
- Fehler (Permission denied, Bunker offline) → harter Abbruch der gesamten Pipeline (ohne Signaturen geht nichts).
7.5 Logging
Pipeline schreibt pro Run ein strukturiertes JSON-Log:
{
"run_id": "<uuid>",
"started_at": "<iso>",
"mode": "diff | force-all | post-single",
"posts": [
{
"slug": "offenheit-das-wesentliche",
"status": "success | failed | skipped-draft",
"action": "new | update",
"event_id": "<64-hex>",
"relays_ok": ["wss://..."],
"relays_failed": [],
"blossom_servers_ok": [],
"images_uploaded": 3,
"duration_ms": 1234
}
],
"ended_at": "<iso>",
"exit_code": 0
}
- stdout: in menschenlesbarer Form gedruckt.
- CI: zusätzlich als Artefakt
publish-log.jsonhochgeladen (30 Tage Retention). Keine Repo-Commits zurück. - Lokal: zusätzlich in
./logs/publish-<timestamp>.json(lokal in.gitignore).
8. Modul- und Dateistruktur
publish/
├── deno.jsonc # Imports, Tasks, Permissions
├── .env.example # Dokumentation (Commit), keine Werte
├── .gitignore # .env, logs/
├── README.md # Quickstart
├── src/
│ ├── cli.ts # CLI-Entrypoint (mit `@std/cli`)
│ ├── core/
│ │ ├── config.ts # BOOTSTRAP_RELAY, AUTHOR_PUBKEY_HEX
│ │ ├── frontmatter.ts # parseFrontmatter(md): { fm, body }
│ │ ├── validation.ts # validateSlug, validatePost
│ │ ├── markdown.ts # transformImageUrls, stripSizeHints
│ │ ├── event.ts # buildKind30023(fm, body)
│ │ ├── signer.ts # NIP-46 Bunker-Wrapper
│ │ ├── relays.ts # loadOutboxRelays, publishEvent
│ │ ├── blossom.ts # loadServerList, uploadBlob
│ │ ├── change-detection.ts # gitDiff, allPostFiles, forceMode
│ │ └── log.ts # structured logger + JSON writer
│ └── subcommands/
│ ├── publish.ts # Hauptbefehl, alle 3 Modi inkl. --dry-run
│ ├── check.ts # Pre-Flight-Validation
│ └── validate-post.ts # Einzel-Post-Check ohne Upload (nur Frontmatter/Bilder)
├── tests/
│ ├── frontmatter_test.ts
│ ├── validation_test.ts
│ ├── markdown_test.ts
│ ├── event_test.ts
│ ├── change-detection_test.ts
│ └── fixtures/
│ └── sample-post.md
└── .github/
└── workflows/
└── publish.yml # CI-Workflow
Tests: Deno-Standard-Test-Runner. Fokus auf Unit-Tests für pure Transformationen (frontmatter, markdown, event-bauen); Integration-Tests mit Mock-Relay und Mock-Bunker.
9. Testing-Strategie
9.1 Unit-Tests
parseFrontmatter: diverse Real-Beispiele aus den 18 Altposts, Edge Cases (Leerzeichen in Strings, YAML-Blocks).validateSlug: Regex-Matching-Grenzen.transformImageUrls: alle Markdown-Bild-Muster, Leerzeichen in Dateinamen, bereits absolute URLs.buildKind30023: Frontmatter → Event-Objekt, Tag-Mapping, draft-Behandlung.gitDiff: Mockgitsubprocess.
9.2 Integration-Tests
- Mock-Relay (
jsr:@welshman/relay-mockoder einfacher in-memory WebSocket-Mock). - Mock-Bunker: Test-Signer mit bekanntem Key.
- Full-Flow: Sample-Post → signieren → publish gegen Mock-Relay → Event vom Mock abrufen → Inhalt vergleichen.
9.3 End-to-End (manuell, einmalig)
- Auf Testnetz: Dedicated Test-Relay, Test-Pubkey, Test-Amber-Account.
- Einen Sample-Post durchschieben, in Habla.news verifizieren.
9.4 Pre-Flight-Check als Test
deno task check wird auch von CI vor jedem Publish-Run ausgeführt. Failed Check → Pipeline bricht ab bevor irgendwas publiziert wird.
10. GitHub-Actions-Workflow
name: Publish Nostr Events
on:
push:
branches: [main]
paths: ['content/posts/**']
workflow_dispatch:
inputs:
force_all:
type: boolean
default: false
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # für git-diff
- uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- name: Pre-Flight Check
env:
BUNKER_URL: ${{ secrets.BUNKER_URL }}
AUTHOR_PUBKEY_HEX: ${{ secrets.AUTHOR_PUBKEY_HEX }}
BOOTSTRAP_RELAY: ${{ secrets.BOOTSTRAP_RELAY }}
run: deno task check
- name: Publish
env:
BUNKER_URL: ${{ secrets.BUNKER_URL }}
AUTHOR_PUBKEY_HEX: ${{ secrets.AUTHOR_PUBKEY_HEX }}
BOOTSTRAP_RELAY: ${{ secrets.BOOTSTRAP_RELAY }}
GITHUB_EVENT_BEFORE: ${{ github.event.before }}
run: |
if [ "${{ inputs.force_all }}" = "true" ]; then
deno task publish --force-all
else
deno task publish
fi
- uses: actions/upload-artifact@v4
if: always()
with:
name: publish-log
path: ./logs/publish-*.json
retention-days: 30
11. Beziehung zur SPA-Spec
Diese Publish-Pipeline und die SPA sind komplementär, aber voneinander entkoppelt:
Gemeinsame Verträge (normativ festgelegt in dieser Spec, Abschnitt 3):
kind:30023Event-Schema — Publish produziert, SPA konsumiert.kind:10002Relay-Liste — Publish liest, SPA liest.kind:10063Blossom-Liste — Publish liest beim Upload, SPA liest für Bild-Fallback (zukünftig).- Alle Bild-URLs zeigen auf Blossom (hash-basiert) — einheitlich für alle Posts.
Unabhängige Entwicklung möglich:
- Publish kann gegen Mock-Relay und Mock-Bunker entwickelt und getestet werden, ohne dass die SPA existiert.
- SPA kann gegen manuell via
nako. ä. geschriebene Test-Events entwickelt werden, ohne dass die Publish-Pipeline existiert.
Abhängigkeit beim Cutover (SPA-Migrationsschritte C + D):
- SPA kann erst live gehen, wenn die 18 Altposts als Events auf Relays liegen.
- Schritt C der SPA-Migration bedeutet konkret: einmaliger lokaler Lauf
deno task publish --force-allmit dem vollständigen Altbestand. Dieser Schritt liegt zeitlich vor Schritt D (dem tatsächlichen Cutover der Hauptdomain). - Voraussetzung ist, dass die Publish-Pipeline zu diesem Zeitpunkt vollständig implementiert und durch
deno task checkvalidiert ist.
Laufender Betrieb:
- Neue Posts: Markdown committen, CI triggert Publish.
- SPA zeigt neuen Post beim nächsten Seitenreload an (Relay-Abfrage ist live).
- Zwei unabhängige Deploy-Zyklen (Publish bei Content-Änderung, SPA-Bundle bei Code-Änderung) ohne Kopplung.
12. Risiken und Mitigationen
| Risiko | Wahrsch. | Auswirkung | Mitigation |
|---|---|---|---|
| Amber offline während CI | mittel | hoch (Pipeline bricht ab) | Clear Error; Nutzer retriggert manuell nachdem Handy verfügbar |
| Bunker-Secret leakt (Repo-Secret) | niedrig | mittel | Secret rotierbar: in Amber Pairing löschen, neu pairen, Secret aktualisieren |
kind:10002 versehentlich überschrieben (Relay-Liste leer) |
niedrig | hoch | check-Subcommand prüft vor jedem Run; Pipeline bricht bei leerer Liste ab |
| Relay-Zensur (Events werden gelöscht) | niedrig | mittel | Multi-Relay-Push; zusätzlich bezahltes nostr.wine als Durability-Anker |
| Git-Diff übersieht Post (Rebase, Force-Push) | niedrig | niedrig | --force-all als Fallback, dokumentiert |
| Blossom-Server löscht Bild | mittel | mittel | Multi-Upload zu mehreren Servern sobald kind:10063 erweitert ist; nak blossom mirror als Ausgleich |
| Blossom-Server komplett weg, kein Mirror | niedrig | hoch | eigener Blossom-Server auf Optiplex (Phase 5) als dauerhafter Anker |
| Privater Schlüssel-Recovery | niedrig | katastrophal | Amber hat Backup-Mechanismus; nsec zusätzlich offline auf Hardware sichern |
13. Evolutionspfad
Jetzt (Bunker-Stufe Amber, Phase 1 Blossom):
- Handy mit Amber als einziger Signer, online während Publish-Runs.
- Ein Blossom-Server in
kind:10063(primal). - Alle Bilder (auch die der 18 Altposts) auf Blossom.
- Relay-Liste mit 4 Public-Relays.
Bunker-Stufe Optiplex (sobald Proxmox-Container läuft):
- Self-hosted Bunker (z. B.
nak bunkerals Container), 24/7 online. - Connection-URL im Secret rotieren; Amber bleibt als Backup/manueller Signer.
- Keine Code-Änderung in der Pipeline.
Phase 5 Blossom (eigener Blossom-Server auf Optiplex):
- Zusätzlicher
server-Tag inkind:10063(https://blossom.joerg-lohrer.de). - Neue Posts werden automatisch auch dorthin hochgeladen (Multi-Upload).
- Markdown-URL zeigt auf Primär-Server (= erster Eintrag in Liste). Soll das der eigene sein: Liste entsprechend ordnen.
Optional später:
deno task mirror— Subcommand, der bestehende Bilder (z. B. vom ersten Server) auch zu später hinzugefügten Servern spiegelt. Hilft bei Blossom-Server-Wechsel.
14. Success-Kriterien Phase 1
deno task checkohne Fehler.- 18 Altposts via einmaligem
deno task publish --force-allpubliziert. - Jeder Post in mindestens 2 Public-Relays abrufbar, in Habla.news korrekt gerendert.
- Alle Bilder auf Blossom erreichbar (Hash-URL liefert die Datei).
- Ein neuer Test-Post via CI auf
main-Push publiziert in unter 90 Sekunden ab Push. publish-log.jsonenthält aussagekräftige Einträge pro Post.- Pipeline läuft ohne nsec-Exposition in irgendeiner Umgebung.
Anhang: Begriffe
- NIP-23: Nostr-Langform-Events,
kind:30023, replaceable perd-Tag. - NIP-46: Nostr-Remote-Signer-Protokoll (Bunker). Signatur-Anfrage und -Antwort verschlüsselt über Relays.
- NIP-65: Outbox-Model,
kind:10002, definiert Read/Write-Relays pro Autor. - BUD-01: Blossom-Upload-Definition:
PUT /uploadmit Nostr-Auth-Header. - BUD-03: Blossom-User-Description-03,
kind:10063mit Server-Liste. - Amber: Android-App, die als NIP-46-Signer fungiert.
- Replaceable Event: Ersetzt vorherige Events mit gleichem
(pubkey, kind, d)-Tupel auf dem Relay.