joerglohrerde/docs/superpowers/specs/2026-04-15-publish-pipeline...

632 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`](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 aus `kind: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)
1. Auf dem Handy: Amber öffnen, Account wählen.
2. In Amber: „Generate Bunker URL" o. ä. — erzeugt eine `bunker://<hex-pubkey>?relay=wss://...&secret=...` URL.
3. Im Handy-Amber: Permission-Regeln setzen:
- `kind:30023` signieren → **auto-approve** für die Publish-Pipeline-App
- alle anderen Kinds → prompt (Sicherheitsnetz, sollte nicht aufschlagen)
4. Bunker-URL in die Pipeline-Umgebung einfügen:
- **Lokal:** in `.env` als `BUNKER_URL=bunker://...` (in `.gitignore`)
- **CI:** als GitHub-Actions-Secret `BUNKER_URL`
5. 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):**
```json
{
"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):**
```json
{
"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_URL` gesetzt, Bunker antwortet auf Ping, Pubkey stimmt mit `AUTHOR_PUBKEY_HEX` überein.
- `kind:10002` auf Bootstrap-Relay gefunden, mindestens 1 Relay eingetragen.
- `kind:10063` auf Bootstrap-Relay gefunden, mindestens 1 Server eingetragen.
- Blossom-Server aus `kind:10063` antwortet 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 Frontmatter `slug:`. Lowercase und URL-kompatibel (az, 09, `-`). Ist Teil des Tupels `(pubkey, kind, d)` für Replaceable-Semantik.
- `["title", "<title-string>"]` — aus Frontmatter `title:`.
- `["published_at", "<unix-seconds>"]` — aus Frontmatter `date:`, als Unix-Zeitstempel in Sekunden. **Stabil** über Edits hinweg — ändert sich nie.
**Empfohlene Tags (wenn im Frontmatter vorhanden):**
- `["summary", "<summary>"]` — aus Frontmatter `description:`.
- `["image", "<absolute-url>"]` — aus Frontmatter `cover.image:` (oder `image:`), transformiert zur absoluten URL gemäß Abschnitt 4.
- `["t", "<tag>"]` — ein Tag-Element pro Eintrag in Frontmatter `tags:`. Tag-Strings unverändert übernommen (Groß-/Kleinschreibung erhalten, weil Tag-Konvention im Nostr-Ökosystem case-sensitive ist).
**Event-Header:**
- `kind`: 30023
- `pubkey`: `AUTHOR_PUBKEY_HEX`
- `created_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`:
```ts
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`, `slug` müssen vorhanden sein; sonst harter Fehler für diesen Post.
- `slug` muss 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:
1. Pipeline sammelt alle Bilder aus dem Post-Ordner (Datei-Scan nach gängigen Bild-Extensions).
2. Jedes Bild wird zu allen Servern aus `kind:10063` hochgeladen (siehe §5).
3. Blossom liefert eine hash-basierte URL zurück (Format: `<server>/<sha256>` oder `<server>/<sha256>.<ext>`).
4. Pipeline baut eine Mapping-Tabelle `<dateiname> → <blossom-url>`.
5. Markdown-Body wird traversiert, alle erkannten Bild-Patterns werden ersetzt:
- `![alt](filename.png)``![alt](<blossom-url>)`
- `[![alt](filename.png)](link)``[![alt](<blossom-url>)](link)`
- `![alt](filename.png =WxH)``![alt](<blossom-url>)` (Größen-Suffix entfernt; SPA skaliert per CSS)
6. Wenn `filename` bereits 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); Fallback `image:` 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:**
1. Alle Dateien im Post-Ordner mit Bild-Extensions (`.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.svg`) sammeln.
2. Hugo-generierte Resize-Varianten (`*_hu_*.png` etc.) werden **ignoriert** — das sind Derivate, keine Originale. Nur die Originaldateien, wie sie im Markdown referenziert werden, zählen.
3. Pro Bild SHA-256 berechnen, zu allen Servern parallel hochladen.
4. Mapping `<filename> → <primary-blossom-url>` aufbauen (primär = erster Server aus Liste).
**Schritte pro Bild (intern):**
1. SHA256-Hash der Datei berechnen.
2. Authorization-Event (`kind:24242`) bauen und via Bunker signieren (enthält Hash, Verb `upload`, Expiration).
3. HTTP `PUT /upload` gegen alle Server gleichzeitig mit Auth-Header `Nostr <base64-signed-event>`.
4. Antworten sammeln: pro Server entweder `200 { url, sha256, ... }` oder Fehler.
5. Erfolg: mindestens 1 Server hat die Datei akzeptiert. Optimal: alle.
6. 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:**
```yaml
on:
push:
branches: [main]
paths: ['content/posts/**']
workflow_dispatch:
inputs:
force_all:
type: boolean
default: false
```
- Push auf `main` mit 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, selbem `pubkey`, selbem `kind` → ersetzt das alte Event (Replaceable-Semantik).
- Selbem `published_at` (Datum aus Frontmatter, unverändert).
- Neuem `created_at` (Signaturzeit).
- Geändertem `content` und 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:
```json
{
"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.json` hochgeladen (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`: Mock `git` subprocess.
### 9.2 Integration-Tests
- Mock-Relay (`jsr:@welshman/relay-mock` oder 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
```yaml
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:30023` Event-Schema — Publish produziert, SPA konsumiert.
- `kind:10002` Relay-Liste — Publish liest, SPA liest.
- `kind:10063` Blossom-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 `nak` o. ä. 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-all` mit 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 check` validiert 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 bunker` als 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 in `kind: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 check` ohne Fehler.
- 18 Altposts via einmaligem `deno task publish --force-all` publiziert.
- 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.json` enthä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 per `d`-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 /upload` mit Nostr-Auth-Header.
- **BUD-03:** Blossom-User-Description-03, `kind:10063` mit 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.