diff --git a/CloudronManifest.json b/CloudronManifest.json
index d0be45b..7451db5 100644
--- a/CloudronManifest.json
+++ b/CloudronManifest.json
@@ -3,7 +3,7 @@
"title": "Sendewerk",
"description": "Redaktionelle Planung und Durchführung von heinzlive-Sendungen.",
"tagline": "Vom Fundstück bis zur Livesendung",
- "version": "0.6.1",
+ "version": "0.7.0",
"healthCheckPath": "/healthz",
"httpPort": 3000,
"addons": {
diff --git a/docs/ACCEPTANCE.md b/docs/ACCEPTANCE.md
index c05396b..c8ad78c 100644
--- a/docs/ACCEPTANCE.md
+++ b/docs/ACCEPTANCE.md
@@ -1,17 +1,52 @@
# Abnahme
+## Etappe 7
+
+| Kriterium | Nachweis | Stand |
+| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------- |
+| Feedverwaltung und geplanter/manueller Abruf | Barkeeper-Oberfläche, Prozess-Scheduler und gemeinsame Abruffunktion | erfüllt am 25.10.2026 |
+| Getrennter Vorschau-Eingang | RSS-Beiträge erscheinen separat und werden nur nach Mehrfachauswahl zu Pieces | erfüllt am 25.10.2026 |
+| Kategoriezuordnung und Herkunft | Auswahlkategorien oder Feed-Standard sowie Original-URL als Piece-Quelle im Verhaltenstest | erfüllt am 25.10.2026 |
+| Dubletten und dauerhaftes Verwerfen | wiederholter Testabruf erzeugt keinen zweiten Eintrag und stellt Verworfenes nicht wieder her | erfüllt am 25.10.2026 |
+| HTML-Bereinigung und Abrufgrenzen | Testfeed entfernt fremdes HTML; Loopback wird vor Request blockiert; Größe, Zeit, Redirects und Eintragszahl sind begrenzt | erfüllt am 25.10.2026 |
+| Fehlerisolation | defekter lokaler Testfeed wird protokolliert, weiterer fälliger Feed trotzdem importiert | erfüllt am 25.10.2026 |
+| Versionierte REST-API und OpenAPI | `/api/v1` für Pieces, Sendungen und Einplanungen; `/api/v1/openapi.json` | erfüllt am 25.10.2026 |
+| Begrenzte und widerrufbare Zugänge | gehashte Tokens; Verhaltenstest für Lesen, verweigertes Schreiben und sofortigen Widerruf | erfüllt am 25.10.2026 |
+| Änderungsnachweis und Idempotenz | Audit-Ereignisse und persistente Idempotenz; identische Wiederholung liefert dasselbe Ergebnis | erfüllt am 25.10.2026 |
+| API-Rundlauf | echter HTTP-Test: lesen, anlegen, ändern, erneut lesen, einplanen und sortieren | erfüllt am 25.10.2026 |
+| Nutzung ohne KI | RSS und API sind optional; bestehende Redaktion funktioniert unverändert ohne externen Client | erfüllt am 25.10.2026 |
+| Konkrete ChatGPT-/Codex-Anbindung | keine Clientumgebung oder Zugangsdaten vorhanden | offen |
+
+- `npm run check`: 0 Fehler und 0 Warnungen.
+- `npm test`: 58 von 58 Verhaltenstests erfolgreich.
+- Gezielte Etappe-7-Tests: 6 von 6 erfolgreich.
+- `npm run build`: erfolgreich mit `@sveltejs/adapter-node`.
+- `npm audit --omit=dev`: 0 gemeldete Schwachstellen.
+- Gezielte Chromium-Prüfung: Desktop-Verwaltung, mobiler Eingang und kompletter
+ HTTP-API-Rundlauf erfolgreich, keine Konsolenfehler.
+- `npm run test:e2e`: auf dem Standardport wegen eines bereits laufenden fremden
+ Prozesses nicht startbar; identische temporäre Konfiguration auf Port 4188 mit
+ 16 von 18 erfolgreichen Abläufen. Bekannte Fehler bleiben bei Live-Zeitanzeige
+ und Abschluss/Freigabe bestehen.
+- `npm run lint`: nicht erfolgreich wegen acht bereits vorhandener
+ Prettier-Abweichungen außerhalb der Etappe-7-Dateien; geänderte Dateien wurden
+ separat formatiert.
+- Dokumentationsstatus: **API/MCP getestet, Client-Anbindung offen**. Konkret ist
+ die REST-API getestet; ein zusätzlicher MCP-Adapter wurde nicht benötigt und
+ keine echte ChatGPT-/Codex-Verbindung hergestellt.
+
## Wartungsrelease 0.6.1
-| Bereich | Stand |
-| --- | --- |
-| Live-Timer, Kopfzeile, Drawer und Positionsanzeige | behoben |
-| Mobile Formulare und Iconsuche | behoben |
-| Vorbereitungsaufwand und Übersicht | behoben |
-| Bild-Präsentationsmodus | behoben |
-| Twitch-Tags je Abschnitt | behoben |
-| Sendungskarten und Gastabstände | behoben |
-| Gemeinsamer Freigabedialog einschließlich Live-Regie | behoben |
-| Editorzustand und Drag-and-drop | bestehende zustandserhaltende bzw. alternative Bedienwege unverändert; Browserprüfung offen |
+| Bereich | Stand |
+| ---------------------------------------------------- | ------------------------------------------------------------------------------------------- |
+| Live-Timer, Kopfzeile, Drawer und Positionsanzeige | behoben |
+| Mobile Formulare und Iconsuche | behoben |
+| Vorbereitungsaufwand und Übersicht | behoben |
+| Bild-Präsentationsmodus | behoben |
+| Twitch-Tags je Abschnitt | behoben |
+| Sendungskarten und Gastabstände | behoben |
+| Gemeinsamer Freigabedialog einschließlich Live-Regie | behoben |
+| Editorzustand und Drag-and-drop | bestehende zustandserhaltende bzw. alternative Bedienwege unverändert; Browserprüfung offen |
- `npm run check`: 0 Fehler und 0 Warnungen.
- `npm test`: 52 von 52 Verhaltenstests erfolgreich.
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 77647a7..fd657c1 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -3,7 +3,7 @@
## Systemgrenze
Sendewerk ist ein einzelner SvelteKit-Prozess. Browserseiten, serverseitige
-Actions, spätere REST-Endpunkte und Hintergrundaufgaben verwenden dieselbe
+Actions, REST-Endpunkte und Hintergrundaufgaben verwenden dieselbe
Geschäftslogik. SQLite und ein lokales Upload-Verzeichnis sind die einzigen
persistenten Laufzeitabhängigkeiten. Externe Mail-, RSS- oder KI-Dienste sind
nicht Voraussetzung für Kernfunktionen.
@@ -11,7 +11,7 @@ nicht Voraussetzung für Kernfunktionen.
```text
Browser -> SvelteKit-Routen -> Anwendungsdienste -> SQLite
| -> Upload-Verzeichnis
- +-> später: Mail/RSS über Adapter
+ +-> Mail/RSS über begrenzte Adapter
```
Servercode lebt unter `src/lib/server` und darf nicht in Browser-Bundles
@@ -33,13 +33,13 @@ spätere API dieselben Regeln verwenden.
## Gesamtdatenmodell
-Die ersten sechs Etappen materialisieren Identität, Inhaltsverwaltung,
-Sendungsplanung, Live-Durchführung, Historie und Lesefreigaben. Folgende Grenzen
-gelten für umgesetzte und spätere Bereiche.
+Die ersten sieben Etappen materialisieren Identität, Inhaltsverwaltung,
+Sendungsplanung, Live-Durchführung, Historie, Lesefreigaben, RSS-Import und eine
+technische API. Folgende Grenzen gelten für umgesetzte und spätere Bereiche.
| Bereich | Geplante Entitäten und Beziehungen |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
-| Identität | `users`, Rollen, Sessions, Einladungen, Reset-Tokens und Sperren sind umgesetzt; technische Zugänge folgen später |
+| Identität | `users`, Rollen, Sessions, Einladungen, Reset-Tokens und Sperren sowie getrennte technische Zugänge |
| Taxonomie | getrennte `section_categories`, `formats`, `topics` samt optionalem Themenicon; Pieces und Vorschläge mehrfach verknüpft |
| Inhalt | `pieces` mit Status, Soft Delete, Favorit, Revision, Texten, Quellen, Werkzeugen und privaten Medien; Vorlagen speichern Snapshots |
| Sendungsbau | `shows`, geordnete `show_sections`, `piece_usages` samt Quellen-/Werkzeug-/Themen-/Mediensnapshots, Reservebank und Vorlagen |
@@ -99,6 +99,43 @@ konfigurierten Outbox-Pfad, `smtp` nutzt Nodemailer nur nach expliziter Auswahl.
Die feste Anwendungs-Origin erzeugt Links und bestimmt den `Secure`-Status der
Cookies; sie wird nicht aus ungeprüften Request-Headern übernommen.
+Technische Zugänge sind keine Browsersitzungen. Sie besitzen zufällige
+Bearer-Tokens, von denen nur SHA-256-Hashes gespeichert werden, und genau einen
+der Umfänge `read` oder `read_write`. Widerruf wirkt beim nächsten Request.
+`/api/v1` umgeht lediglich den Browser-Loginredirect; jeder Endpunkt prüft Token
+und erforderlichen Umfang selbst. Schreibzugriffe hinterlassen ein
+`audit_event`; Neuanlage und Planungsänderungen verlangen zusätzlich einen je
+Zugang eindeutigen, persistent gespeicherten Idempotenzschlüssel.
+
+## RSS-Import
+
+`src/lib/server/feeds.ts` bildet Abruf, XML-Verarbeitung und Übernahme gemeinsam
+für manuelle Aktionen und den Prozess-Scheduler ab. Der Scheduler startet beim
+ersten Request und prüft minütlich höchstens zehn fällige aktive Feeds. Jeder
+Feed wird getrennt behandelt, sodass ein Fehler weitere Abrufe nicht stoppt.
+HTTP-Abrufe erlauben nur HTTP(S) auf Standardports, prüfen DNS sowie jedes
+Redirectziel gegen interne Adressen und begrenzen Laufzeit, Redirects und
+Antwortgröße. RSS/Atom wird mit deaktivierter Entity-Verarbeitung gelesen;
+höchstens 200 Einträge je Antwort werden berücksichtigt und fremdes HTML wird zu
+reinem Text reduziert.
+
+`feed_entries` ist ein eigener Eingang. `(feed_id, external_id)` verhindert
+Dubletten; Aktualisierungen ändern den redaktionellen Status nicht. `discarded`
+bleibt deshalb auch nach späteren Abrufen erhalten. Erst die bewusste
+Mehrfachauswahl erzeugt innerhalb einer Transaktion normale Inbox-Pieces mit
+Kategorien und Original-URL als Quelle und setzt den Eintrag auf `imported`.
+
+## Technische REST-API
+
+Die Routen unter `/api/v1` bilden keine zweite Fachlogik, sondern rufen
+`content.ts` und `shows.ts` auf. Piece- und Sendungsrevisionen schützen weiterhin
+vor verlorenen Änderungen. `api_idempotency` speichert Request-Hash, Status und
+JSON-Antwort, sodass wiederholte Schreibbefehle dasselbe Ergebnis liefern und
+ein gleicher Schlüssel mit verändertem Inhalt abgewiesen wird. Die OpenAPI-3.1-
+Beschreibung ist unter `/api/v1/openapi.json` erreichbar. Ein MCP-Adapter oder
+produktspezifischer KI-Client gehört nicht zur Laufzeit und ist nicht
+Voraussetzung für die Bedienung.
+
## Inhalte, Markdown und Medien
Etappe 3 führt die Inhaltslogik in `src/lib/server/content.ts` zusammen. Listen
diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md
index 2de2df0..acbc832 100644
--- a/docs/DECISIONS.md
+++ b/docs/DECISIONS.md
@@ -459,3 +459,36 @@ Bildmodus verwendet ein separates boolesches Kennzeichen, damit bestehende
Tabellen und Daten trotz älterer Modus-Check-Constraints ohne riskanten
Tabellenumbau erhalten bleiben. Freigabeverwaltung bleibt eine gemeinsame
Komponente und öffnet in allen drei internen Einstiegen als nativer Dialog.
+
+## D-029: RSS als persistenter Eingang mit begrenztem Abruf
+
+Externe Feeds schreiben nie direkt in den Fundus. Quelle und stabiler
+Eintragsschlüssel werden in einem eigenen Eingang gespeichert; dessen Zustand
+`discarded` oder `imported` wird bei einem erneuten Abruf nicht zurückgesetzt.
+Erst eine bewusste Auswahl erzeugt über `content.ts` ein normales Piece samt
+Originalquelle und gewählten Kategorien. Dadurch bleiben redaktionelle
+Entscheidung, Dublettenschutz und Herkunft nachvollziehbar.
+
+Manueller und geplanter Abruf verwenden denselben Adapter. Wie bei
+Linkmetadaten werden private Ziele, Nichtstandardports und ungeprüfte Redirects
+gesperrt; zusätzlich gelten enge Zeit-, Größen- und Mengenlimits. XML-Entities
+bleiben deaktiviert und fremdes HTML wird nicht gespeichert. Ein Fehler wird am
+betroffenen Feed persistiert und unterbricht keine weiteren fälligen Feeds.
+
+## D-030: Technische Tokens, Scopes und persistente Idempotenz
+
+Externe Assistenz erhält keine simulierte Redaktionssitzung und keine
+Sonderrechte. Eigene zufällige Bearer-Tokens liegen nur gehasht vor, sind sofort
+widerrufbar und erlauben entweder Lesen oder Lesen und Schreiben. REST-Routen
+unter `/api/v1` verwenden die bestehenden Inhalts- und Sendungsdienste, damit
+Validierung, Revisionen und Transaktionen identisch bleiben. Jede erfolgreiche
+Mutation wird mit technischem Akteur, Aktion und Ziel protokolliert.
+
+Neuanlage, Einplanung und Sortierung verlangen einen `Idempotency-Key`.
+Zugang, Schlüssel, Hash des Requestinhalts und vollständige Antwort werden in
+derselben SQLite-Transaktion gespeichert. Eine identische Wiederholung erhält
+dieselbe Antwort; ein veränderter Request mit demselben Schlüssel wird
+abgewiesen. OpenAPI 3.1 beschreibt diese Grenze maschinenlesbar. Ein MCP-Adapter
+oder ein bestimmter KI-Client wird nicht vorgetäuscht: Die Anwendung bleibt ohne
+KI vollständig nutzbar und eine konkrete Client-Verbindung muss separat geprüft
+werden.
diff --git a/docs/RELEASE_NOTES.md b/docs/RELEASE_NOTES.md
index c99a847..ff6201b 100644
--- a/docs/RELEASE_NOTES.md
+++ b/docs/RELEASE_NOTES.md
@@ -1,5 +1,26 @@
# Release Notes
+## 0.7.0 - RSS-Import und externe Assistenz
+
+Veröffentlicht am 25. Oktober 2026
+
+- Barkeeper verwalten RSS-/Atom-Feeds mit Intervall und Standardkategorie;
+ sichere manuelle und geplante Abrufe laufen fehlerisoliert.
+- Neue Beiträge landen in einem getrennten Auswahlbereich im Eingang. Erst eine
+ bewusste Übernahme erzeugt Pieces, während dauerhaftes Verwerfen, Herkunft,
+ Kategorien und Dublettenschutz erhalten bleiben.
+- Abrufe sperren interne Ziele und begrenzen Ports, Redirects, Laufzeit,
+ Antwortgröße und Eintragszahl. Fremdes Feed-HTML wird zu Text bereinigt.
+- Die versionierte REST-API `/api/v1` liest und ändert Pieces, Sendungen und
+ Einplanungen über getrennte technische Lese- oder Schreibzugänge.
+- Technische Tokens werden nur einmal gezeigt und gehasht gespeichert. Widerruf,
+ Audit-Ereignisse, Revisionen und persistente Idempotenz schützen Änderungen.
+- `/api/v1/openapi.json` beschreibt die Schnittstelle für externe Clients.
+ Sendewerk bleibt ohne KI nutzbar; eine konkrete ChatGPT-/Codex-Anbindung ist
+ weiterhin offen.
+- Migration `0018_rss_and_api.sql` ergänzt Feed-, Zugangs-, Audit- und
+ Idempotenzdaten. Paket, Lockdatei und Cloudron-Manifest tragen Version `0.7.0`.
+
## 0.6.1 - Redaktionelle Wartung
Veröffentlicht am 25. Oktober 2026
diff --git a/docs/STATUS.md b/docs/STATUS.md
index bd62ab1..f245360 100644
--- a/docs/STATUS.md
+++ b/docs/STATUS.md
@@ -2,10 +2,40 @@
## Aktueller Umfang
-Etappe 1 bis Etappe 6 sind abgeschlossen. Architektur, Gesamtdatenmodell,
+Etappe 1 bis Etappe 7 sind abgeschlossen. Architektur, Gesamtdatenmodell,
Anmeldung, Rollen, persistente Inhaltsbearbeitung, Sendungsplanung,
Live-Steuerung, Präsentationsausgabe, unveränderliche Sendungshistorie,
-Archiv und sichere Lesefreigaben sind dokumentiert.
+Archiv, sichere Lesefreigaben, RSS-Import und technische REST-Zugänge sind
+dokumentiert.
+
+## RSS-Import und externe Assistenz 0.7.0
+
+- Barkeeper verwalten RSS-/Atom-Feeds mit Standardkategorie, Aktivierung und
+ Abrufintervall; manueller Abruf und ein begrenzter Prozess-Scheduler verwenden
+ dieselbe Fehler- und Dublettenlogik.
+- Feedabrufe prüfen Protokoll, Port, DNS und Redirects, begrenzen Laufzeit,
+ Antwortgröße und Eintragszahl und lassen Fehler anderer Feeds unberührt.
+- Fremdes HTML wird entfernt. Beiträge bleiben mit stabiler Herkunft im
+ getrennten Importeingang, bis die Redaktion sie auswählt, kategorisiert und
+ als normale Pieces übernimmt oder dauerhaft verwirft.
+- `/api/v1` bietet Pieces und Sendungen über gehashte, widerrufbare technische
+ Bearer-Tokens mit reinen Lese- oder Lese-/Schreibrechten an. Mutationen nutzen
+ bestehende Dienste, Audit-Ereignisse, Revisionen und persistente
+ Idempotenzschlüssel.
+- Die maschinenlesbare Beschreibung liegt unter `/api/v1/openapi.json`; eine
+ ChatGPT-/Codex- oder andere konkrete Client-Anbindung wurde nicht eingerichtet.
+- Migration `0018_rss_and_api.sql` ergänzt ausschließlich additive Tabellen und
+ Indizes. Sendewerk bleibt ohne KI und ohne konfigurierte Feeds vollständig
+ nutzbar.
+
+Geprüft wurden 58 Verhaltenstests, Typprüfung, Produktionsbuild, Dependency-
+Audit, vier RSS- und zwei Zugangssicherheitstests sowie ein echter HTTP-Rundlauf
+lesen → anlegen → idempotent wiederholen → ändern → erneut lesen → einplanen →
+sortieren. Desktop-Verwaltung und mobiler Eingang wurden in Chromium ohne
+Konsolenfehler geprüft. In der bestehenden Gesamtmatrix waren 16 von 18 Abläufen
+erfolgreich; die bereits bekannte Live-Zeitanzeige und der Ablauf
+Abschluss/Freigabe schlugen weiterhin fehl. Der globale Lintlauf bleibt an acht
+vorhandenen Formatierungsabweichungen außerhalb dieser Etappe offen.
## Wartungsrelease 0.6.1
diff --git a/migrations/0018_rss_and_api.sql b/migrations/0018_rss_and_api.sql
new file mode 100644
index 0000000..06ef29d
--- /dev/null
+++ b/migrations/0018_rss_and_api.sql
@@ -0,0 +1,66 @@
+CREATE TABLE feed_sources (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL UNIQUE,
+ url TEXT NOT NULL UNIQUE,
+ is_enabled INTEGER NOT NULL DEFAULT 1 CHECK (is_enabled IN (0, 1)),
+ refresh_interval_minutes INTEGER NOT NULL DEFAULT 60 CHECK (refresh_interval_minutes BETWEEN 15 AND 10080),
+ default_category_id TEXT REFERENCES section_categories(id) ON DELETE SET NULL,
+ etag TEXT NOT NULL DEFAULT '',
+ last_modified TEXT NOT NULL DEFAULT '',
+ last_fetched_at_utc TEXT,
+ next_fetch_at_utc TEXT,
+ last_error TEXT NOT NULL DEFAULT '',
+ created_at_utc TEXT NOT NULL,
+ updated_at_utc TEXT NOT NULL
+);
+
+CREATE TABLE feed_entries (
+ id TEXT PRIMARY KEY,
+ feed_id TEXT NOT NULL REFERENCES feed_sources(id) ON DELETE CASCADE,
+ external_id TEXT NOT NULL,
+ title TEXT NOT NULL,
+ url TEXT NOT NULL,
+ summary TEXT NOT NULL DEFAULT '',
+ published_at_utc TEXT,
+ status TEXT NOT NULL DEFAULT 'new' CHECK (status IN ('new', 'imported', 'discarded')),
+ piece_id TEXT REFERENCES pieces(id) ON DELETE SET NULL,
+ first_seen_at_utc TEXT NOT NULL,
+ updated_at_utc TEXT NOT NULL,
+ UNIQUE (feed_id, external_id)
+);
+
+CREATE INDEX feed_entries_status_idx ON feed_entries(status, first_seen_at_utc DESC);
+CREATE INDEX feed_sources_due_idx ON feed_sources(is_enabled, next_fetch_at_utc);
+
+CREATE TABLE technical_accesses (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL UNIQUE,
+ token_hash TEXT NOT NULL UNIQUE,
+ permissions TEXT NOT NULL CHECK (permissions IN ('read', 'read_write')),
+ created_by_user_id TEXT NOT NULL REFERENCES users(id),
+ created_at_utc TEXT NOT NULL,
+ last_used_at_utc TEXT,
+ revoked_at_utc TEXT
+);
+
+CREATE TABLE api_idempotency (
+ access_id TEXT NOT NULL REFERENCES technical_accesses(id) ON DELETE CASCADE,
+ request_key TEXT NOT NULL,
+ request_hash TEXT NOT NULL,
+ status_code INTEGER NOT NULL,
+ response_json TEXT NOT NULL CHECK (json_valid(response_json)),
+ created_at_utc TEXT NOT NULL,
+ PRIMARY KEY (access_id, request_key)
+);
+
+CREATE TABLE audit_events (
+ id TEXT PRIMARY KEY,
+ actor_type TEXT NOT NULL CHECK (actor_type IN ('user', 'technical')),
+ actor_id TEXT NOT NULL,
+ action TEXT NOT NULL,
+ target_type TEXT NOT NULL,
+ target_id TEXT NOT NULL,
+ created_at_utc TEXT NOT NULL
+);
+
+CREATE INDEX audit_events_created_idx ON audit_events(created_at_utc DESC);
diff --git a/package-lock.json b/package-lock.json
index 1c0102e..39a0a77 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,15 +1,16 @@
{
"name": "sendewerk",
- "version": "0.6.1",
+ "version": "0.7.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sendewerk",
- "version": "0.6.1",
+ "version": "0.7.0",
"dependencies": {
"@node-rs/argon2": "^2.2.2",
"better-sqlite3": "^13.0.3",
+ "fast-xml-parser": "^5.11.2",
"file-type": "^22.2.0",
"marked": "^18.1.1",
"metascraper": "^5.58.5",
@@ -599,6 +600,18 @@
"node": ">= 22.13"
}
},
+ "node_modules/@nodable/entities": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.1.0.tgz",
+ "integrity": "sha512-LsS/DjHr+uDM647Gru/cA8+J3a3HfhttwCKLyuoyN7yXTFCxKBtSgMQBvrW9yNPa2/zDRUNuGPaH5QUFoa4arQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodable"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/@node-rs/argon2": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@node-rs/argon2/-/argon2-2.2.2.tgz",
@@ -1630,6 +1643,18 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/anynum": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz",
+ "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/aria-query": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
@@ -2689,6 +2714,45 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-xml-builder": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.1.tgz",
+ "integrity": "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "path-expression-matcher": "^1.6.2",
+ "xml-naming": "^0.3.0"
+ }
+ },
+ "node_modules/fast-xml-parser": {
+ "version": "5.11.2",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.11.2.tgz",
+ "integrity": "sha512-R9iDuNrQYeQut46cn2r2wHKn4HYzVDvDm5J1wW+koZewykv0yuO2HChTYeZtrULyHIDM9cj9TUXloCsueCQUog==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@nodable/entities": "^3.0.1",
+ "fast-xml-builder": "^1.2.0",
+ "is-unsafe": "^2.0.0",
+ "path-expression-matcher": "^1.6.2",
+ "strnum": "^2.4.2",
+ "xml-naming": "^0.3.0"
+ },
+ "bin": {
+ "fxparser": "src/cli/cli.js"
+ }
+ },
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -3055,6 +3119,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-unsafe": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.2.tgz",
+ "integrity": "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/is-uri": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/is-uri/-/is-uri-1.2.14.tgz",
@@ -4052,6 +4128,21 @@
"node": ">=8"
}
},
+ "node_modules/path-expression-matcher": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz",
+ "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -4545,6 +4636,21 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/strnum": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.5.1.tgz",
+ "integrity": "sha512-UM4nTLWzQiDBseVxUGxN0wJzIk0UU89Qi7pKXz9qISo+lhviJikTuikBsK/oFRlpvYHvSdz0TVoLyQ+sEpYwxA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "anynum": "^1.0.1"
+ }
+ },
"node_modules/strtok3": {
"version": "10.6.2",
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.6.2.tgz",
@@ -5284,6 +5390,21 @@
"node": ">=18"
}
},
+ "node_modules/xml-naming": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz",
+ "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
diff --git a/package.json b/package.json
index f74a05f..20016cd 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "sendewerk",
"private": true,
- "version": "0.6.1",
+ "version": "0.7.0",
"type": "module",
"engines": {
"node": ">=22.17 <25"
@@ -26,6 +26,7 @@
"dependencies": {
"@node-rs/argon2": "^2.2.2",
"better-sqlite3": "^13.0.3",
+ "fast-xml-parser": "^5.11.2",
"file-type": "^22.2.0",
"marked": "^18.1.1",
"metascraper": "^5.58.5",
diff --git a/src/hooks.server.ts b/src/hooks.server.ts
index 942d2f4..9f0d6df 100644
--- a/src/hooks.server.ts
+++ b/src/hooks.server.ts
@@ -2,6 +2,7 @@ import { randomBytes } from 'node:crypto';
import { redirect } from '@sveltejs/kit';
import type { Handle } from '@sveltejs/kit/hooks';
import { deleteSession, getSessionUser, hasUsers, SESSION_COOKIE } from '#lib/server/auth.js';
+import { startFeedScheduler } from '#lib/server/feeds.js';
const publicPaths = new Set([
'/',
@@ -14,9 +15,11 @@ const publicPaths = new Set([
]);
export const handle: Handle = async ({ event, resolve }) => {
+ startFeedScheduler();
const presentationPath = event.url.pathname.startsWith('/heinzlive/');
const sharePath = event.url.pathname.startsWith('/freigabe/');
const shareMediaPath = /^\/freigabe\/[^/]+\/media\/[^/]+$/.test(event.url.pathname);
+ const apiPath = event.url.pathname.startsWith('/api/v1/');
const presentationTestPath = /^\/fundus\/[^/]+\/praesentation(?:\/|$)/.test(event.url.pathname);
const token = event.cookies.get(SESSION_COOKIE);
event.locals.user = getSessionUser(token);
@@ -26,6 +29,7 @@ export const handle: Handle = async ({ event, resolve }) => {
!presentationPath &&
!sharePath &&
!publicPaths.has(event.url.pathname) &&
+ !apiPath &&
!event.locals.user
) {
redirect(303, hasUsers() ? '/login' : '/setup');
diff --git a/src/lib/server/api-http.ts b/src/lib/server/api-http.ts
new file mode 100644
index 0000000..1ee9dd8
--- /dev/null
+++ b/src/lib/server/api-http.ts
@@ -0,0 +1,47 @@
+import { json } from '@sveltejs/kit';
+
+export async function apiBody(request: Request) {
+ const type = request.headers.get('content-type') ?? '';
+ if (!type.includes('application/json')) throw new Error('API_JSON_REQUIRED');
+ try {
+ return (await request.json()) as Record