Compare commits

..
239 changed files with 529 additions and 14680 deletions
-6
View File
@@ -1,6 +0,0 @@
root = true
[*.{bat,cmd}]
charset = latin1
end_of_line = crlf
insert_final_newline = true
-5
View File
@@ -95,11 +95,6 @@ COOLIFY_RESOURCE_UUID=
COOLIFY_TRIGGER_MODE=webhook
COOLIFY_DEPLOY_METHOD=GET
COOLIFY_DEPLOY_ENDPOINT=/api/v1/deploy?uuid={uuid}&force={force}
COOLIFY_DEPLOYMENT_STATUS_ENDPOINT=/api/v1/deployments/{deployment_id}
VENDOO_DEPLOY_TIMEOUT_SECONDS=900
VENDOO_DEPLOY_HEALTH_POLL_SECONDS=15
VENDOO_BUILD_REVISION=
VENDOO_COOLIFY_AUTO_DEPLOY=false
VENDOO_GITHUB_REPOSITORY=Masterluke77/vendoo
VENDOO_GITHUB_BRANCH=main
VENDOO_GHCR_IMAGE=ghcr.io/masterluke77/vendoo
-3
View File
@@ -1,3 +0,0 @@
* text=auto eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
-125
View File
@@ -5,7 +5,6 @@ on:
branches: [main, develop, 'feature/**', 'release/**', 'hotfix/**']
pull_request:
branches: [main, develop]
workflow_dispatch:
permissions:
contents: read
@@ -43,21 +42,9 @@ jobs:
- name: Git-Staging-Filter Regressionstest
run: npm run verify:git-staging
- name: Release-Manifest und Paketvertrag prüfen
run: npm run verify:release-manifest
- name: Git-Ignore-Grenzen für Quell- und Runtimeordner prüfen
run: npm run verify:ignore-boundaries
- name: Syntaxprüfung
run: npm run check
- name: PowerShell-Regressionen statisch prüfen
run: npm run verify:powershell-static
- name: Updater-3.4-Lifecycle und Startkette prüfen
run: npm run verify:updater-start-chain && npm run verify:updater-lifecycle
- name: Produktiven Source-Root und Archivgrenzen prüfen
run: npm run verify:source-boundaries
@@ -124,115 +111,3 @@ jobs:
- name: Docker-Image testweise bauen
run: docker build --pull --tag vendoo:ci .
windows-release-contract:
name: Windows vollständiger Releasevertrag
runs-on: windows-latest
timeout-minutes: 35
steps:
- name: Repository auschecken
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Node.js 22 aktivieren
uses: actions/setup-node@v4
with:
node-version: '22.x'
cache: npm
- name: Abhängigkeiten reproduzierbar installieren
run: npm ci
- name: Alle PowerShell-Dateien mit Windows PowerShell 5.1 parsen
shell: powershell
run: .\tools\verify-powershell-parser.ps1 -ReportPath "$env:RUNNER_TEMP\powershell-parser-diagnostics.txt"
- name: Parserdiagnose bei Fehler sichern
if: failure()
uses: actions/upload-artifact@v4
with:
name: powershell-parser-diagnostics-${{ github.event.pull_request.head.sha || github.sha }}
path: ${{ runner.temp }}/powershell-parser-diagnostics.txt
if-no-files-found: error
retention-days: 14
- name: Vollständigen Vendoo-Releasevertrag unter Windows ausführen
shell: pwsh
run: npm run verify:all
package-after-acceptance:
name: Updater-3.4-Paket nach Abnahme
needs: [quality, windows-release-contract]
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Exakten geprüften Head auschecken
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
- name: Node.js 22 aktivieren
uses: actions/setup-node@v4
with:
node-version: '22.x'
cache: npm
- name: Abhängigkeiten reproduzierbar installieren
run: npm ci
- name: main für den Provenienzvergleich aktualisieren
run: git fetch origin main:refs/remotes/origin/main
- name: Reproduzierbares Root-Manifest bestätigen
shell: bash
run: |
node tools/generate-release-manifest.mjs .
git diff --exit-code -- release-manifest.json
- name: Paket mit unveränderlicher Provenienz erzeugen
env:
VENDOO_SOURCE_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }}
run: node tools/build-release.mjs
- name: Paketmanifest und SHA-256 prüfen
shell: bash
run: |
set -euo pipefail
release_name="$(tr -d '\r\n' < release-output/RELEASE_NAME.txt)"
package_dir="release-output/${release_name}"
node --input-type=module - "$package_dir" <<'NODE'
import { readAndValidateManifest } from './tools/release-manifest-lib.mjs';
const packageDir = process.argv[2];
const manifest = readAndValidateManifest(packageDir);
if (manifest.schema !== 'vendoo-release-package-v2') throw new Error(`Unerwartetes Paketschema: ${manifest.schema}`);
if (manifest.updaterVersion !== '3.4.0') throw new Error(`Unerwartete Updater-Version: ${manifest.updaterVersion}`);
if (manifest.targetVersion !== '1.43.0') throw new Error(`Unerwartete Zielversion: ${manifest.targetVersion}`);
console.log(`Paket validiert: ${manifest.targetVersion}, Updater ${manifest.updaterVersion}, Payload ${manifest.payloadHash}`);
NODE
archive="release-output/${release_name}.zip"
python tools/build-deterministic-zip.py "$package_dir" "$archive"
first_hash="$(sha256sum "$archive" | awk '{print $1}')"
python tools/build-deterministic-zip.py "$package_dir" "$archive"
second_hash="$(sha256sum "$archive" | awk '{print $1}')"
if [[ "$first_hash" != "$second_hash" ]]; then
echo "Der ZIP-Build ist nicht byte-reproduzierbar." >&2
exit 1
fi
python -m zipfile -t "$archive"
(
cd release-output
sha256sum "${release_name}.zip" > "${release_name}.zip.sha256"
sha256sum --check "${release_name}.zip.sha256"
)
- name: Abgenommenes Paket als CI-Artefakt bereitstellen
uses: actions/upload-artifact@v4
with:
name: vendoo-1.43.0-updater-3.4-${{ github.event.pull_request.head.sha || github.sha }}
path: |
release-output/*.zip
release-output/*.zip.sha256
if-no-files-found: error
retention-days: 14
+21 -25
View File
@@ -16,20 +16,7 @@ env:
IMAGE_NAME: masterluke77/vendoo
jobs:
powershell-parser:
name: Windows PowerShell 5.1 Parser
runs-on: windows-latest
timeout-minutes: 10
steps:
- name: Repository auschecken
uses: actions/checkout@v6
- name: Alle PowerShell-Dateien mit Windows PowerShell 5.1 parsen
shell: powershell
run: .\tools\verify-powershell-parser.ps1
release-assets:
needs: powershell-parser
name: Release-Paket und Prüfsummen
permissions:
contents: write
@@ -55,21 +42,30 @@ jobs:
EXPECTED="v$(node -p "require('./package.json').version")"
test "$RELEASE_TAG" = "$EXPECTED" || { echo "Release-Tag $RELEASE_TAG passt nicht zu $EXPECTED." >&2; exit 1; }
- name: ESM-Exportverträge explizit prüfen
run: npm run verify:esm-exports
- name: FLUX-Loop-Hotfix explizit prüfen
run: npm run verify:flux-loop
- name: Vollständige Prüfung
run: npm run verify:all
- name: Release-Manifest reproduzierbar prüfen
run: |
node tools/generate-release-manifest.mjs .
git diff --exit-code -- release-manifest.json
npm run verify:git
npm run verify:git-regression
npm run verify:git-staging
npm run check
npm run verify:source-boundaries
npm run verify:esm-exports
npm run verify:flux-loop
npm run verify:capabilities
npm run verify:coolify
npm run verify:initial-modules
npm run verify:architecture
npm run verify:themes
npm run verify:security
npm run verify:identity
npm run verify:catalog
npm run verify:modules
npm run verify:deployment
npm run verify:config
npm run verify:installer-shell
npm run verify:db
- name: Manifestbasiertes Release-Staging erzeugen
- name: Release-Staging erzeugen
run: node tools/build-release.mjs
- name: ZIP und SHA-256 erzeugen
@@ -1,71 +0,0 @@
name: Vendoo Release Manifest Sync
on:
push:
branches:
- 'release/*-production-update-v*'
workflow_dispatch:
permissions:
contents: write
actions: write
concurrency:
group: vendoo-release-manifest-sync-${{ github.ref }}
cancel-in-progress: false
jobs:
sync:
name: Release-Manifest reproduzierbar synchronisieren
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Release-Branch auschecken
uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
fetch-depth: 0
- name: Node.js 22 aktivieren
uses: actions/setup-node@v4
with:
node-version: '22.x'
- name: Release-Manifest erzeugen
run: node tools/generate-release-manifest.mjs .
- name: Geändertes Manifest committen
id: manifest
shell: bash
run: |
set -euo pipefail
if git diff --quiet -- release-manifest.json; then
echo "Release-Manifest ist bereits aktuell."
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "head_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
exit 0
fi
git config user.name "vendoo-release-bot"
git config user.email "vendoo-release-bot@users.noreply.github.com"
git add release-manifest.json
git commit -m "chore: Release-Manifest synchronisieren [manifest-sync]"
git push origin HEAD:${GITHUB_REF_NAME}
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "head_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Finalen Manifest-Head explizit validieren
if: ${{ steps.manifest.outputs.changed == 'true' }}
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
final_head="${{ steps.manifest.outputs.head_sha }}"
remote_head="$(git ls-remote origin "refs/heads/${GITHUB_REF_NAME}" | awk '{print $1}')"
if [[ -z "$remote_head" || "$remote_head" != "$final_head" ]]; then
echo "Der gepushte Manifest-Head ist nicht stabil: lokal=$final_head remote=$remote_head" >&2
exit 1
fi
gh workflow run ci.yml --repo "$GITHUB_REPOSITORY" --ref "$GITHUB_REF_NAME"
gh workflow run sync-release-manifest.yml --repo "$GITHUB_REPOSITORY" --ref "$GITHUB_REF_NAME"
echo "Vendoo CI und Manifest-Sync wurden für finalen Head $final_head explizit registriert."
@@ -1,83 +0,0 @@
name: Vendoo Production Deploy
on:
workflow_dispatch:
inputs:
target_version:
description: Expected Vendoo version
required: true
type: string
public_base_url:
description: Public Vendoo URL
required: true
default: https://vendoo.flatlined.de
type: string
permissions:
contents: read
concurrency:
group: vendoo-production-deploy
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Validate inputs
shell: bash
env:
TARGET_VERSION: ${{ inputs.target_version }}
PUBLIC_BASE_URL: ${{ inputs.public_base_url }}
run: |
set -euo pipefail
[[ "$TARGET_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
[[ "$PUBLIC_BASE_URL" =~ ^https://[^/]+/?$ ]]
- name: Trigger Coolify
shell: bash
env:
COOLIFY_DEPLOY_WEBHOOK_URL: ${{ secrets.COOLIFY_DEPLOY_WEBHOOK_URL }}
run: |
set -euo pipefail
test -n "$COOLIFY_DEPLOY_WEBHOOK_URL"
curl --fail --silent --show-error --location --request GET \
--connect-timeout 20 --max-time 90 \
"$COOLIFY_DEPLOY_WEBHOOK_URL" >/tmp/coolify-response.txt
echo "Coolify accepted the deployment trigger."
- name: Wait for exact production version
shell: bash
env:
TARGET_VERSION: ${{ inputs.target_version }}
PUBLIC_BASE_URL: ${{ inputs.public_base_url }}
run: |
set -euo pipefail
BASE_URL="${PUBLIC_BASE_URL%/}"
deadline=$((SECONDS + 1500))
last_version="unknown"
while (( SECONDS < deadline )); do
if body=$(curl --fail --silent --show-error --connect-timeout 10 --max-time 30 \
-H 'Accept: application/json' -H 'Cache-Control: no-cache' \
"$BASE_URL/readyz?github_run=${GITHUB_RUN_ID}&attempt=${GITHUB_RUN_ATTEMPT}"); then
ready=$(jq -r '.ok // false' <<<"$body")
version=$(jq -r '.version // empty' <<<"$body")
if [[ -n "$version" ]]; then last_version="$version"; fi
echo "ready=$ready version=${version:-unknown} expected=$TARGET_VERSION"
if [[ "$ready" == "true" && "$version" == "$TARGET_VERSION" ]]; then exit 0; fi
fi
sleep 15
done
echo "Timeout waiting for Vendoo $TARGET_VERSION. Last version: $last_version" >&2
exit 1
- name: Final health response
shell: bash
env:
PUBLIC_BASE_URL: ${{ inputs.public_base_url }}
run: |
set -euo pipefail
BASE_URL="${PUBLIC_BASE_URL%/}"
curl --fail --silent --show-error -H 'Accept: application/json' \
"$BASE_URL/healthz?github_run=${GITHUB_RUN_ID}" | jq .
@@ -1,125 +0,0 @@
name: Vendoo Windows Release Diagnostics
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: vendoo-windows-diagnostics-${{ github.ref }}
cancel-in-progress: true
jobs:
windows-release-diagnostics:
name: Windows Release-Gates einzeln
runs-on: windows-latest
timeout-minutes: 35
steps:
- name: Repository auschecken
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Node.js 22 aktivieren
uses: actions/setup-node@v4
with:
node-version: '22.x'
cache: npm
- name: Abhängigkeiten reproduzierbar installieren
run: npm ci
- name: Alle PowerShell-Dateien mit Windows PowerShell 5.1 parsen
shell: powershell
run: .\tools\verify-powershell-parser.ps1
- name: Git-Sicherheitsgate
run: npm run verify:git
- name: Git-Sicherheitsgate Regressionstest
run: npm run verify:git-regression
- name: Git-Staging-Filter Regressionstest
run: npm run verify:git-staging
- name: Release-Manifest prüfen
run: npm run verify:release-manifest
- name: Git-Ignore-Grenzen prüfen
run: npm run verify:ignore-boundaries
- name: Syntax und Identity-Datenbank prüfen
run: npm run check
- name: PowerShell-Regressionen statisch prüfen
run: npm run verify:powershell-static
- name: Source-Root-Grenzen prüfen
run: npm run verify:source-boundaries
- name: ESM-Exportverträge prüfen
run: npm run verify:esm-exports
- name: FLUX-Loop-Hotfix prüfen
run: npm run verify:flux-loop
- name: Capability-Graph prüfen
run: npm run verify:capabilities
- name: Coolify- und Produktionsoperationen prüfen
run: npm run verify:coolify
- name: Initial deaktivierte Module prüfen
run: npm run verify:initial-modules
- name: Plattformarchitektur prüfen
run: npm run verify:architecture
- name: Theme Manager prüfen
run: npm run verify:themes
- name: Security Foundation prüfen
run: npm run verify:security
- name: Identity und Settings prüfen
run: npm run verify:identity
- name: Einladungen prüfen
run: npm run verify:invitations
- name: SQLite-Cleanup unter Windows prüfen
run: npm run verify:sqlite-cleanup
- name: Katalog und Medien prüfen
run: npm run verify:catalog
- name: Modulmanager prüfen
run: npm run verify:modules
- name: Produktionsoperationen prüfen
run: npm run verify:production-operations
- name: One-Click-Updater prüfen
run: npm run verify:one-click-update
- name: Modulnavigation prüfen
run: npm run verify:module-navigation
- name: Updater-UI prüfen
run: npm run verify:updater-ui
- name: Updater-Hotfix-Vertrag prüfen
run: npm run verify:updater-hotfix
- name: Deployment prüfen
run: npm run verify:deployment
- name: Konfigurationsspeicher prüfen
run: npm run verify:config
- name: Installer-Shell-Portabilität prüfen
run: npm run verify:installer-shell
- name: Datenbankmigrationen prüfen
run: npm run verify:db
+42 -42
View File
@@ -13,22 +13,22 @@
node_modules/
# SQLite runtime data
/db/*.db
/db/*.db-shm
/db/*.db-wal
/db/*.sqlite
/db/*.sqlite3
db/*.db
db/*.db-shm
db/*.db-wal
db/*.sqlite
db/*.sqlite3
# User and runtime data
/uploads/*
!/uploads/.gitkeep
/backups/
/logs/
/tmp/
/temp/
/sessions/
/data/
/docker-data/
uploads/*
!uploads/.gitkeep
backups/
logs/
tmp/
temp/
sessions/
data/
docker-data/
*.log
# OS and editor files
@@ -43,20 +43,20 @@ compose.override.yaml
docker-compose.override.yml
# Local AI runtimes and downloaded model data
/local-ai/comfyui/
/local-ai/downloads/
/local-ai/extract-temp/
/local-ai/fashn-vton/.venv/
/local-ai/fashn-vton/engine/
/local-ai/fashn-vton/weights/
/local-ai/fashn-vton/outputs/
/local-ai/fashn-vton/runtime/
/local-ai/fashn-vton/hf-cache/
/local-ai/fashn-vton/fashn-vton-*.zip
/local-ai/fashn-vton/python311/
/local-ai/fashn-vton/uv/
/local-ai/fashn-vton/uv-cache/
/local-ai/fashn-vton/downloads/
local-ai/comfyui/
local-ai/downloads/
local-ai/extract-temp/
local-ai/fashn-vton/.venv/
local-ai/fashn-vton/engine/
local-ai/fashn-vton/weights/
local-ai/fashn-vton/outputs/
local-ai/fashn-vton/runtime/
local-ai/fashn-vton/hf-cache/
local-ai/fashn-vton/fashn-vton-*.zip
local-ai/fashn-vton/python311/
local-ai/fashn-vton/uv/
local-ai/fashn-vton/uv-cache/
local-ai/fashn-vton/downloads/
# Historical handover and feature-package archives (never source)
/Archiv/
@@ -64,32 +64,32 @@ docker-compose.override.yml
/archives/
# Local update, rollback and slice archives
/updates/
updates/
# Operations Center runtime
/operations/staging/
/operations/updates/
/operations/rollback/
/operations/apply-*/
/operations/pending-operation.json
/operations/last-operation.json
operations/staging/
operations/updates/
operations/rollback/
operations/apply-*/
operations/pending-operation.json
operations/last-operation.json
# Deployment assistant state
/.vendoo-install-mode
/runtime/
.vendoo-install-mode
runtime/
# Generated deployment and release output
/release-output/
release-output/
*.release.zip
*.release.sha256.txt
/.vendoo-github-state.json
.vendoo-github-state.json
# First-run and local installer state
/operations/first-admin-code.txt
/config/local/
operations/first-admin-code.txt
config/local/
# Installed Vendoo module packages (runtime)
/modules/
modules/
# Python cache
__pycache__/
-36
View File
@@ -1,36 +0,0 @@
# Vendoo 1.43.0 Start hier
## Production Updater 3.3 starten
1. Dieses ZIP vollständig in einen neuen lokalen Ordner entpacken, zum Beispiel `C:\Vendoo-Update`.
2. Im Ordner `vendoo` doppelt auf `UPDATE_VENDOO.bat` klicken.
3. Den Vendoo Production Updater bis zum eindeutigen Ergebnis geöffnet lassen.
Der Updater zeigt zehn klare Phasen, Fortschritt, Live-Protokoll und einen dauerhaft sichtbaren Fehlerzustand. Beim ersten Lauf kann einmalig die GitHub-Anmeldung im Browser erforderlich sein. GitHub CLI wird bei Bedarf als verwaltete portable Kopie geladen und per SHA-256 geprüft.
Ein Coolify-Webhook wird nicht abgefragt. Nach einem erfolgreichen Merge verwendet Vendoo das bereits eingerichtete Coolify Auto Deploy der GitHub-Verknüpfung und bestätigt anschließend `/readyz` mit exakt Version `1.43.0`.
## Release-Architektur 3.0
- Die geklonte `main`-Arbeitskopie wird nicht geleert.
- `release-manifest.json` enthält jede auszuliefernde Datei mit Größe und SHA-256.
- Pflichtdateien wie `db/schema.sql` werden vor den Tests hart geprüft.
- Dateien werden als Overlay angewendet und im Ziel erneut gehasht.
- `release-manifest.json` wird anschließend als bytegeprüfte Steuerdatei in die Arbeitskopie übernommen.
- Löschungen sind nur über die explizite Liste `removedFiles` erlaubt.
- Alte Updater-2.x-Sitzungen werden nicht übernommen.
- GitHub Actions führt den vollständigen Releasevertrag auf Windows und Linux aus.
## Inhalt von Vendoo 1.43.0
- modulabhängige Navigation: deaktivierte Module verschwinden sofort aus der Seitenleiste,
- optionales, standardmäßig deaktiviertes Modul **Produktbild Studio**,
- grundlegend überarbeitete Seite **Neuer Artikel**,
- dauerhaft sichtbare Aktionen **Artikel generieren** und **Speichern**,
- manifestbasierter Production Updater 3.3.
## Sicherheitsgrenzen
Der Updater ersetzt oder löscht niemals `/app/data`, Datenbanken, Uploads, Backups, `.env` oder API-Schlüssel. GitHub-Checks, kontrollierter Merge, Coolify Auto Deploy und `/readyz`-Versionskontrolle bleiben verpflichtend.
Technische Details: `docs/PRODUCTION_UPDATER_3_3.md`.
-98
View File
@@ -1,103 +1,5 @@
## Vendoo Production Updater 3.3 Manifest Control Artifact Contract
- `release-manifest.json` wird nach dem Payload-Overlay ausdrücklich in die Git-Arbeitskopie kopiert.
- Die Steuerdatei wird bytegenau gegen das Release-Paket geprüft.
- Manifestprüfung in einer Git-Baseline toleriert zusätzliche unveränderte Baseline-Dateien, prüft aber alle manifestierten Dateien vollständig.
- `removedFiles` darf die Manifest-Steuerdatei nicht löschen.
- Der Windows-Lauf kann dadurch `verify-production-updater-3.3.mjs` innerhalb der geklonten Arbeitskopie ausführen.
- Prozessfehler zeigen in Status und Oberfläche zuerst die echte Fehlerausgabe; lange Standardausgaben verdrängen die Ursache nicht mehr.
## Vendoo Production Updater 3.1 Manifest-Based Release Application
- replaces destructive clone cleanup and exclusion-list copying with SHA-256 verified overlay manifest
- requires `db/schema.sql` and all release-critical source files before tests can start
- preserves baseline files unless deletion is explicitly declared in `removedFiles`
- verifies every payload file before and after copying
- rejects stale Updater-2.x sessions and uses a dedicated v3 release branch
## Vendoo Production Updater 2.10 Windows Installer-Shell Portability
- `verify:installer-shell` läuft über einen plattformneutralen Node-Wrapper.
- Unter Windows wird `sh.exe` aus Git for Windows über `where git`, `git --exec-path` und bekannte Installationspfade ermittelt.
- Keine globale `sh`-PATH-Annahme mehr.
- Git-Fortschritt auf stderr wird bei Exit-Code 0 als Ausgabe statt als Fehler protokolliert; npm-Warnungen erscheinen als Warnung.
- Zielversion der Anwendung bleibt Vendoo 1.43.0.
## Vendoo Production Updater 2.9 Systematic Windows SQLite Handle Audit
- Zentraler Cleanup für temporäre Vendoo-SQLite-Datenbanken.
- Einladungs-Verifier schließt `better-sqlite3` vor dem Entfernen des Temp-Ordners.
- Aktive Verifier werden transitiv auf offene `lib/db.mjs`-Handles geprüft.
- Windows-Cleanup-Regression läuft für Initialmodule und Einladungen jeweils dreifach isoliert.
## Vendoo Production Updater 2.8 Windows SQLite Cleanup
- `verify:initial-modules` schließt die temporäre `better-sqlite3`-Datenbank jetzt vor dem Entfernen des Testordners.
- Windows-`EBUSY` beim Löschen von `vendoo.db` behoben.
- Neuer dreifacher Cleanup-Regressionstest verhindert offene SQLite-Dateien und Temp-Reste.
# Vendoo 1.43.0 Production Updater 2.4
- GitHub-Browseranmeldung aus dem versteckten Worker entfernt.
- Sichtbares, klar beschriftetes Geräte-Anmeldefenster mit `--web --clipboard`.
- Stilles Loginstatus-Polling und automatische Fortsetzung nach erfolgreicher Anmeldung.
- Downloadfortschritt auf 10-%-Stufen gedrosselt; keine Logflut mehr.
- Neues Updater-2.4-Regressionsgate.
# Changelog
## Updater 2.1 Worker Host & Bootstrap Diagnostics
- überwachten Worker-Host eingeführt
- Status und Protokollpfad vor Worker-Start garantiert
- Standardausgabe und Standardfehler vollständig erfasst
- Lade-, Parser- und Initialisierungsfehler mit Datei und Zeile sichtbar
- Diagnoseordner direkt in der Oberfläche erreichbar
- Worker-Core beendet den Host nicht mehr eigenmächtig
## 1.43.0 Updater Revision 2.0 Production Updater Replacement
- Bisherige Winget-/Konsolen-Hotfix-Kette vollständig durch einen eigenständigen WPF-Production-Updater ersetzt.
- Neutrale dunkle Oberfläche mit zehn klaren Updatephasen, Fortschritt, Laufzeit, Heartbeat und Live-Protokoll.
- Fehlerzustände bleiben dauerhaft sichtbar; das Fenster schließt sich weder bei Fehlern noch bei einem unerwarteten Worker-Ende automatisch.
- Aktionen für Protokoll öffnen, Fehler kopieren, kontrollierten Abbruch und Wiederaufnahme.
- Verwaltete portable GitHub CLI mit offiziellem Release-Download und SHA-256-Prüfung statt Winget-Abhängigkeit.
- Feste Timeouts, Prozessbaum-Abbruch und Heartbeats für alle externen Werkzeuge.
- Persistente Sitzung für sichere Wiederaufnahme nach Abbruch oder Neustart.
- GitHub-, Test-, Merge-, Coolify- und Produktionsprüfung bleiben als getrennte Phasen nachvollziehbar.
## 1.43.0 Module-aware Navigation, Listing Studio UX & Updater Status UI
- Navigationseinträge verschwinden unmittelbar, wenn das zugehörige Modul deaktiviert wird.
- Neues authentifiziertes, minimales Modulstatus-API für die Oberfläche.
- Produktbild Studio als eigenständiges, standardmäßig deaktiviertes Modul.
- Seite „Neuer Artikel“ neu geordnet, lesbarer und konsequent am Theme-System ausgerichtet.
- „Artikel generieren“ und „Speichern“ bleiben in einer sticky Aktionsleiste dauerhaft erreichbar.
- Ein-Klick-Updater mit grafischer Fortschrittsanzeige, Prozentwert, Phase und Live-Protokoll.
## 1.42.0 Production Operations & Controlled Update Completion
- Persistente Deployment-State-Machine mit Ereignisverlauf und Wiederaufnahme nach Neustart.
- Idempotency-Key und Sperre gegen parallele Deployment-Aufträge.
- Vorab-Backup wird erstellt, verifiziert und mit dem Auftrag verknüpft.
- Coolify-Trigger, Remote-Status, Readiness und Zielversion werden getrennt bewertet.
- Webhook-Abschlüsse benötigen eine zusätzliche exakte Bestätigung `CONFIRM`.
- Fehler und Zeitüberschreitungen wechseln in `rollback_required` und blockieren neue Aufträge bis zur manuellen Prüfung.
- Neuer Produktionscheck und Produktionsstatus im Update Center.
- Additive Tabellen `deployment_runs`, `deployment_events`, `backup_verifications` und `production_checks`.
- Keine Löschung oder Rücksetzung bestehender Daten, Benutzer, Uploads, Secrets, Backups oder Module.
- Neuer `UPDATE_VENDOO.bat`-Assistent automatisiert Voraussetzungen, Volltest, Release-Branch, Pull Request, Merge und Coolify-Deployment.
- Coolify-Webhook wird nur einmalig als verschlüsseltes GitHub Actions Secret hinterlegt.
- Produktionsworkflow meldet Erfolg ausschließlich bei `/readyz` und exakt Version `1.42.0`.
## 1.41.2 Git Ignore Boundary & Module Tracking Hotfix
- Root-Runtimeordner in `.gitignore` sind jetzt mit `/` verankert.
- `app/modules/` und `app/modules/sessions/` werden nicht mehr durch `modules/` beziehungsweise `sessions/` ausgeblendet.
- Neues echtes Git-Regression-Gate prüft `git check-ignore`, `git add --all` und `git ls-files`.
- GitHub-Deploy-Assistent blockiert den Push, wenn produktive Moduldateien fehlen, ignoriert oder nicht getrackt sind.
- Keine Datenbankmigration und keine Änderung an Nutzdaten.
## 1.41.1 CI Source Root & Archive Exclusion Hotfix
- ESM-Import-/Exportprüfung auf den produktiven Source-Root begrenzt.
-31
View File
@@ -1063,34 +1063,3 @@ Neue Kernel-Routen benötigen zwingend einen Modulbesitzer und eine explizite Po
- 17 Module, 15 nativ, 2 Legacy Bridges und 38 aufgelöste Capabilities
- GHCR-Releasebilder mit versionierten, `stable`- und `latest`-Tags
## 1.41.2 Git Ignore Boundary & Module Tracking
- Root-verankerte Runtime-Ignores
- `app/modules/` und `app/modules/sessions/` bleiben versionierter Source
- Git-Ignore-Semantiktest mit echtem temporärem Repository
- Pflichtdatei- und Tracking-Gate im GitHub-Deploy-Assistenten
## 1.42.0 Production Operations & Controlled Update Completion
- Persistente Deployment-State-Machine: `checking`, `backup_pending`, `backup_running`, `backup_verified`, `deploy_requested`, `deploy_running`, `health_wait`, `succeeded`, `failed`, `rollback_required`.
- Idempotente Aufträge und harter Doppelstartschutz.
- Verifiziertes Vorab-Backup mit SHA-256-/Manifestbezug.
- Coolify-API-Statusabfrage über konfigurierbaren relativen Endpunkt; Webhook-Modus mit expliziter Abschlussbestätigung.
- Readiness- und Zielversionsprüfung vor Erfolgsmeldung.
- Persistenter Ereignisverlauf, Neustart-Wiederaufnahme und kontrollierte Aufhebung einer Rollback-Sperre.
- Produktionsstatus und Produktionscheck im Update Center.
- Additive Migration ohne Reset bestehender Produktivdaten.
## 1.43.0 Module-aware Navigation, Listing Studio & Updater Status UI
- Authentifizierter Modulstatus-Endpunkt `/api/modules/navigation` mit minimaler Antwortfläche.
- Navigationselemente und optionale UI-Panels sind über `data-module` beziehungsweise `data-module-panel` an den realen Modulstatus gekoppelt.
- Modulstatus wird vor geschützten Modul-APIs geladen; ein deaktiviertes AI-Modul blockiert den App-Start nicht.
- Neuer Manifestvertrag `defaultEnabled`, damit ein Modul standardmäßig aus, aber administrativ aktivierbar sein kann.
- `vendoo.product-image-tools` stellt `product-images.variants` bereit und startet standardmäßig deaktiviert.
- Listing Studio mit sticky Command Bar, klarer Zwei-Spalten-Arbeitsfläche und dauerhaft sichtbaren Aktionen.
- Grafische Update-Oberfläche mit Fortschritt, Phase, Detailmeldung, Live-Protokoll, sicherer einmaliger Coolify-Eingabe und korrektem Exit-Code.
+21 -38
View File
@@ -3,9 +3,9 @@ aa343f8ee7948e6b87603e39cf208f8fd1c42a023f1c2261c708c0cd8a46faba .env.example
58e3f4b76cc9398f32dcd5481b2080e7e5ba322aca04b8b8374446299c5d7dd6 .github/CODEOWNERS
5bd335eb750d084ca12b6a3b3f35d0cd7bc454ba24ed280187c66acf75eaf396 .github/dependabot.yml
f65684e98b01d0e0b6bfe1284b5de3354559e4cd51e634fe9e9e2e5473b890e5 .github/pull_request_template.md
505e50970bda1156b91a2f3f5031877495e95b63db06129910ac7326c14c9c92 .github/workflows/ci.yml
a5b12e2d6bd326034c5c45ff137b649aee580dccc9cf4a41c9d6cc44e81c1b0f .github/workflows/release.yml
851ad84ebaa6d07af5fe975b3461c2402e7315a294ae29f0cd1a5f13cb7fc44f .gitignore
c171b46c41f53363139b7ba2b442a6dc4fd2f2807811a2dcc4adbd14844972c1 .github/workflows/ci.yml
6b1fc844cf0f220c9791a049bbaf3e53b7a93d1036dbbd5abe1aad2f0f7637fb .github/workflows/release.yml
6caf6e9ab52bd7f634994876c359e543cc7bc8bed2f0e7c09b70eee2651e07eb .gitignore
9bfa732952ad571d06527b32d96d79133fc23fa607df00de440b2802f543608c .npmrc
9d7c2a587c12f2b12ba2923e367ade8cdd4601dcac800ca2ac34afb302e9a6e1 app/architecture-boundaries.json
a6bdb9b6fdc97a0b2bbc052bd242a9885a75bcc46ee4f0414d8b7e215b6940de app/contracts/module.schema.json
@@ -84,11 +84,11 @@ c6558b8a0cd9d7ef30d0e259747805e3d91a5359c2dbfbd79e6794bb403a9926 app/modules/us
8640accd1550932459ecd14738139284cbcdd93f31a61ab6e7748791fc82933f app/modules/users/routes.mjs
fc4af51a35fd08289ec4380a78c06b3236a2469041ecfc911b9e0d7d269afa19 app/modules/users/service.mjs
c197ce417fbce6dab1d8ee157b0a105a099dd2021ece4dac3891d0b2022a0d58 bootstrap.mjs
c525080bcf08aa43d694f5075c43c7ad9c06901b0aed453695b343393f7a8b9f CHANGELOG.md
bee3a2c088e7fc26d4b59429e51f127adf6c0acaefb9ee28a9e35d349f533de2 CHANGELOG.md
61823fc9fdc036170cce6edeecbb612b442d1c65fc9f7a7eaba47e5016316ee5 CLAUDE.md
61199cecda2c84954de2b60c07eb9f3a2117fbaaa4c430319c461b2e76a10254 compose.bind-mounts.example.yaml
b8167851bcdbefa4a22e143cc6de8ab4441d3970752629bb2d8fdb03984f2904 compose.vps.yaml
b76a40463ce31202dbdc85db371eb9c6fcafce8cd263c685dac990311dbd50f4 compose.yaml
7cb13c091d0ee663ca6a5c004735bf1fdb1a4507ad967ddd2e6624dd9d47faf1 compose.vps.yaml
a2a091aa0c39059b48aa12403ba22ed017266d310d9b21c192211d6f3f926356 compose.yaml
6323f984f1e04ab135356a14ec151941638041f6b305ed68772d5e1812972075 db/schema.sql
49690c4e4c8e84de8eb40ffe79823493b7be20fe46fa2ad9e6544f7f0afbfd2c deploy/Caddyfile
4ffc43e64b0e1e51a97d3f7f6a344ebcff537395008cf909c0fc4e3d3ab0c0cf deploy/coolify.env.example
@@ -107,7 +107,6 @@ c050c8e11f2bdb0a742851423dcff864bafba9cc839c52b2f8d48b75f7b18633 docs/ABNAHMEBE
513144e961bcc6d3a6c6a5f82b80b24733f20cd2e0d14259b500b426a4f9dee6 docs/ABNAHMEBERICHT_1_40_2.md
1439ecb811af31dc2d54ca63f0521ad445e30940fae91ac7d23adc352c8332ad docs/ABNAHMEBERICHT_1_41_0.md
b23f235ef4ddf3bbc0fde848fefe9bdcc4b90d1efb25dc61d51cf7cb295415ec docs/ABNAHMEBERICHT_1_41_1.md
2f199ee66a97bbec2671d528f3b22c4583530644f7c16a4820fbe9c05e731bbd docs/ABNAHMEBERICHT_1_41_2.md
b39d2d892068e1313fc00c9345d54240b6d50caf8a940261a088e67d8778ac63 docs/architecture/CATALOG_MEDIA_MODULES_1_39_0.md
e5f065ba8d516a5ebd47e7e6859d0aab40ef69797413e358d969ef27c454fcdd docs/architecture/DESIGN_SYSTEM_1_35_0.md
732ef23db1ef50862a423841f58e4628ad3b2a2ef2744ed7c5ef1fe01e3e38d1 docs/architecture/IDENTITY_SETTINGS_MODULES_1_38_0.md
@@ -126,7 +125,6 @@ ba9a9b93622fadc399cd9067d80b4ea339193eb19825106264a3b86b7128d107 docs/GENERATOR
e5c6ac33f5c0dc93099148d95af123a9aeb72298d885cabf0b1dc96a9cad31d7 docs/GITHUB_IMPORT_STATUS_1_39_0.md
509fd2822e4c547459fef9a54054d583dc21adb1b1101eda92603f1cbb783604 docs/GITHUB_IMPORT_STATUS_1_41_0.md
8e4ee3c837df6e929a819491b977123872191c8e4f041f8598c9479df10d28fa docs/GITHUB_IMPORT_STATUS_1_41_1.md
16effe96702a6951b6cf3bb06da052420a492024c252a88b69256a3f4a92eccf docs/GITHUB_IMPORT_STATUS_1_41_2.md
5a25e952d7d79b562bc19c990451830ea2ac3e730c9af399425ea18ffb8fd61d docs/GITHUB_IMPORT_STATUS.md
65b7c1cb3a300fa8209628f32d843d1cfba7baaae1f1078bd3cfc1737443df92 docs/GITHUB_MERGE_RELEASE_1_41_0.md
65d19c1576f4a6011008a7e447a3530222517752fb59ebc5bbb506b791e32605 docs/HOTFIX_1_23_1_EDITOR_CACHE_VINTED_FIELDS.md
@@ -177,7 +175,6 @@ dd77564536e5d85eaa76c7a429a61f73ac78547d72734620bc4102b3452cd7b4 docs/RELEASE_1
69b6df0262868a6e6ef81f406ee4c9acd3f662d1f6ee11d7fdd5fea96a2f0de7 docs/RELEASE_1_41_0_COOLIFY_PRODUCTION.md
97f7bc395f2ce6bef24140e75dc8956a0d56cbaf15cb94876d4594d7aa54b1db docs/RELEASE_1_41_0_VERIFICATION.json
29c795b5599bafdf0f911a4c7ab2844ca3fb311bcdd5f3983c7fa2742110c3ff docs/RELEASE_1_41_1_CI_SOURCE_ROOT_ARCHIVE_EXCLUSION_HOTFIX.md
178927940cc1266307f81bbf418870e627d2e52b4956c2d9065c31e04c9bf7dc docs/RELEASE_1_41_2_GIT_IGNORE_BOUNDARY_HOTFIX.md
1908713d7458ecacc833faced77984ef5c0155cd61b0eac13a4f874e3f3a5676 docs/ROADMAP_1_38_0.md
185b1007c90273126ebe92bffe69f88ae4ec4a3f04451cb9d5d4b772a0482391 docs/ROADMAP_1_39_0.md
b581d7d949139f37798dded557235a679621172f444a6271e7fc7ade3020299a docs/ROADMAP_1_40_0.md
@@ -302,7 +299,7 @@ e3a3863b58a571a0f0180bdd333f9389b918415b4ea96b70f3ab6d4105d95c50 extensions/saf
2bf2d5ccf02622cf1d62fd553d21c0ef1b744616742e7962ac302c10d37426e2 extensions/safari/web-extension/popup.js
273322e8dc29b0c6b639a6cc7e44a9570ceece09306fd46003b05a8fc86c88b7 extensions/safari/web-extension/vinted-categories.js
273322e8dc29b0c6b639a6cc7e44a9570ceece09306fd46003b05a8fc86c88b7 extensions/vinted-categories.js
4be8e17f83ebb805abb1beddf4d7a79e3a44ce58585cb6b8143bb01a47e16c6b FEATURES.md
f4caaaad32095dbb6dbb7ef68f1777fdd0a036e1efdfe611446789790021b51d FEATURES.md
7f78a0d512de717ba76c9a5e3e1bd2e52db9edf146e8eeb7297458fde5d92afa GitHub-Deploy-Assistent.bat
9a5bfcafdce53130d7bc36695090a8f87096b4ead9dd5ccc3d46d1d80c07a0fa lib/ai-claude.mjs
ad6ba4ef521557d5c06bc546cda70db57e710e8173daadae74e8f15f99836c1c lib/ai-flux-prompt-jobs.mjs
@@ -349,15 +346,15 @@ b1c62e118d9be316274e966c558cdcccdd41afb3a9f05a60ec1e4a3433d2ca0c MockupDesign/L
a52983b9ea2bf920d115485d10bc30226da88bc5cba8b00bebf801b21c096346 MockupDesign/Publish.png
8c8bf278d0c6725e1dbb97b3c3d0c36a9bfc0753c3c86433fdc294bec70dbc0e MockupDesign/Startseite.png
62f14221b1d71c9e631efdadd221397ff657adae4b737df52c8bff1610b90999 MockupDesign/Vorlagen.png
a111671a21576140185a0c4d01038e200f87cdcbf903efe57bb84f65c972fd1a package-lock.json
740a9ee1699aa8e24fab4ab66373bf4322bee0e60c6dd80dbc07e8f9d18a3771 package.json
62daf878d676237dff931c292a79309662176b84f28be9dbd1af8af42827e883 package-lock.json
7b4ca7c0bad995c03180c04291f0f71a4a6cd35446b4db96fc33bf0a0dd36930 package.json
3da71dd28cb25c7c7317b9061855d40c6a3ab8bc39391963b49ee1e707f838e5 platforms/ebay-de.mjs
cc1ca2b4384c8c548bf755dc33101d248541bf893c3d5f35369961f904afcc00 platforms/ebay-ka.mjs
fa52b247dd758aeee06120499bd1ae6c53e9f460f8bd7762b8f06141b201e32b platforms/etsy.mjs
5c925e3934a4c9e2f26dc15b3449a249f5ce20ecebbd459b15365a34804824d6 platforms/vinted.mjs
a2ea3f5f56dba8bd4d23fa54a26fd941567b4febdd1ed1b4e9f5bcc1c9786e53 public/android-chrome-192x192.png
46ea6ba4ca6ca2352f37353b8bc4ca258935d29ecb8d3c9b133a48ea75c0c58f public/android-chrome-512x512.png
cdf390c3ae338cc422ecd5338b3f50adb20fdbf4fe6193f2f4553c096efb2f10 public/app.js
d06f9ac5e795a2a1c246e89ec69b64d7c90cdf0d34d8d3507ec1950a523ce17c public/app.js
e0e7b8ef01c36f0625b6157aa46bd69de05c17be0b054208fb635d6507b52154 public/apple-touch-icon.png
a2ea3f5f56dba8bd4d23fa54a26fd941567b4febdd1ed1b4e9f5bcc1c9786e53 public/brand/favicons/android-chrome-192x192.png
46ea6ba4ca6ca2352f37353b8bc4ca258935d29ecb8d3c9b133a48ea75c0c58f public/brand/favicons/android-chrome-512x512.png
@@ -399,8 +396,8 @@ fb3dbcd03f7ed15b773b582c2a2b785c3040d3319aba5b7c682a7e74e1cf3b4e public/core/sa
4d69e2cb6b7f9f63b54d708a5d8b1ebefcdcb9f88a977053db5031c05b098464 public/design-system/tokens/source.json
4a9e65edeaee3421edc3dd4831230062494f1a42f8495f4cb979fde8664d06c4 public/design-system/tokens.css
15bf8dd655b34a8015447e834d387af12685238ede598f5f0e7927eea2933483 public/favicon.ico
6d90d0dc93416395caf6155e5efc13e3e87c9c44b828542983d9033bb7a49e08 public/index.html
85a655300980b6ec32b35faadec4b30b85cf45cd5799e8179050ac07a280917d public/login.html
b9a6d053dc1b664e515b06e532f454f34be9b82bd95d456218dc0e991b6afcce public/index.html
248b411391d654e1d87903cb36d65582acbb08a78dbafe156feaf335c1fbe434 public/login.html
e37558ee9612a36db8a19999fbb320f1c249a8e155f94c83a82926bec920f989 public/login.js
59291f75eca405e3856bafea76cc601d681060eb6b36bf82a4be3c3ab7eac290 public/mobile-upload-expired.html
f046f166d89fc32774b87deb02e6d144fd2f40c0925aac67487e5b7c489a0ec1 public/mobile-upload.css
@@ -413,23 +410,23 @@ bf10d5ccc53f287a8b61d488af50c3151c764f36a29ee5d4fe9ee321fa0eda06 public/modules
0edc61f0c2bb604c3adc40813b18fe5ca8d9b846ec2a3d9f406ae26ae313d3ee public/modules/theme-manager/theme-manager.js
e0a6c981d4231eab26d981be62647d0e6f89e05e8371baad121348202ad54f49 public/style.css
ff525e0af963e6f54636cfdc1bd2d045aaf82e7b0199755eb5cdf4c52110be2e public/vendoo-icons.js
9cf6628dd714347563fdc5925e323c0a274ee5967d78555757be85c56fcdc0b0 README.md
bfcb47b331d55689921a3839950af20e17e96a6c44eddad5c74a7e342f078aa8 README.md
f397e46da92394d34c2fc01715f037cca380703ecfa1d4a451de8ec7342e05b3 scripts/docker-backup.sh
86a411ae1fa5e36a11b0a8cb61e6c0169e0b80d3cedc9b7b82c3cfff23dd7390 scripts/docker-entrypoint.sh
1b58c1e5f762c4928d42605c01872f38f6d6bca92342260f3cd88a132e6af095 scripts/docker-restore.sh
1720c13fed290c08361266257279e86c8b5909c4b39d3729439335cbcc06a387 scripts/github-deploy.ps1
d4d41170106979e1be9266a569215ff484e80452f568f0bf8dd9cdc9978dbb7c scripts/github-deploy.sh
be7cfcdd337279294efca8e3c4541e8c60a0d638ae80fe3cfd9021c0d6c326e1 scripts/github-deploy.ps1
8f594c7cc7f0d3a00a4167a9e1a9f34173d13d73d08388da0e2c932bbd318302 scripts/github-deploy.sh
0a9bafc79c1c5d59fb9b51ebc34c00eec4a4e9a5e873ce1818f50f3ab983da70 scripts/install-local-flux.ps1
2924f4e8abe5ebbd099fe1ceb2bc6ceb4bdda55fa3e57dd299cfc05d310a9246 scripts/runtime-start-local-flux.ps1
7c6176d8311b1ba9d511beb40edd41b8248e61c298528bbec3f9ef1171856c05 scripts/runtime-stop-local-flux.ps1
1a5736cf5fccc991df122163977c2eac80281bd99a54a5680b3d7eae99e4b79d scripts/start-local-flux.bat
3bf3aa74f5cc1f2946bb2da637eb30fe8daec783a32f9721ebb0468e20df90bd scripts/uninstall-local-flux.ps1
327bb6e7b99939508b807d0c3c6d5617a2d8f0d5464ba0b1865d8e9e1ed82dde SECURITY.md
af7234a31ef7dca4545c8c071f3773739f856a173fdbf96d0e060cd95c7ade63 SECURITY.md
abb508872f30a0d6e4b6d33ab2f4cb14dea11838b15dbe502e6fac2ecc16cf13 server.mjs
eae7a799aaeec708ea28aca9fc6b08132909cf0b41d412e889064876ce4ba94b setup-gui.ps1
a8a56c5b7062f4a671317b53e50442b53bc641ff44f32155444b6b605ec263b4 setup-gui.ps1
cf4519ce5adc01f05458e3286e86f3a419cceaff5fcdd869a1d07f1c1d1a0d8c setup.bat
9433d663b6537ab5bb52a80733b18ac97e932ec50650b2d6bae8d67d5f05cdb4 setup.ps1
9cf6480f9eeda7fecc82a6bf1d9bfb3d94b75ca42c92e7697679b9efd8816215 setup.sh
eb20bcc9151a9ed4a039aaebc6f5776223313bd4281972735856aa202fdfc2f2 setup.ps1
94f028ca7249592af0fe583d72537afd2fe9cd8e87e6ecae806719293386bc4f setup.sh
2851c914a1b50a036cb265ddd32e5545cc7e8e34ba9d682445926acc23eb8f28 THIRD_PARTY_NOTICES.md
90485a16e3468cb9a6750646700fbb036cb9ac410e9b8bd5c582f7b923bf3c31 tools/build-release.mjs
94f4ced84834fceeb4f797bf8aab0703c873b57fd5d4a5b3fb02fee12e42bd2e tools/check-syntax.mjs
@@ -438,21 +435,17 @@ ac8440bce2659a035302609722f1dfed5d125223376cd575ffc0636397c94b54 tools/generate
e3d8c6e8dfd566acbe037ad744edbf60816152d4994698c2a9c46d2b890e246a tools/verify-capability-graph-1.40.2.mjs
8916ca58f8f67b7186d4b3644d13a56743965a57a50dc30bb33bd6baa8b2b646 tools/verify-capability-graph-1.41.0.mjs
e7cbaaa0ca1f0a655af500aea14044455a80cfae10246716a17488f96e99dae7 tools/verify-capability-graph-1.41.1.mjs
62b3e3c998759d59b78ae0a6609f51bf87369880ea1384b274c771d70d2a6476 tools/verify-capability-graph-1.41.2.mjs
85114eff887ba65ca7df00f43333414c44b3aadc16fbc2fe10dc4dd4f9aa7aed tools/verify-catalog-media-1.39.0.mjs
753288b83a655dbe4a9da8e93d8c3956ae17544699e2221873d479d72b977e2f tools/verify-catalog-media-1.40.0.mjs
f208ce5267103ab81c30da71e7b7c51c973b36dd617a1bc42c12063fbf1473f0 tools/verify-catalog-media-1.41.0.mjs
1c8da7af9bb5d743a60997fcba3345e568a862c97ff7102621134fa347a0056f tools/verify-catalog-media-1.41.1.mjs
b05890cd996c6dc28a47fb662026ac794bd096c23c3b1bb7a9fdc01224e8032e tools/verify-catalog-media-1.41.2.mjs
93d8fdee3feaf6e82ef15e7ac84bfd7e43ad13192f94655ccfce37f5a87b21ef tools/verify-catalog-services-1.39.0.mjs
60de35103ed4c4abf30445b4465b849f8be8842f1a4c22da6ae5d74943dba386 tools/verify-catalog-services-1.40.0.mjs
f89edc3b9d32c1618522c5810e53f25b47e72d52e8a041a2e7ae7939fbe169f2 tools/verify-catalog-services-1.41.0.mjs
122d1664ce9eff57331fd5f86554943494d46ba38e90f7e1a13c0de33ad6906d tools/verify-catalog-services-1.41.1.mjs
fb8ee4afa1d6abc09106ab032793da8dfef43bcb8614c7fe04086eb52cfb2a0b tools/verify-catalog-services-1.41.2.mjs
8f9ede5f112ee03323db3a6c73aee00b7037f5139dad241b11baf5f0e9cee4e0 tools/verify-config-store.mjs
e5a0614ef4f2ea73bdaaa1924cfafe86550a1a50c0105c569e405c332d8389a5 tools/verify-coolify-deployment-1.41.0.mjs
63afde37353f750f4bfe0b02324f774a7b3c7ece9339d85e5428b9a427c0d11e tools/verify-coolify-deployment-1.41.1.mjs
a66efb128565db5267c03a1503eb29d1f12024e1a357e1fd17d409430c2503f3 tools/verify-coolify-deployment-1.41.2.mjs
5752f6edb956696781da8bddb1c1848d26941fbed5a524691653c84c88c34bff tools/verify-db-migrations.py
3745de55a110d8254983b096c6afab196ddaec7f1153cd2f34c1e480b15aa33e tools/verify-deployment-1.32.0.mjs
44f6b349b722e5a83d4725c4879fa1a9383f59ab5634c49d3d7d9d7aa0cd5e12 tools/verify-deployment-1.33.0.mjs
@@ -469,17 +462,14 @@ b65eec7fd54e1459b363d99d20f9438ae5bc4b518e775de8b97e33e89a382e42 tools/verify-d
77f44ad4e13da47bcf4bc9c6a37d014c8b5f6c3e735ecfdb63830e09e43df296 tools/verify-deployment-1.40.2.mjs
9a33b8594744ed4c7774f12da5b14dce64049c1c0df65e98c9cd26d5404fda0c tools/verify-deployment-1.41.0.mjs
a885727e8847bf32ead7d7dc80e2e27deac62df53763a187717fcd8e6157a1f5 tools/verify-deployment-1.41.1.mjs
ca6e6baebc9489fd101d005f75c34c90b28e914443a11e3ad759513f653f4c82 tools/verify-deployment-1.41.2.mjs
c2d270efae9209d6fdf810f44db4cfdb9642db29060d5b75c2d920a859bc6918 tools/verify-esm-export-contracts.mjs
58c275a972393d7db7cab087523d3963a743e5a0d654fbc9b2cc09618abfea6c tools/verify-flux-loop-hotfix-1.40.1.mjs
58c275a972393d7db7cab087523d3963a743e5a0d654fbc9b2cc09618abfea6c tools/verify-flux-loop-hotfix-1.40.2.mjs
824ea8ed05fb68ef6bedba83e76697f9b03eb9830db981781e002c64f8b6858d tools/verify-flux-loop-hotfix-1.41.0.mjs
a055379e9ca3e7ebded24d4c75ccd54f8f81aa3618d63cdf41d79e3a17f126ac tools/verify-flux-loop-hotfix-1.41.1.mjs
c84f814c5e7dba7144709c6be860ab8ecbcb0541d167dca1e3172717196a327b tools/verify-flux-loop-hotfix-1.41.2.mjs
e06cda8d9850a84cde34a99009c51a18dbc1b7a66e45c82f35aadd631a71540b tools/verify-git-ignore-boundaries-1.41.2.mjs
d716f48436179c7f156b43a687b6149648fd38de12767da2be1bc63e3e1fce31 tools/verify-git-safety-regression.mjs
872cb8d1e0f86e2e571ddcce1adbb002d0a86f2d834f4ee7f601e011c7306e70 tools/verify-git-safety.mjs
ad2d1df5725da388ca427ddf6da0e0d62bf02999c6554e8c1ab918d63f5b8d50 tools/verify-git-staging.mjs
3e2eb9358b3a7060cf7d47d52dadb7744544a7682befb45822303cb96b2788ba tools/verify-git-staging.mjs
1d4351e4a827e718766a1677ddfbc3771a91942d0d5a93753e39f1b99c68a4dd tools/verify-hotfix-1.23.2.mjs
2f43dd1a89a36527cd3b4873b8cc9f7386cdfc06ade96fcc05dd5deb15dbcec7 tools/verify-identity-db-1.38.0.py
2f43dd1a89a36527cd3b4873b8cc9f7386cdfc06ade96fcc05dd5deb15dbcec7 tools/verify-identity-db-1.39.0.py
@@ -489,15 +479,12 @@ a1dddb35f4b6eeafe47ff3f40e5565bdb4477c8f63101d3c0d7f84c7393abe4c tools/verify-i
ff839066b02121a19046f19093904550e1184785e73dd2d079ab518cc74583ff tools/verify-identity-settings-1.40.0.mjs
3725fdbb20e93dd0bcbf3ad385bbb0c459cdd68448371757e549543b007155cc tools/verify-identity-settings-1.41.0.mjs
3dc54d074497913255774603f043e1154e2ae865ca62d7d4d682826cb02906c2 tools/verify-identity-settings-1.41.1.mjs
6d1d890a184a4cb4e1ae9edf784a0df250563e7287f6d3ee5fc43c89913b95c9 tools/verify-identity-settings-1.41.2.mjs
9492911fac3771e3b906b5a583e4bbf136ec8383b5fc23b0478f58b7407f7c65 tools/verify-initial-disabled-modules-1.41.0.mjs
932e2359c3ee00924e1a89cdcdabe44ddf1a60479f75f4d4094f2102d1c12949 tools/verify-initial-disabled-modules-1.41.1.mjs
745cb929519db756ba23e3198b4c92ff9e91173bd03d821bc2e37280af0a32b7 tools/verify-initial-disabled-modules-1.41.2.mjs
44c95844422be3deaad6054ee3f0cced9c25986815d976e0c64b042d0c2c5e1e tools/verify-installer-shell.sh
33ca957bf8acd3cc73354ef4c8d5e4b61c44810ecd8a8994ff89305cf1b9429c tools/verify-module-manager-1.40.0.mjs
2a80e837f754e7490f775956161e4fc8866c127c37b95d688baa239fee4cd883 tools/verify-module-manager-1.41.0.mjs
6b83edfaa57dd3633c7d100a8ed476ef63ae41384d215c787a2cbb6e608b22e5 tools/verify-module-manager-1.41.1.mjs
f4da6e0be42aa7eda0a496f4ab77547af6e3238a6d405aec00a61a9bb3622c1a tools/verify-module-manager-1.41.2.mjs
cab03e98687af87f359f6424d440fc0b644da97b3c614509b8c7d008d4b18a8d tools/verify-platform-architecture-1.35.0.mjs
732bb15431746a829e45ae55c695360cbf1dd21d4fc3be26a98f35708d571e84 tools/verify-platform-architecture-1.36.0.mjs
ee8ae7bed4e93a2ef278662552a4ae5d751306aad7133a93f380bbf2a5d5a28c tools/verify-platform-architecture-1.37.0.mjs
@@ -507,7 +494,6 @@ c9e704c372cf6a973fcdb887b337ead6509fd03e78570e6d1c9e24b1d879fe6b tools/verify-p
8b1b0eda98f0d16a99b47b0cb7a9e550d40d3dd048abbec6695c11ceca96f259 tools/verify-platform-architecture-1.40.2.mjs
2a06e9f6e73b37f35d055d0dd911e9cb741fa1fdb2cdfc5d926a2635f817356c tools/verify-platform-architecture-1.41.0.mjs
957cc887090f8d9c3a477c532e1aca79d24a3725924cb6e5a934f121a155c0e4 tools/verify-platform-architecture-1.41.1.mjs
ef13554f4d1a16dc97e5c17af3a719a0bd25839dae0f0f28608a706f893010f4 tools/verify-platform-architecture-1.41.2.mjs
36e35fedb08923c22fd8551389d33432b66dbd4215d95c594ef29bfc790c98e0 tools/verify-release-1.25.0.mjs
89621694b7bad18b6f3f1ac1956032f64112c40d3edc069ca581dc813d57d628 tools/verify-release-1.25.1-static.mjs
c908f5186cf2d71ef83d5e649327ae19965568933acfb32057ca00d29cf890bc tools/verify-release-1.25.1.mjs
@@ -525,13 +511,11 @@ a415d294679adab8ea76b86235a522e73ad9dd9ea0e2348691b1de3191255b81 tools/verify-s
f5296410e7c56a924978569a8ae904a1de8d4e327cb833e34e4b49e0384f3892 tools/verify-security-foundation-1.40.0.mjs
cd21f37b51ca99312c624a3b353d9eb5bff7e3dbca774600ae429f468b1d6b3f tools/verify-security-foundation-1.41.0.mjs
a619c7ecff5febe8afa746125ba100c603598951dfc177562307614ecf509715 tools/verify-security-foundation-1.41.1.mjs
6610a5d5ea6e4debedcf582c5d9992914110526bc2c371d6b9c5472fb8083768 tools/verify-security-foundation-1.41.2.mjs
31a120622ce8bb8451014f2662366f81abd28d0f3be59adfe35d17e0ada1838f tools/verify-slice30-mock.mjs
20957a4a038b9afdb01a8c98b881644daf793d2a6bb778a4c8e04780687d00e4 tools/verify-slice31-image-editor.mjs
21061ed5dc88feef48e80efffd9a1054d38598dce5a5d3f9f6bde39b539b83f0 tools/verify-slice32-batch.mjs
0f05b1ac3b686cf535bcfb3e6f6df24a29c3429ade1b3c666c1b99abb1a7311b tools/verify-slice32-image-render.mjs
168e58bd08b657abf2c7a84c7d0210e8393720e3f2b665703b03d45646480b04 tools/verify-source-boundaries-1.41.1.mjs
4424e4abf9a1dddd138b0dcb91b6d63f646c3a0c8eaae484d1e9d5b34bb35d77 tools/verify-source-boundaries-1.41.2.mjs
c9aa6693aa93ff1f220a2ce3bc35ef714a0b18a53eae0f907879fc92c66c1e38 tools/verify-theme-manager-1.36.0.mjs
1e61c4b180aa09de3dae9e72aef313b09a6f422229f180984b69a8d226e7cd8d tools/verify-theme-manager-1.37.0.mjs
3257b31d80034c077b25d3f1b13f8378378772bc6512445aa5ac8e882e4be109 tools/verify-theme-manager-1.38.0.mjs
@@ -539,6 +523,5 @@ c9aa6693aa93ff1f220a2ce3bc35ef714a0b18a53eae0f907879fc92c66c1e38 tools/verify-t
072a20e27b80b911ace2c9c18d1bb0298f59345967b6ee8f3d6b9075c522a6ba tools/verify-theme-manager-1.40.0.mjs
251bc56d2d52cbac1e126e35ad5e0077affb2b5cde6b251ad3df6a39ce67d16d tools/verify-theme-manager-1.41.0.mjs
33a6f24d26cda4d2129501c4e826ed0dda4eb9e15a324bf823b2ef19540e31ef tools/verify-theme-manager-1.41.1.mjs
8d825f95253f96b182ac9322b20c8f32c189eedd2a8c366170791c7623acbb1f tools/verify-theme-manager-1.41.2.mjs
0f3f684019763a14f5fbaec42e42c680cf748d930d46cd607fc61eb413ca4553 tools/verify-ui-contracts-1.26.1.mjs
a7ba863085e196ba5629cfbc344fcf8a05f45406f6bc6c6b75b3f45bf48bfa46 update-manifest.json
5f599423e8bec8b88de8aaf1223be49f63fb53b4a090e9129b7592c4a6e5c4d7 update-manifest.json
+13 -3
View File
@@ -1,4 +1,14 @@
@echo off
rem Alter Dateiname bleibt als Kompatibilitaetsstarter erhalten.
call "%~dp0UPDATE_VENDOO.bat"
exit /b %ERRORLEVEL%
setlocal EnableExtensions
chcp 65001 >nul
title Vendoo GitHub Deploy Assistent
cd /d "%~dp0"
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -STA -File "%~dp0scripts\github-deploy.ps1"
set "EXITCODE=%ERRORLEVEL%"
if not "%EXITCODE%"=="0" (
echo.
echo [FEHLER] GitHub-Deployment wurde mit Exit-Code %EXITCODE% beendet.
echo.
pause
)
exit /b %EXITCODE%
Binary file not shown.
@@ -0,0 +1,15 @@
Vendoo Logo & Favicon Paket
Inhalt:
- logo/: Hauptlogo und transparente Varianten
- icons/: PNG-App-Icons und allgemeine Größen für Web/Mobile/Desktop
- favicons/: favicon.ico, PNG-Favicons, Apple/Android/Web-Manifest-Dateien
- social/: Markenübersicht / Guideline-Sheet
Wichtige Dateien:
- logo/vendoo-logo-transparent.png
- icons/vendoo-icon-512x512.png
- favicons/favicon.ico
- favicons/apple-touch-icon.png
- favicons/site.webmanifest
- favicons/html-snippet.txt
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#faf6f2</TileColor>
</tile>
</msapplication>
</browserconfig>
Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

@@ -0,0 +1,9 @@
<!-- Vendoo favicon package -->
<link rel="icon" type="image/x-icon" href="/favicons/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="48x48" href="/favicons/favicon-48x48.png">
<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png">
<link rel="manifest" href="/favicons/site.webmanifest">
<meta name="msapplication-config" content="/favicons/browserconfig.xml">
<meta name="theme-color" content="#FAF6F2">
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,19 @@
{
"name": "Vendoo",
"short_name": "Vendoo",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#FAF6F2",
"background_color": "#FAF6F2",
"display": "standalone"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

+14 -38
View File
@@ -1,30 +1,6 @@
# Vendoo 1.43.0
# Vendoo 1.41.1
Vendoo 1.43.0 erweitert die kontrollierte Produktionsgrundlage um modulabhängige Navigation, ein optionales Produktbild Studio, ein neu aufgebautes Listing Studio und einen grafischen Ein-Klick-Updater. Der Container bleibt unveränderlich: Vendoo führt weder `git pull` noch Docker-Befehle aus, sondern delegiert freigegebene Deployments nach einem verifizierten Vorab-Backup an Coolify.
## Release 1.43.0: Module-aware Navigation, Listing Studio & Updater UI
- deaktivierte Fachmodule verschwinden unmittelbar aus der linken Navigation
- Modulstatus wird vor modulgeschützten APIs geladen, damit deaktivierte Module den App-Start nicht blockieren
- Produktbild Studio als eigenes natives Modul mit `defaultEnabled: false`
- überarbeitete Seite **Neuer Artikel** mit theme-konformen Flächen, Abständen und responsivem Arbeitsbereich
- sticky Aktionsleiste: **Artikel generieren** und **Speichern** bleiben oben erreichbar
- grafischer `UPDATE_VENDOO.bat`-Ablauf mit Phase, Detailtext, Prozentfortschritt, Live-Protokoll und sicherer Webhook-Eingabe
## Release 1.42.0: Production Operations & Controlled Update Completion
- persistente Deployment-Aufträge und Ereignisse in SQLite
- idempotente Auslösung und Sperre gegen parallele Deployments
- verifiziertes Vorab-Backup als fester Bestandteil des Auftrags
- getrennte Zustände für Coolify-Annahme, laufendes Deployment, Readiness, Erfolg, Fehler und Rollbackbedarf
- Erfolg erst nach Readiness und bestätigter Zielversion beziehungsweise expliziter externer Bestätigung
- Produktionscheck für HTTPS, Cookies, Proxy, Hosts/Origins, persistente Pfade, SQLite, Backupziel, FLUX und Coolify-Konfiguration
- kein Docker-Socket, kein `git pull`, kein `npm install` und keine Selbstüberschreibung im Produktivcontainer
## Hotfix 1.41.2: native Module zuverlässig in Git
Runtimeordner werden in `.gitignore` ausschließlich am Projektroot ausgeschlossen. Dadurch bleiben `app/modules/` und `app/modules/sessions/` normale, versionierte Quellordner. Der Deploy-Assistent prüft vor jedem Push, dass alle produktiven Moduldateien physisch vorhanden und durch Git getrackt sind.
Vendoo 1.41.1 ist die korrigierte Produktionsgrundlage für den Betrieb über **Coolify**, GitHub und `https://vendoo.flatlined.de`. Der Container bleibt unveränderlich: Vendoo führt weder `git pull` noch Docker-Befehle aus, sondern delegiert freigegebene Deployments nach einem verifizierten Vorab-Backup an Coolify.
## CI-Hotfix 1.41.1
@@ -44,16 +20,16 @@ Runtimeordner werden in `.gitignore` ausschließlich am Projektroot ausgeschloss
- keine Weitergabe des Docker-Sockets an Vendoo
- genaue Anleitung: `docs/COOLIFY_PRODUCTION_1_41_0.md`
## Kontrollierte Updates
## Kontrollierte Web-Updates
- Production Updater 3.3 mit SHA-256-geprüftem Payload und bytegeprüfter Manifest-Steuerdatei
- keine destruktive Leerung der geklonten Git-Baseline
- Pflichtdateien wie `db/schema.sql` werden vor Tests und Push geprüft
- Pull Request und vollständige GitHub-Checks auf Windows und Linux
- Coolify Auto Deploy nach erfolgreichem Merge nach `main`
- exakte Produktionsbestätigung über `/readyz` und Zielversion
- kein selbstverändernder Container, kein `git pull` in Produktion, kein Docker-Socket
- technische Details: `docs/PRODUCTION_UPDATER_3_1.md`
- geschütztes natives Modul `vendoo.deployments`
- Coolify-Deploy-Webhook oder minimal berechtigter API-Token
- Coolify-Zugangsdaten AES-256-GCM-verschlüsselt im Secret Store
- exakte Administratorbestätigung `DEPLOY`
- verpflichtendes Vorab-Backup vor dem Deploymentauftrag
- GitHub-Repository, Produktionsbranch und Update-Kanal im Update Center
- kein selbstverändernder Container, kein `git pull`, kein Docker-Socket
- vollständiger Ablauf: `docs/WEB_UPDATE_CENTER_1_41_0.md`
## GitHub-, Merge- und Releaseprozess
@@ -67,10 +43,10 @@ Runtimeordner werden in `.gitignore` ausschließlich am Projektroot ausgeschloss
## Plattformstand
- 18 registrierte Module
- 16 native Module, davon Produktbild Studio standardmäßig deaktiviert
- 17 registrierte Module
- 15 native Module
- 2 transparente Legacy Bridges: System und Operations
- 41 vollständig aufgelöste Capabilities
- 38 vollständig aufgelöste Capabilities
- geschützte Kernmodule und kontrollierbare optionale Module
- `.vmod`-Import/Export, Quarantäne und Abhängigkeitssteuerung
- Theme-, Security-, Identity-, Catalog-, Media-, Publishing-, AI-, FLUX- und Quality-Module bleiben enthalten
-5
View File
@@ -144,8 +144,3 @@ Die Weboberfläche verwendet eine erzwungene Content Security Policy ohne Inline
- Antworten externer Deploymentdienste werden begrenzt und Redirects nicht automatisch verfolgt
- der Initialwert für deaktivierte Module überschreibt keine spätere persistente Administratorentscheidung
## Git-Ignore-Grenzen
Lokale Runtimeordner werden ausschließlich als Root-Pfade (`/modules/`, `/sessions/`, `/logs/` usw.) ignoriert. Produktive Quellpfade unter `app/modules/` dürfen weder ignoriert noch ungetrackt sein; CI und Deploy-Assistent erzwingen diese Grenze.
-51
View File
@@ -1,51 +0,0 @@
@echo off
setlocal EnableExtensions DisableDelayedExpansion
cd /d "%~dp0"
if errorlevel 1 goto :path_error
set "VENDOO_PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe"
if not exist "%VENDOO_PS%" goto :powershell_missing
echo Vendoo Production Updater 3.4 preflight...
"%VENDOO_PS%" -NoLogo -NoProfile -ExecutionPolicy Bypass -File ".\scripts\updater\Vendoo.Updater.Preflight.ps1"
set "VENDOO_RC=%ERRORLEVEL%"
if not "%VENDOO_RC%"=="0" goto :preflight_failed
"%VENDOO_PS%" -NoLogo -NoProfile -ExecutionPolicy Bypass -STA -File ".\scripts\update-vendoo-gui.ps1"
set "VENDOO_RC=%ERRORLEVEL%"
if not "%VENDOO_RC%"=="0" goto :gui_failed
exit /b 0
:path_error
echo.
echo [ERROR] The Vendoo package directory could not be opened.
echo Path: %~dp0
echo.
pause
exit /b 30
:powershell_missing
echo.
echo [ERROR] Windows PowerShell 5.1 was not found.
echo Expected: %VENDOO_PS%
echo.
pause
exit /b 31
:preflight_failed
echo.
echo [ERROR] The Windows PowerShell preflight failed with exit code %VENDOO_RC%.
echo Log: %CD%\logs\updater-preflight.log
echo.
pause
exit /b %VENDOO_RC%
:gui_failed
echo.
echo [ERROR] The updater UI exited with code %VENDOO_RC%.
echo Log: %CD%\logs\updater-bootstrap.log
echo.
pause
exit /b %VENDOO_RC%
+15 -75
View File
@@ -4,91 +4,31 @@
"title": "Vendoo Module Manifest",
"type": "object",
"additionalProperties": false,
"required": [
"schemaVersion",
"id",
"name",
"version",
"type",
"status",
"requires",
"permissions",
"provides",
"consumes",
"owns"
],
"required": ["schemaVersion", "id", "name", "version", "type", "status", "requires", "permissions", "provides", "consumes", "owns"],
"properties": {
"schemaVersion": {
"const": 1
},
"id": {
"type": "string",
"pattern": "^vendoo\\.[a-z][a-z0-9-]*$"
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 80
},
"version": {
"type": "string",
"pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$"
},
"type": {
"enum": [
"core",
"feature",
"adapter",
"extension-host"
]
},
"status": {
"enum": [
"native",
"legacy-bridge",
"disabled"
]
},
"defaultEnabled": {
"type": "boolean",
"default": true
},
"description": {
"type": "string",
"maxLength": 500
},
"requires": {
"$ref": "#/$defs/moduleIds"
},
"permissions": {
"$ref": "#/$defs/capabilities"
},
"provides": {
"$ref": "#/$defs/capabilities"
},
"consumes": {
"$ref": "#/$defs/capabilities"
},
"owns": {
"$ref": "#/$defs/capabilities"
}
"schemaVersion": { "const": 1 },
"id": { "type": "string", "pattern": "^vendoo\\.[a-z][a-z0-9-]*$" },
"name": { "type": "string", "minLength": 1, "maxLength": 80 },
"version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$" },
"type": { "enum": ["core", "feature", "adapter", "extension-host"] },
"status": { "enum": ["native", "legacy-bridge", "disabled"] },
"description": { "type": "string", "maxLength": 500 },
"requires": { "$ref": "#/$defs/moduleIds" },
"permissions": { "$ref": "#/$defs/capabilities" },
"provides": { "$ref": "#/$defs/capabilities" },
"consumes": { "$ref": "#/$defs/capabilities" },
"owns": { "$ref": "#/$defs/capabilities" }
},
"$defs": {
"moduleIds": {
"type": "array",
"uniqueItems": true,
"items": {
"type": "string",
"pattern": "^vendoo\\.[a-z][a-z0-9-]*$"
}
"items": { "type": "string", "pattern": "^vendoo\\.[a-z][a-z0-9-]*$" }
},
"capabilities": {
"type": "array",
"uniqueItems": true,
"items": {
"type": "string",
"pattern": "^[a-z][a-z0-9-]*(?:\\.[a-z][a-z0-9-]*)+$"
}
"items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*(?:\\.[a-z][a-z0-9-]*)+$" }
}
}
}
-32
View File
@@ -296,38 +296,6 @@ export function validateAuthToken(token) {
return row;
}
export function listInvitations(limit = 100) {
const safeLimit = Math.max(1, Math.min(500, Number(limit) || 100));
return db.prepare(`SELECT id, email, role, used, expires_at, created_by, created_at,
CASE
WHEN used = 1 THEN 'revoked_or_used'
WHEN expires_at <= datetime('now') THEN 'expired'
ELSE 'pending'
END AS status
FROM auth_tokens
WHERE type = 'invite'
ORDER BY created_at DESC
LIMIT ?`).all(safeLimit);
}
export function getInvitation(id) {
return db.prepare(`SELECT id, email, role, used, expires_at, created_by, created_at,
CASE
WHEN used = 1 THEN 'revoked_or_used'
WHEN expires_at <= datetime('now') THEN 'expired'
ELSE 'pending'
END AS status
FROM auth_tokens WHERE id = ? AND type = 'invite'`).get(Number(id));
}
export function revokeInvitation(id) {
const invitation = getInvitation(id);
if (!invitation) return null;
if (invitation.status === 'pending') db.prepare('UPDATE auth_tokens SET used = 1 WHERE id = ? AND type = ?').run(Number(id), 'invite');
return { ...invitation, status: invitation.status === 'pending' ? 'revoked' : invitation.status, used: 1 };
}
// --- Rate Limiting ---
const rateLimits = new Map();
@@ -1,173 +0,0 @@
import { createHash, createPublicKey, verify as verifySignature } from 'crypto';
import { posix as pathPosix } from 'path';
import { PlatformError } from '../../kernel/errors.mjs';
import { validateModuleManifest } from '../../kernel/module-contract.mjs';
export const MODULE_PACKAGE_FORMAT = 'vendoo.module.package';
export const MODULE_PACKAGE_SCHEMA = 1;
export const MAX_PACKAGE_BYTES = 10 * 1024 * 1024;
export const MAX_FILE_COUNT = 200;
export const MAX_FILE_BYTES = 2 * 1024 * 1024;
function stable(value) {
if (Array.isArray(value)) return value.map(stable);
if (value && typeof value === 'object') {
return Object.keys(value).sort().reduce((result, key) => {
result[key] = stable(value[key]);
return result;
}, {});
}
return value;
}
export function canonicalJson(value) {
return JSON.stringify(stable(value));
}
function sha256(value) {
return createHash('sha256').update(value).digest('hex');
}
export function packageIntegrityPayload(modulePackage) {
return {
format: MODULE_PACKAGE_FORMAT,
schemaVersion: MODULE_PACKAGE_SCHEMA,
manifest: modulePackage.manifest,
runtime: modulePackage.runtime,
files: modulePackage.files.map(file => ({ path: file.path, sha256: file.sha256, size: file.size })),
};
}
export function calculatePackageDigest(modulePackage) {
return sha256(canonicalJson(packageIntegrityPayload(modulePackage)));
}
function safeRelativePath(value) {
const raw = String(value || '').replace(/\\/g, '/').trim();
const normalized = pathPosix.normalize(raw);
if (!raw || normalized === '.' || normalized.startsWith('../') || normalized.includes('/../') || normalized.startsWith('/') || /^[A-Za-z]:/.test(raw) || raw.includes('\0')) {
throw new PlatformError(`Unsicherer Moduldateipfad: ${raw || '(leer)'}`, {
code: 'MODULE_PACKAGE_PATH_INVALID', status: 400, expose: true,
});
}
return normalized;
}
function validateRuntime(runtime = {}) {
const mode = String(runtime.mode || 'declarative').trim();
if (!['builtin-reference', 'declarative', 'isolated'].includes(mode)) {
throw new PlatformError(`Nicht unterstützter Modul-Runtime-Modus: ${mode}`, { code: 'MODULE_RUNTIME_INVALID', status: 400, expose: true });
}
const entry = runtime.entry === undefined || runtime.entry === null || runtime.entry === '' ? null : safeRelativePath(runtime.entry);
if (mode === 'declarative' && entry) {
throw new PlatformError('Deklarative Module dürfen keinen ausführbaren Einstiegspunkt besitzen.', { code: 'MODULE_RUNTIME_EXECUTABLE_FORBIDDEN', status: 400, expose: true });
}
if (mode === 'isolated' && !entry) {
throw new PlatformError('Isolierte Module benötigen einen Einstiegspunkt.', { code: 'MODULE_RUNTIME_ENTRY_REQUIRED', status: 400, expose: true });
}
return Object.freeze({ mode, entry });
}
function validateFiles(files, runtime) {
if (!Array.isArray(files)) throw new PlatformError('Moduldateien fehlen.', { code: 'MODULE_PACKAGE_FILES_INVALID', status: 400, expose: true });
if (files.length > MAX_FILE_COUNT) throw new PlatformError('Das Modulpaket enthält zu viele Dateien.', { code: 'MODULE_PACKAGE_TOO_MANY_FILES', status: 400, expose: true });
const seen = new Set();
let total = 0;
const normalized = files.map(item => {
if (!item || typeof item !== 'object' || Array.isArray(item)) throw new PlatformError('Ungültiger Moduldateieintrag.', { code: 'MODULE_PACKAGE_FILE_INVALID', status: 400, expose: true });
const path = safeRelativePath(item.path);
if (seen.has(path)) throw new PlatformError(`Doppelte Moduldatei: ${path}`, { code: 'MODULE_PACKAGE_FILE_DUPLICATE', status: 400, expose: true });
seen.add(path);
const content = String(item.content || '');
let buffer;
try { buffer = Buffer.from(content, 'base64'); } catch { buffer = null; }
if (!buffer || buffer.length > MAX_FILE_BYTES) throw new PlatformError(`Moduldatei ist ungültig oder zu groß: ${path}`, { code: 'MODULE_PACKAGE_FILE_TOO_LARGE', status: 400, expose: true });
const digest = sha256(buffer);
if (!/^[a-f0-9]{64}$/.test(String(item.sha256 || '')) || digest !== String(item.sha256)) {
throw new PlatformError(`Prüfsumme stimmt nicht: ${path}`, { code: 'MODULE_PACKAGE_FILE_HASH_MISMATCH', status: 400, expose: true });
}
if (Number(item.size) !== buffer.length) throw new PlatformError(`Dateigröße stimmt nicht: ${path}`, { code: 'MODULE_PACKAGE_FILE_SIZE_MISMATCH', status: 400, expose: true });
total += buffer.length;
if (runtime.mode === 'declarative' && /\.(?:mjs|cjs|js|node|exe|dll|so|dylib|ps1|bat|cmd|sh)$/i.test(path)) {
throw new PlatformError(`Deklaratives Modul enthält ausführbaren Code: ${path}`, { code: 'MODULE_PACKAGE_EXECUTABLE_FORBIDDEN', status: 400, expose: true });
}
return Object.freeze({ path, sha256: digest, size: buffer.length, content });
});
if (total > MAX_PACKAGE_BYTES) throw new PlatformError('Das Modulpaket ist zu groß.', { code: 'MODULE_PACKAGE_TOO_LARGE', status: 400, expose: true });
if (runtime.entry && !seen.has(runtime.entry)) throw new PlatformError('Der Runtime-Einstiegspunkt fehlt im Paket.', { code: 'MODULE_RUNTIME_ENTRY_MISSING', status: 400, expose: true });
return Object.freeze(normalized);
}
function verifyOptionalSignature(modulePackage, trustKeys = {}) {
const signature = modulePackage.signature;
if (!signature) return { signed: false, trusted: false, keyId: null };
const algorithm = String(signature.algorithm || '').toLowerCase();
const keyId = String(signature.keyId || '').trim();
const value = String(signature.value || '').trim();
if (algorithm !== 'ed25519' || !keyId || !value) {
throw new PlatformError('Ungültige Modulsignatur.', { code: 'MODULE_SIGNATURE_INVALID', status: 400, expose: true });
}
const publicKey = trustKeys[keyId];
if (!publicKey) return { signed: true, trusted: false, keyId };
try {
const verified = verifySignature(null, Buffer.from(modulePackage.integrity.digest, 'hex'), createPublicKey(publicKey), Buffer.from(value, 'base64'));
if (!verified) throw new Error('Signaturprüfung fehlgeschlagen');
return { signed: true, trusted: true, keyId };
} catch (error) {
throw new PlatformError('Modulsignatur konnte nicht bestätigt werden.', { code: 'MODULE_SIGNATURE_REJECTED', status: 400, expose: true, cause: error });
}
}
export function validateModulePackage(input, { trustKeys = {}, allowBuiltinReference = false } = {}) {
const source = input && typeof input === 'object' && !Array.isArray(input) ? input : null;
if (!source) throw new PlatformError('Ungültiges Modulpaket.', { code: 'MODULE_PACKAGE_INVALID', status: 400, expose: true });
if (source.format !== MODULE_PACKAGE_FORMAT || Number(source.schemaVersion) !== MODULE_PACKAGE_SCHEMA) {
throw new PlatformError('Nicht unterstütztes Modulpaketformat.', { code: 'MODULE_PACKAGE_VERSION_UNSUPPORTED', status: 400, expose: true });
}
const manifest = validateModuleManifest(source.manifest);
const runtime = validateRuntime(source.runtime);
if (runtime.mode === 'builtin-reference' && !allowBuiltinReference) {
throw new PlatformError('Referenzexporte eingebauter Module können nicht installiert werden.', { code: 'MODULE_BUILTIN_REFERENCE_NOT_INSTALLABLE', status: 400, expose: true });
}
if (manifest.type !== 'extension-host' && runtime.mode !== 'builtin-reference') {
throw new PlatformError('Installierbare Fremdmodule müssen den Typ extension-host verwenden.', { code: 'MODULE_EXTERNAL_TYPE_REQUIRED', status: 400, expose: true });
}
const files = validateFiles(source.files || [], runtime);
const normalized = {
format: MODULE_PACKAGE_FORMAT,
schemaVersion: MODULE_PACKAGE_SCHEMA,
manifest,
runtime,
files,
createdAt: String(source.createdAt || new Date().toISOString()),
integrity: {
algorithm: String(source.integrity?.algorithm || '').toLowerCase(),
digest: String(source.integrity?.digest || '').toLowerCase(),
},
signature: source.signature || null,
};
if (normalized.integrity.algorithm !== 'sha256' || !/^[a-f0-9]{64}$/.test(normalized.integrity.digest)) {
throw new PlatformError('Paketintegrität fehlt oder ist ungültig.', { code: 'MODULE_PACKAGE_INTEGRITY_INVALID', status: 400, expose: true });
}
const expected = calculatePackageDigest(normalized);
if (expected !== normalized.integrity.digest) {
throw new PlatformError('Paketintegrität stimmt nicht.', { code: 'MODULE_PACKAGE_INTEGRITY_MISMATCH', status: 400, expose: true });
}
const trust = verifyOptionalSignature(normalized, trustKeys);
return Object.freeze({ ...normalized, trust: Object.freeze(trust) });
}
export function buildModulePackage({ manifest, runtime = { mode: 'builtin-reference', entry: null }, files = [], createdAt = new Date().toISOString() }) {
const normalizedFiles = files.map(file => {
const path = safeRelativePath(file.path);
const buffer = Buffer.isBuffer(file.content) ? file.content : Buffer.from(String(file.content || ''), 'utf8');
return { path, sha256: sha256(buffer), size: buffer.length, content: buffer.toString('base64') };
});
// Der Digest muss auf demselben normalisierten Manifest basieren wie die spätere
// Validierung. Sonst würden additive Manifest-Defaults (z. B. defaultEnabled)
// den Digest nachträglich verändern.
const normalizedManifest = validateModuleManifest(manifest);
const draft = { format: MODULE_PACKAGE_FORMAT, schemaVersion: MODULE_PACKAGE_SCHEMA, manifest: normalizedManifest, runtime, files: normalizedFiles, createdAt, integrity: { algorithm: 'sha256', digest: '' }, signature: null };
draft.integrity.digest = calculatePackageDigest(draft);
return validateModulePackage(draft, { allowBuiltinReference: true });
}
-112
View File
@@ -1,112 +0,0 @@
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'fs';
import { dirname, join, resolve, sep } from 'path';
import { PlatformError } from '../../kernel/errors.mjs';
import { validateModulePackage } from './module-package-contract.mjs';
function ensureInside(root, pathname) {
const base = resolve(root);
const target = resolve(pathname);
if (target !== base && !target.startsWith(`${base}${sep}`)) {
throw new PlatformError('Moduldatei liegt außerhalb des Modulspeichers.', { code: 'MODULE_STORAGE_ESCAPE', status: 400, expose: true });
}
return target;
}
export class ModulePackageStore {
#root;
#trustKeys;
constructor(root, { trustKeys = {} } = {}) {
this.#root = resolve(root);
this.#trustKeys = { ...trustKeys };
mkdirSync(this.#root, { recursive: true });
}
#moduleRoot(id) {
return ensureInside(this.#root, join(this.#root, id));
}
#packagePath(id) {
return join(this.#moduleRoot(id), 'package.vmod');
}
list() {
if (!existsSync(this.#root)) return [];
const result = [];
for (const entry of readdirSync(this.#root, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
try {
const packagePath = this.#packagePath(entry.name);
if (!existsSync(packagePath)) continue;
const parsed = JSON.parse(readFileSync(packagePath, 'utf8'));
const pkg = validateModulePackage(parsed, { trustKeys: this.#trustKeys });
result.push({
id: pkg.manifest.id,
manifest: pkg.manifest,
runtime: pkg.runtime,
trust: pkg.trust,
installedAt: String(parsed.installedAt || parsed.createdAt || ''),
activatable: pkg.runtime.mode === 'declarative' || (pkg.runtime.mode === 'isolated' && pkg.trust.trusted),
packagePath,
});
} catch (error) {
result.push({ id: entry.name, invalid: true, error: error?.message || String(error), activatable: false });
}
}
return result.sort((a, b) => String(a.id).localeCompare(String(b.id)));
}
get(id) {
const packagePath = this.#packagePath(id);
if (!existsSync(packagePath)) return null;
const parsed = JSON.parse(readFileSync(packagePath, 'utf8'));
return validateModulePackage(parsed, { trustKeys: this.#trustKeys });
}
install(input) {
const pkg = validateModulePackage(input, { trustKeys: this.#trustKeys });
const root = this.#moduleRoot(pkg.manifest.id);
if (existsSync(root)) throw new PlatformError('Dieses Fremdmodul ist bereits installiert.', { code: 'MODULE_ALREADY_INSTALLED', status: 409, expose: true });
const temporary = `${root}.${process.pid}.tmp`;
mkdirSync(temporary, { recursive: true });
try {
for (const file of pkg.files) {
const destination = ensureInside(temporary, join(temporary, file.path));
mkdirSync(dirname(destination), { recursive: true });
writeFileSync(destination, Buffer.from(file.content, 'base64'), { mode: 0o600 });
}
const persisted = { ...pkg, trust: undefined, installedAt: new Date().toISOString() };
writeFileSync(join(temporary, 'package.vmod'), `${JSON.stringify(persisted, null, 2)}\n`, { mode: 0o600 });
renameSync(temporary, root);
} catch (error) {
rmSync(temporary, { recursive: true, force: true });
throw error;
}
return this.describe(pkg.manifest.id);
}
remove(id) {
const root = this.#moduleRoot(id);
if (!existsSync(root)) return false;
rmSync(root, { recursive: true, force: true });
return true;
}
export(id) {
const packagePath = this.#packagePath(id);
if (!existsSync(packagePath)) throw new PlatformError('Installiertes Modulpaket nicht gefunden.', { code: 'MODULE_NOT_FOUND', status: 404, expose: true });
return readFileSync(packagePath);
}
describe(id) {
const pkg = this.get(id);
if (!pkg) return null;
return {
id: pkg.manifest.id,
manifest: pkg.manifest,
runtime: pkg.runtime,
trust: pkg.trust,
activatable: pkg.runtime.mode === 'declarative' || (pkg.runtime.mode === 'isolated' && pkg.trust.trusted),
};
}
}
-87
View File
@@ -1,87 +0,0 @@
import { mkdirSync, readFileSync, renameSync, writeFileSync, existsSync } from 'fs';
import { dirname } from 'path';
import { PlatformError } from '../../kernel/errors.mjs';
const SCHEMA_VERSION = 1;
function normalizeState(input = {}) {
const modules = {};
const source = input && typeof input === 'object' && !Array.isArray(input) ? input.modules : null;
if (source && typeof source === 'object' && !Array.isArray(source)) {
for (const [id, record] of Object.entries(source)) {
if (!/^vendoo\.[a-z][a-z0-9-]*$/.test(id)) continue;
if (!record || typeof record !== 'object' || Array.isArray(record)) continue;
modules[id] = {
enabled: record.enabled !== false,
source: String(record.source || 'admin'),
updatedAt: String(record.updatedAt || new Date(0).toISOString()),
};
}
}
return { schemaVersion: SCHEMA_VERSION, modules };
}
export class ModuleStateStore {
#path;
#state;
constructor(pathname) {
this.#path = pathname;
this.#state = this.#read();
}
#read() {
try {
if (!existsSync(this.#path)) return normalizeState();
const parsed = JSON.parse(readFileSync(this.#path, 'utf8'));
if (Number(parsed?.schemaVersion || 0) !== SCHEMA_VERSION) {
throw new Error(`Nicht unterstützte Modulstatus-Version: ${parsed?.schemaVersion}`);
}
return normalizeState(parsed);
} catch (error) {
throw new PlatformError('Der persistente Modulstatus konnte nicht gelesen werden.', {
code: 'MODULE_STATE_READ_FAILED',
cause: error,
details: { path: this.#path },
});
}
}
#persist() {
mkdirSync(dirname(this.#path), { recursive: true });
const temporary = `${this.#path}.${process.pid}.tmp`;
writeFileSync(temporary, `${JSON.stringify(this.#state, null, 2)}\n`, { mode: 0o600 });
renameSync(temporary, this.#path);
}
has(id) { return Boolean(this.#state.modules[id]); }
isEnabled(id, fallback = true) {
const record = this.#state.modules[id];
return record ? record.enabled !== false : Boolean(fallback);
}
setEnabled(id, enabled, { source = 'admin' } = {}) {
if (!/^vendoo\.[a-z][a-z0-9-]*$/.test(String(id || ''))) {
throw new PlatformError('Ungültige Modul-ID für den Modulstatus.', { code: 'MODULE_ID_INVALID', status: 400, expose: true });
}
this.#state.modules[id] = {
enabled: Boolean(enabled),
source: String(source || 'admin').slice(0, 80),
updatedAt: new Date().toISOString(),
};
this.#persist();
return { id, ...this.#state.modules[id] };
}
remove(id) {
if (!this.#state.modules[id]) return false;
delete this.#state.modules[id];
this.#persist();
return true;
}
snapshot() {
return JSON.parse(JSON.stringify(this.#state));
}
}
-1
View File
@@ -29,7 +29,6 @@ export function validateModuleManifest(manifest) {
version: String(manifest.version || '').trim(),
type: String(manifest.type || '').trim(),
status: String(manifest.status || 'native').trim(),
defaultEnabled: manifest.defaultEnabled !== false,
description: String(manifest.description || '').trim(),
requires: Object.freeze(uniqueStrings(manifest.requires || [], 'requires', MODULE_ID)),
permissions: Object.freeze(uniqueStrings(manifest.permissions || [], 'permissions')),
+1 -2
View File
@@ -84,7 +84,6 @@ export function createPlatformKernel({ version, hasPermission, moduleTrustKeys =
features.define('modules.package-format', { defaultValue: true, owner: 'vendoo.module-manager', description: 'Aktiviert validierte .vmod-Pakete mit SHA-256-Integrität und optionaler Ed25519-Signatur.' });
features.define('publishing.native-module', { defaultValue: true, owner: 'vendoo.publishing', description: 'Aktiviert native Publishing-Verträge.' });
features.define('ai.native-module', { defaultValue: true, owner: 'vendoo.ai', description: 'Aktiviert native AI-Verträge.' });
features.define('product-images.optional-module', { defaultValue: false, owner: 'vendoo.product-image-tools', description: 'Aktiviert optionale Ghost-Mannequin- und Flatlay-Produktbildvarianten.' });
features.define('flux.native-module', { defaultValue: true, owner: 'vendoo.flux-studio', description: 'Aktiviert native FLUX-Verträge.' });
features.define('quality.native-module', { defaultValue: true, owner: 'vendoo.quality', description: 'Aktiviert native Quality-Verträge.' });
features.define('deployments.coolify', { defaultValue: true, owner: 'vendoo.deployments', description: 'Aktiviert kontrollierte Coolify-Deployments ohne Docker-Socket.' });
@@ -94,7 +93,7 @@ export function createPlatformKernel({ version, hasPermission, moduleTrustKeys =
.split(',').map(value => value.trim()).filter(Boolean));
for (const module of loadBuiltInModules()) {
const forcedEnabled = PROTECTED_MODULE_IDS.includes(module.manifest.id);
const defaultEnabled = module.manifest.status !== 'disabled' && module.manifest.defaultEnabled !== false && !initialDisabledModules.has(module.manifest.id);
const defaultEnabled = module.manifest.status !== 'disabled' && !initialDisabledModules.has(module.manifest.id);
const enabled = forcedEnabled || moduleStateStore.isEnabled(module.manifest.id, defaultEnabled);
modules.register(module, { enabled });
}
-34
View File
@@ -1,34 +0,0 @@
import manifest from './module.json' with { type: 'json' };
let runtime = null;
let started = false;
export function configureAiModule(deps = {}) {
runtime = {
startModelJobs: deps.startModelJobs || (() => {}),
stopModelJobs: deps.stopModelJobs || (() => {}),
startBatchJobs: deps.startBatchJobs || (() => {}),
stopBatchJobs: deps.stopBatchJobs || (() => {}),
};
}
export function createAiModule() {
return {
manifest,
source: 'builtin',
lifecycle: {
async start() {
if (!runtime) throw new Error('AI-Modul ist nicht konfiguriert.');
runtime.startModelJobs();
runtime.startBatchJobs();
started = true;
},
async stop() {
runtime?.stopModelJobs();
runtime?.stopBatchJobs();
started = false;
},
async health() { return { ok: Boolean(runtime && started), native: true, workers: ['model-photos', 'image-batch'] }; },
},
};
}
-28
View File
@@ -1,28 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.ai",
"name": "AI Services",
"version": "1.0.0",
"type": "feature",
"status": "native",
"description": "Native AI-Services für Text-, Modellfoto- und Batch-Jobs mit kontrolliertem Worker-Lifecycle.",
"requires": [
"vendoo.security",
"vendoo.media"
],
"permissions": [
"generation.execute"
],
"provides": [
"ai.text",
"ai.images"
],
"consumes": [
"media.assets",
"security.policies"
],
"owns": [
"ai.providers",
"ai.jobs"
]
}
-32
View File
@@ -1,32 +0,0 @@
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { PlatformError } from '../../kernel/errors.mjs';
const here = dirname(fileURLToPath(import.meta.url));
const manifest = JSON.parse(readFileSync(join(here, 'module.json'), 'utf8'));
let service = null;
let setupTokenMatcher = () => false;
export function configureAuthModule({ setupTokenMatches }) {
if (typeof setupTokenMatches === 'function') setupTokenMatcher = setupTokenMatches;
}
export function getAuthService() {
if (!service) throw new PlatformError('Auth-Modul ist noch nicht gestartet.', { code: 'AUTH_MODULE_NOT_READY', status: 503, expose: true });
return service;
}
export function createAuthModule() {
return {
manifest,
lifecycle: {
async start() {
const { createAuthService } = await import('./service.mjs');
service = createAuthService({ setupTokenMatches: setupTokenMatcher });
},
async stop() { service = null; },
async health() { return getAuthService().health(); },
},
};
}
-14
View File
@@ -1,14 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.auth",
"name": "Authentication",
"version": "2.0.0",
"type": "core",
"status": "native",
"description": "Native Authentifizierung mit Setup, Login, Einladungen, Passwortregeln und sicheren Sitzungsübergaben.",
"requires": ["vendoo.security"],
"permissions": ["sessions.manage"],
"provides": ["auth.identity", "auth.tokens"],
"consumes": ["security.policies"],
"owns": ["auth.tokens", "auth.login-history"]
}
-67
View File
@@ -1,67 +0,0 @@
import { objectSchema } from '../../kernel/input-schema.mjs';
import { getAuthService } from './index.mjs';
const emailRule = { type: 'string', required: true, minLength: 3, maxLength: 254, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ };
export function registerPublicAuthRoutes({ app, routes, deps }) {
routes.register(app, {
id: 'auth.setup-status.read', method: 'GET', path: '/auth/setup-check', owner: 'vendoo.auth', policy: 'auth.public',
csrf: false, stability: 'stable',
handler: async (_req, res) => res.json(getAuthService().setupStatus({ mailConfigured: deps.isMailerConfigured(), setupTokenRequired: deps.setupTokenRequired })),
});
routes.register(app, {
id: 'auth.setup.create', method: 'POST', path: '/auth/setup', owner: 'vendoo.auth', policy: 'auth.public', csrf: false, stability: 'stable',
input: objectSchema({ email: emailRule, name: { type: 'string', maxLength: 120 }, password: { type: 'string', required: true, maxLength: 512, trim: false }, setup_token: { type: 'string', maxLength: 512, trim: false } }),
handler: async (req, res) => {
const result = getAuthService().setup({ ...req.validatedBody, setupToken: req.get('x-vendoo-setup-token') || req.validatedBody.setup_token, ip: deps.getClientIp(req), userAgent: req.get('user-agent') || '' });
deps.setSessionCookies(req, res, result.session, result.csrf);
res.json({ ok: true, redirect: '/' });
},
});
routes.register(app, {
id: 'auth.login.create', method: 'POST', path: '/auth/login', owner: 'vendoo.auth', policy: 'auth.public', csrf: false, stability: 'stable',
input: objectSchema({ email: emailRule, password: { type: 'string', required: true, maxLength: 512, trim: false } }),
handler: async (req, res) => {
const result = getAuthService().login({ ...req.validatedBody, ip: deps.getClientIp(req), userAgent: req.get('user-agent') || '' });
deps.setSessionCookies(req, res, result.session, result.csrf);
res.json({ ok: true, redirect: '/' });
},
});
routes.register(app, {
id: 'auth.invite.create', method: 'POST', path: '/auth/invite', owner: 'vendoo.auth', policy: 'auth.authenticated',
middleware: [deps.requireAuth, deps.csrfProtect], csrf: true, stability: 'stable',
input: objectSchema({ email: emailRule, name: { type: 'string', maxLength: 120 }, role: { type: 'string', enum: ['admin','manager','editor','publisher','viewer'] } }),
handler: async (req, res) => {
const { token } = getAuthService().createInvite({ email: req.validatedBody.email, role: req.validatedBody.role || 'editor', createdBy: req.user.id });
const result = await deps.sendMagicLink(req.validatedBody.email, token, `${req.protocol}://${req.get('host')}`, 'invite');
deps.auditLog(req.user.id, req.user.email, 'user.invited', 'user', req.validatedBody.email, `Rolle: ${req.validatedBody.role || 'editor'}`, deps.getClientIp(req));
res.json({ ok: true, message: 'Einladung gesendet!', consoleFallback: result.consoleFallback || false });
},
});
routes.register(app, {
id: 'auth.magic.redirect', method: 'GET', path: '/auth/magic', owner: 'vendoo.auth', policy: 'auth.public', csrf: false, stability: 'stable',
handler: async (req, res) => {
const token = String(req.query?.token || '');
if (!token) return res.redirect('/login.html?error=' + encodeURIComponent('Kein Token angegeben'));
res.redirect(`/login.html?invite=${encodeURIComponent(token)}`);
},
});
routes.register(app, {
id: 'auth.invite.accept', method: 'POST', path: '/auth/accept-invite', owner: 'vendoo.auth', policy: 'auth.public', csrf: false, stability: 'stable',
input: objectSchema({ token: { type: 'string', required: true, minLength: 16, maxLength: 512, trim: false }, password: { type: 'string', required: true, maxLength: 512, trim: false }, name: { type: 'string', maxLength: 120 } }),
handler: async (req, res) => {
const result = getAuthService().acceptInvite({ ...req.validatedBody, ip: deps.getClientIp(req), userAgent: req.get('user-agent') || '' });
deps.setSessionCookies(req, res, result.session, result.csrf);
res.json({ ok: true, redirect: '/' });
},
});
routes.register(app, {
id: 'auth.logout.create', method: 'POST', path: '/auth/logout', owner: 'vendoo.auth', policy: 'auth.authenticated',
middleware: [deps.requireAuth, deps.csrfProtect], csrf: true, stability: 'stable',
handler: async (req, res) => {
getAuthService().logout({ token: req.cookies?.auth_token, ip: deps.getClientIp(req) });
deps.clearSessionCookies(res);
res.json({ ok: true });
},
});
}
-100
View File
@@ -1,100 +0,0 @@
import {
auditLog, checkRateLimit, clearFailedLogins, createAuthToken, createSession, createUser,
generateCsrfToken, getUserByEmail, isSetupMode, isUserLocked, recordLogin,
registerFailedLogin, setPassword, updateUser, validateAuthToken, validatePasswordStrength, verifyPassword,
validateSession, destroySession,
} from '../../core/identity/identity-store.mjs';
import { PlatformError } from '../../kernel/errors.mjs';
function fail(message, status = 400, code = 'AUTH_FAILED', details = null) {
throw new PlatformError(message, { status, code, expose: true, details });
}
export function createAuthService({ setupTokenMatches }) {
return Object.freeze({
setupStatus({ mailConfigured, setupTokenRequired }) {
return { setupMode: isSetupMode(), mailConfigured: Boolean(mailConfigured), setupTokenRequired: Boolean(setupTokenRequired) };
},
setup({ email, name, password, setupToken, ip, userAgent }) {
if (!isSetupMode()) fail('Setup bereits abgeschlossen', 400, 'SETUP_COMPLETED');
if (!setupTokenMatches(setupToken)) fail('Ungültiger Einrichtungs-Code.', 403, 'SETUP_TOKEN_INVALID');
const passwordCheck = validatePasswordStrength(password);
if (!passwordCheck.valid) fail(`Passwort benötigt ${passwordCheck.problems.join(', ')}.`, 400, 'PASSWORD_WEAK');
try {
const user = createUser(email, name || email.split('@')[0], 'admin', null, password);
auditLog(user.id, email, 'user.created', 'user', String(user.id), 'Erster Admin (Setup)', ip);
const session = createSession(user.id, ip, userAgent);
recordLogin(user.id, ip, userAgent, true);
auditLog(user.id, email, 'login.success', null, null, 'via setup', ip);
return { user, session, csrf: generateCsrfToken() };
} catch (error) {
fail(error?.message || 'Setup fehlgeschlagen.', 500, 'SETUP_FAILED');
}
},
login({ email, password, ip, userAgent }) {
const rl = checkRateLimit(`login:${ip}`, 5, 15 * 60 * 1000);
if (!rl.allowed) fail(`Zu viele Versuche. Bitte warte ${rl.retryAfter} Sekunden.`, 429, 'LOGIN_RATE_LIMIT', { retry_after: rl.retryAfter });
const user = getUserByEmail(email);
if (user && isUserLocked(user)) {
auditLog(user.id, email, 'login.blocked_locked', null, null, user.locked_until, ip);
fail('Account nach mehreren Fehlversuchen vorübergehend gesperrt.', 423, 'ACCOUNT_LOCKED', { locked_until: user.locked_until });
}
if (!user || !verifyPassword(password, user.password_hash)) {
if (user) {
recordLogin(user.id, ip, userAgent, false);
const lockResult = registerFailedLogin(user.id);
if (lockResult?.locked) {
auditLog(user.id, email, 'login.account_locked', 'user', String(user.id), lockResult.lockedUntil, ip);
fail('Zu viele Fehlversuche. Account für 15 Minuten gesperrt.', 423, 'ACCOUNT_LOCKED', { locked_until: lockResult.lockedUntil });
}
}
auditLog(user?.id || null, email, 'login.failed', null, null, null, ip);
fail('E-Mail oder Passwort falsch', 401, 'LOGIN_INVALID');
}
if (!user.active) fail('Account deaktiviert', 403, 'ACCOUNT_DISABLED');
clearFailedLogins(user.id);
const session = createSession(user.id, ip, userAgent);
recordLogin(user.id, ip, userAgent, true);
auditLog(user.id, email, 'login.success', null, null, 'via password', ip);
return { user, session, csrf: generateCsrfToken() };
},
createInvite({ email, role, createdBy }) {
return createAuthToken(email, 'invite', role || 'editor', createdBy);
},
acceptInvite({ token, password, name, ip, userAgent }) {
const passwordCheck = validatePasswordStrength(password);
if (!passwordCheck.valid) fail(`Passwort benötigt ${passwordCheck.problems.join(', ')}.`, 400, 'PASSWORD_WEAK');
const authToken = validateAuthToken(token);
if (!authToken) fail('Link ungültig oder abgelaufen', 400, 'INVITE_INVALID');
let user = getUserByEmail(authToken.email);
if (!user) {
user = createUser(authToken.email, name || authToken.email.split('@')[0], authToken.role, authToken.created_by, password);
auditLog(user.id, user.email, 'user.activated', 'user', String(user.id), null, ip);
} else {
setPassword(user.id, password);
if (name) user = updateUser(user.id, { name });
}
if (!user.active) fail('Account deaktiviert', 403, 'ACCOUNT_DISABLED');
const session = createSession(user.id, ip, userAgent);
recordLogin(user.id, ip, userAgent, true);
auditLog(user.id, user.email, 'login.success', null, null, 'via invite', ip);
return { user, session, csrf: generateCsrfToken() };
},
logout({ token, ip }) {
if (!token) return { ok: true };
const session = validateSession(token);
if (session) auditLog(session.user_id, session.email, 'logout', null, null, null, ip);
destroySession(token);
return { ok: true };
},
health() {
return { ok: true, setup_mode: isSetupMode() };
},
});
}
-39
View File
@@ -1,39 +0,0 @@
import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { createAuthModule } from './auth/index.mjs';
import { createSecurityModule } from './security/index.mjs';
import { createSessionsModule } from './sessions/index.mjs';
import { createSettingsModule } from './settings/index.mjs';
import { createListingsModule } from './listings/index.mjs';
import { createInventoryModule } from './inventory/index.mjs';
import { createMediaModule } from './media/index.mjs';
import { createThemesModule } from './themes/index.mjs';
import { createUsersModule } from './users/index.mjs';
import { createModuleManagerModule } from './module-manager/index.mjs';
import { createAiModule } from './ai/index.mjs';
import { createFluxStudioModule } from './flux-studio/index.mjs';
import { createProductImageToolsModule } from './product-image-tools/index.mjs';
import { createQualityModule } from './quality/index.mjs';
import { createPublishingModule } from './publishing/index.mjs';
import { createDeploymentsModule } from './deployments/index.mjs';
const here = dirname(fileURLToPath(import.meta.url));
const legacyModuleNames = ['system', 'operations'];
function legacyModule(name) {
return {
manifest: JSON.parse(readFileSync(join(here, name, 'module.json'), 'utf8')),
lifecycle: {
async start() {}, async stop() {}, async health() { return { ok: true, bridge: 'legacy' }; },
},
};
}
export function loadBuiltInModules() {
return [
...legacyModuleNames.map(legacyModule),
createSecurityModule(), createAuthModule(), createSessionsModule(), createUsersModule(), createSettingsModule(), createThemesModule(),
createMediaModule(), createListingsModule(), createInventoryModule(), createAiModule(), createProductImageToolsModule(), createFluxStudioModule(), createQualityModule(), createPublishingModule(), createModuleManagerModule(), createDeploymentsModule(),
];
}
-28
View File
@@ -1,28 +0,0 @@
import manifest from './module.json' with { type: 'json' };
import { PlatformError } from '../../kernel/errors.mjs';
import { createDeploymentService } from './service.mjs';
let service = null;
let adapters = {};
export function configureDeploymentsModule(nextAdapters = {}) {
adapters = { ...adapters, ...nextAdapters };
}
export function getDeploymentService() {
if (!service) throw new PlatformError('Deployment-Modul ist noch nicht gestartet.', {
code: 'DEPLOYMENT_MODULE_NOT_READY', status: 503, expose: true,
});
return service;
}
export function createDeploymentsModule() {
return {
manifest,
lifecycle: {
async start() { service = createDeploymentService(adapters); },
async stop() { service?.stop?.(); service = null; },
async health() { return getDeploymentService().health(); },
},
};
}
-38
View File
@@ -1,38 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.deployments",
"name": "Deployments & Updates",
"version": "1.1.0",
"type": "core",
"status": "native",
"description": "Persistente, kontrollierte Coolify-Deployments mit Vorab-Backup, Idempotenz, Statusverfolgung, Readiness-, Zielversions- und Produktionschecks ohne Docker-Socket.",
"requires": [
"vendoo.security",
"vendoo.settings",
"vendoo.operations",
"vendoo.module-manager"
],
"permissions": [
"updates.manage"
],
"provides": [
"deployments.control",
"deployments.coolify",
"deployments.production-checks",
"deployments.state-machine",
"updates.web"
],
"consumes": [
"security.secrets",
"settings.configuration",
"operations.backup",
"modules.management"
],
"owns": [
"deployments.configuration",
"deployments.events",
"deployments.history",
"deployments.production-checks",
"updates.coolify"
]
}
-192
View File
@@ -1,192 +0,0 @@
import { randomUUID } from 'node:crypto';
import { db } from '../../../lib/db.mjs';
import { PlatformError } from '../../kernel/errors.mjs';
import {
ACTIVE_DEPLOYMENT_STATES,
DEPLOYMENT_STATES,
canTransitionDeployment,
isActiveDeploymentState,
} from './state-machine.mjs';
function parseJson(value, fallback) {
try { return value ? JSON.parse(value) : fallback; } catch { return fallback; }
}
function serializeJson(value, fallback) {
try { return JSON.stringify(value ?? fallback); } catch { return JSON.stringify(fallback); }
}
function normalizeRun(row) {
if (!row) return null;
return {
...row,
request: parseJson(row.request_json, {}),
result: parseJson(row.result_json, {}),
active: isActiveDeploymentState(row.state),
};
}
function fail(message, code = 'DEPLOYMENT_STATE_INVALID', status = 409, details = null) {
throw new PlatformError(message, { code, status, expose: true, details });
}
export function createDeploymentRepository(database = db) {
const insertRun = database.prepare(`INSERT INTO deployment_runs (
id, idempotency_key, state, provider, trigger_mode, current_version, target_version,
build_revision_before, requested_by, reason, github_repository, github_branch,
request_json, timeout_at, started_at, updated_at
) VALUES (?, ?, 'checking', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`);
const insertEvent = database.prepare(`INSERT INTO deployment_events
(run_id, state, event_type, message, details_json)
VALUES (?, ?, ?, ?, ?)`);
const getRunStmt = database.prepare('SELECT * FROM deployment_runs WHERE id = ?');
const getIdempotentStmt = database.prepare('SELECT * FROM deployment_runs WHERE idempotency_key = ?');
const getActiveStmt = database.prepare(`SELECT * FROM deployment_runs
WHERE state IN (${ACTIVE_DEPLOYMENT_STATES.map(() => '?').join(',')})
ORDER BY created_at DESC LIMIT 1`);
function addEvent(runId, state, eventType, message, details = {}) {
insertEvent.run(runId, state, String(eventType || 'state').slice(0, 80), String(message || '').slice(0, 1000), serializeJson(details, {}));
}
function createRun(input) {
const id = input.id || randomUUID();
const existing = getIdempotentStmt.get(input.idempotencyKey);
if (existing) return { run: normalizeRun(existing), reused: true };
const active = getActiveStmt.get(...ACTIVE_DEPLOYMENT_STATES);
if (active) fail('Es läuft bereits ein Deployment oder es ist ein Rollback erforderlich.', 'DEPLOYMENT_ALREADY_ACTIVE', 409, { run_id: active.id, state: active.state });
try {
insertRun.run(
id,
input.idempotencyKey,
input.provider || 'coolify',
input.triggerMode || 'webhook',
input.currentVersion,
input.targetVersion,
input.buildRevisionBefore || null,
input.requestedBy || null,
input.reason || 'manual',
input.githubRepository || null,
input.githubBranch || null,
serializeJson(input.request || {}, {}),
input.timeoutAt || null,
);
} catch (error) {
if (String(error?.message || '').includes('UNIQUE')) {
const raced = getIdempotentStmt.get(input.idempotencyKey);
if (raced) return { run: normalizeRun(raced), reused: true };
}
throw error;
}
addEvent(id, 'checking', 'run.created', 'Deployment-Auftrag wurde angelegt.', {
target_version: input.targetVersion,
trigger_mode: input.triggerMode,
});
return { run: normalizeRun(getRunStmt.get(id)), reused: false };
}
function getRun(id) { return normalizeRun(getRunStmt.get(id)); }
function findByIdempotencyKey(key) { return normalizeRun(getIdempotentStmt.get(key)); }
function getActiveRun() { return normalizeRun(getActiveStmt.get(...ACTIVE_DEPLOYMENT_STATES)); }
function listRuns(limit = 25) {
const safeLimit = Math.max(1, Math.min(200, Number(limit) || 25));
return database.prepare('SELECT * FROM deployment_runs ORDER BY created_at DESC LIMIT ?').all(safeLimit).map(normalizeRun);
}
function listEvents(runId, limit = 200) {
const safeLimit = Math.max(1, Math.min(1000, Number(limit) || 200));
return database.prepare(`SELECT * FROM deployment_events WHERE run_id = ? ORDER BY id ASC LIMIT ?`).all(runId, safeLimit)
.map(row => ({ ...row, details: parseJson(row.details_json, {}) }));
}
function transition(runId, nextState, { message = '', eventType = 'state.changed', details = {}, patch = {} } = {}) {
if (!DEPLOYMENT_STATES.includes(nextState)) fail('Unbekannter Deployment-Zustand.', 'DEPLOYMENT_STATE_UNKNOWN', 500, { next_state: nextState });
const current = getRunStmt.get(runId);
if (!current) fail('Deployment-Auftrag wurde nicht gefunden.', 'DEPLOYMENT_RUN_NOT_FOUND', 404);
if (current.state === nextState) return normalizeRun(current);
if (!canTransitionDeployment(current.state, nextState)) {
fail(`Ungültiger Deployment-Übergang von ${current.state} nach ${nextState}.`, 'DEPLOYMENT_TRANSITION_BLOCKED', 409, {
run_id: runId, from: current.state, to: nextState,
});
}
const completed = ['succeeded', 'failed'].includes(nextState);
const columns = ['state = ?', "updated_at = datetime('now')"];
const values = [nextState];
const allowedPatch = new Set([
'deployment_id', 'backup_id', 'result_json', 'error_code', 'error_message',
'build_revision_after', 'timeout_at',
]);
for (const [key, value] of Object.entries(patch || {})) {
if (!allowedPatch.has(key)) continue;
columns.push(`${key} = ?`);
values.push(key.endsWith('_json') ? serializeJson(value, {}) : value);
}
if (completed) columns.push("completed_at = datetime('now')");
values.push(runId);
database.prepare(`UPDATE deployment_runs SET ${columns.join(', ')} WHERE id = ?`).run(...values);
addEvent(runId, nextState, eventType, message || `Zustand: ${nextState}`, details);
return normalizeRun(getRunStmt.get(runId));
}
function patchRun(runId, patch = {}, { eventType = null, message = '', details = {} } = {}) {
const current = getRunStmt.get(runId);
if (!current) fail('Deployment-Auftrag wurde nicht gefunden.', 'DEPLOYMENT_RUN_NOT_FOUND', 404);
const allowedPatch = new Set([
'deployment_id', 'backup_id', 'result_json', 'error_code', 'error_message',
'build_revision_after', 'timeout_at',
]);
const columns = ["updated_at = datetime('now')"];
const values = [];
for (const [key, value] of Object.entries(patch || {})) {
if (!allowedPatch.has(key)) continue;
columns.push(`${key} = ?`);
values.push(key.endsWith('_json') ? serializeJson(value, {}) : value);
}
values.push(runId);
database.prepare(`UPDATE deployment_runs SET ${columns.join(', ')} WHERE id = ?`).run(...values);
if (eventType) addEvent(runId, current.state, eventType, message || eventType, details);
return normalizeRun(getRunStmt.get(runId));
}
function recordBackupVerification({ backupId, runId = null, status, sha256 = null, fileSize = 0, sqliteIntegrity = null, manifest = {}, errors = [], warnings = [] }) {
const id = randomUUID();
database.prepare(`INSERT INTO backup_verifications
(id, backup_id, deployment_run_id, status, sha256, file_size, sqlite_integrity, manifest_json, errors_json, warnings_json, verified_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`).run(
id, backupId, runId, status, sha256, Number(fileSize) || 0, sqliteIntegrity,
serializeJson(manifest, {}), serializeJson(errors, []), serializeJson(warnings, []),
);
return database.prepare('SELECT * FROM backup_verifications WHERE id = ?').get(id);
}
function createProductionCheck({ createdBy = null, results = [] } = {}) {
const id = randomUUID();
const passed = results.filter(item => item.status === 'pass').length;
const warnings = results.filter(item => item.status === 'warning').length;
const failed = results.filter(item => item.status === 'fail').length;
const overall = failed ? 'failed' : (warnings ? 'warning' : 'passed');
database.prepare(`INSERT INTO production_checks
(id, overall_status, passed_count, warning_count, failed_count, results_json, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(id, overall, passed, warnings, failed, serializeJson(results, []), createdBy);
return { id, overall_status: overall, passed_count: passed, warning_count: warnings, failed_count: failed, results };
}
function latestProductionCheck() {
const row = database.prepare('SELECT * FROM production_checks ORDER BY created_at DESC LIMIT 1').get();
return row ? { ...row, results: parseJson(row.results_json, []) } : null;
}
return Object.freeze({
createRun, getRun, findByIdempotencyKey, getActiveRun, listRuns, listEvents,
transition, patchRun, addEvent, recordBackupVerification,
createProductionCheck, latestProductionCheck,
});
}
export const deploymentRepository = createDeploymentRepository();
-132
View File
@@ -1,132 +0,0 @@
import { objectSchema } from '../../kernel/input-schema.mjs';
import { PlatformError } from '../../kernel/errors.mjs';
import { getDeploymentService } from './index.mjs';
const settingsSchema = objectSchema({
provider: { type: 'string', enum: ['none', 'coolify'] },
coolify_url: { type: 'string', maxLength: 1000 },
resource_uuid: { type: 'string', maxLength: 120 },
trigger_mode: { type: 'string', enum: ['webhook', 'api'] },
deploy_method: { type: 'string', enum: ['GET', 'POST'] },
deploy_endpoint: { type: 'string', maxLength: 500 },
deployment_status_endpoint: { type: 'string', maxLength: 500 },
deployment_timeout_seconds: { type: 'number', integer: true, min: 60, max: 7200 },
health_poll_seconds: { type: 'number', integer: true, min: 5, max: 300 },
github_repository: { type: 'string', maxLength: 200 },
github_branch: { type: 'string', maxLength: 120 },
ghcr_image: { type: 'string', maxLength: 300 },
update_channel: { type: 'string', enum: ['stable', 'beta'] },
backup_before_deploy: { type: 'boolean' },
backup_uploads: { type: 'boolean' },
api_token: { type: 'string', maxLength: 16384, trim: false },
deploy_webhook_url: { type: 'string', maxLength: 2000, trim: false },
});
const triggerSchema = objectSchema({
force: { type: 'boolean' },
confirmation: { type: 'string', required: true, maxLength: 20 },
reason: { type: 'string', maxLength: 120 },
idempotency_key: { type: 'string', maxLength: 160 },
target_version: { type: 'string', maxLength: 40 },
});
const confirmSchema = objectSchema({
confirmation: { type: 'string', required: true, maxLength: 20 },
outcome: { type: 'string', required: true, enum: ['running', 'succeeded', 'failed'] },
message: { type: 'string', maxLength: 500 },
});
function requireRun(run) {
if (!run) throw new PlatformError('Deployment-Auftrag wurde nicht gefunden.', {
code: 'DEPLOYMENT_RUN_NOT_FOUND', status: 404, expose: true,
});
return run;
}
export function registerDeploymentRoutes({ app, routes, deps }) {
routes.register(app, {
id: 'deployments.status.read', method: 'GET', path: '/api/admin/deployment', owner: 'vendoo.deployments',
policy: 'updates.manage', audit: 'deployments.status.read', rateLimit: 'administration', csrf: false, stability: 'stable',
handler: async (_req, res) => res.json(await getDeploymentService().status()),
});
routes.register(app, {
id: 'deployments.settings.update', method: 'PUT', path: '/api/admin/deployment', owner: 'vendoo.deployments',
policy: 'updates.manage', audit: 'deployments.settings.updated', rateLimit: 'administration', csrf: true, stability: 'stable', input: settingsSchema,
handler: async (req, res) => {
const result = getDeploymentService().update(req.validatedBody || {});
deps.auditLog(req.user.id, req.user.email, 'deployment.settings.updated', 'deployment', 'coolify', null, deps.getClientIp(req));
res.json(result);
},
});
routes.register(app, {
id: 'deployments.connection.test', method: 'POST', path: '/api/admin/deployment/test', owner: 'vendoo.deployments',
policy: 'updates.manage', audit: 'deployments.connection.tested', rateLimit: 'administration', csrf: true, stability: 'stable',
handler: async (req, res) => {
const result = await getDeploymentService().testConnection();
deps.auditLog(req.user.id, req.user.email, 'deployment.connection.tested', 'deployment', 'coolify', JSON.stringify({ ok: result.ok }), deps.getClientIp(req));
res.json(result);
},
});
routes.register(app, {
id: 'deployments.updates.check', method: 'POST', path: '/api/admin/deployment/updates/check', owner: 'vendoo.deployments',
policy: 'updates.manage', audit: 'deployments.updates.checked', rateLimit: 'administration', csrf: true, stability: 'stable',
handler: async (req, res) => {
const result = await getDeploymentService().checkUpdates();
deps.auditLog(req.user.id, req.user.email, 'deployment.updates.checked', 'deployment', 'manifest', JSON.stringify({ update_available: result.update_available || false }), deps.getClientIp(req));
res.json(result);
},
});
routes.register(app, {
id: 'deployments.trigger', method: 'POST', path: '/api/admin/deployment/trigger', owner: 'vendoo.deployments',
policy: 'updates.manage', audit: 'deployments.triggered', rateLimit: 'administration', csrf: true, stability: 'stable', input: triggerSchema,
handler: async (req, res) => {
const idempotencyKey = req.get('Idempotency-Key') || req.validatedBody?.idempotency_key || '';
const result = await getDeploymentService().trigger(req.validatedBody || {}, { userId: req.user.id, idempotencyKey });
deps.auditLog(req.user.id, req.user.email, 'deployment.triggered', 'deployment', result.id, JSON.stringify({ backup_id: result.backup_id, branch: result.github_branch, target_version: result.target_version, reused: result.reused }), deps.getClientIp(req));
res.status(result.reused ? 200 : 202).json(result);
},
});
routes.register(app, {
id: 'deployments.runs.list', method: 'GET', path: '/api/admin/deployment/runs', owner: 'vendoo.deployments',
policy: 'updates.manage', audit: 'deployments.runs.read', rateLimit: 'administration', csrf: false, stability: 'stable',
handler: async (req, res) => res.json({ runs: getDeploymentService().listRuns(Number(req.query.limit) || 20) }),
});
routes.register(app, {
id: 'deployments.runs.read', method: 'GET', path: '/api/admin/deployment/runs/:id', owner: 'vendoo.deployments',
policy: 'updates.manage', audit: 'deployments.run.read', rateLimit: 'administration', csrf: false, stability: 'stable',
handler: async (req, res) => res.json(requireRun(getDeploymentService().getRun(req.params.id))),
});
routes.register(app, {
id: 'deployments.runs.refresh', method: 'POST', path: '/api/admin/deployment/runs/:id/refresh', owner: 'vendoo.deployments',
policy: 'updates.manage', audit: 'deployments.run.refreshed', rateLimit: 'administration', csrf: true, stability: 'stable',
handler: async (req, res) => {
requireRun(getDeploymentService().getRun(req.params.id));
const result = await getDeploymentService().refreshRun(req.params.id);
deps.auditLog(req.user.id, req.user.email, 'deployment.run.refreshed', 'deployment', req.params.id, JSON.stringify({ state: result.state }), deps.getClientIp(req));
res.json(result);
},
});
routes.register(app, {
id: 'deployments.runs.confirm', method: 'POST', path: '/api/admin/deployment/runs/:id/confirm', owner: 'vendoo.deployments',
policy: 'updates.manage', audit: 'deployments.run.confirmed', rateLimit: 'administration', csrf: true, stability: 'stable', input: confirmSchema,
handler: async (req, res) => {
const result = await getDeploymentService().confirmExternal(req.params.id, req.validatedBody || {});
deps.auditLog(req.user.id, req.user.email, 'deployment.run.confirmed', 'deployment', req.params.id, JSON.stringify({ outcome: req.validatedBody?.outcome, state: result.state }), deps.getClientIp(req));
res.json(result);
},
});
routes.register(app, {
id: 'deployments.production.check.run', method: 'POST', path: '/api/admin/deployment/production-check', owner: 'vendoo.deployments',
policy: 'updates.manage', audit: 'deployments.production.checked', rateLimit: 'administration', csrf: true, stability: 'stable',
handler: async (req, res) => {
const result = await getDeploymentService().runProductionCheck({ userId: req.user.id });
deps.auditLog(req.user.id, req.user.email, 'deployment.production.checked', 'deployment', result.id, JSON.stringify({ overall_status: result.overall_status, failed_count: result.failed_count }), deps.getClientIp(req));
res.json(result);
},
});
routes.register(app, {
id: 'deployments.production.check.read', method: 'GET', path: '/api/admin/deployment/production-check', owner: 'vendoo.deployments',
policy: 'updates.manage', audit: 'deployments.production.check.read', rateLimit: 'administration', csrf: false, stability: 'stable',
handler: async (_req, res) => res.json({ check: getDeploymentService().latestProductionCheck() }),
});
}
-695
View File
@@ -1,695 +0,0 @@
import { randomUUID } from 'node:crypto';
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { readRuntimeConfig, setConfigValue, writeRuntimeConfig } from '../../../lib/config-store.mjs';
import { getSecret, getSecretMetadata, setSecret } from '../../../lib/secret-store.mjs';
import {
APP_ROOT, APP_VERSION, BACKUP_DIR, DATA_ROOT, DB_PATH, LOG_DIR,
MODULES_DIR, MODULE_STATE_PATH, OPERATIONS_DIR, UPLOADS_DIR,
runtimePathSummary, verifyRuntimeWritable,
} from '../../../lib/runtime-paths.mjs';
import { db } from '../../../lib/db.mjs';
import { PlatformError } from '../../kernel/errors.mjs';
import { deploymentRepository } from './repository.mjs';
const PROVIDERS = new Set(['none', 'coolify']);
const MODES = new Set(['webhook', 'api']);
const CHANNELS = new Set(['stable', 'beta']);
const METHODS = new Set(['GET', 'POST']);
const TERMINAL_COOLIFY_SUCCESS = new Set(['finished', 'success', 'succeeded', 'completed', 'healthy']);
const TERMINAL_COOLIFY_FAILURE = new Set(['failed', 'error', 'cancelled', 'canceled', 'stopped']);
const MASKED_SECRET = '••••••••';
function fail(message, code = 'DEPLOYMENT_CONFIGURATION_INVALID', status = 400, details = null) {
throw new PlatformError(message, { code, status, expose: true, details });
}
function clean(value, max = 1000) {
const text = String(value ?? '').trim();
if (text.length > max) fail('Deployment-Einstellung ist zu lang.');
if (/\r|\n|\0/.test(text)) fail('Deployment-Einstellung enthält unzulässige Steuerzeichen.');
return text;
}
function cleanUuid(value) {
const text = clean(value, 120);
if (text && !/^[a-zA-Z0-9_-]{6,120}$/.test(text)) fail('Coolify Resource UUID besitzt ein ungültiges Format.');
return text;
}
function cleanIdempotencyKey(value) {
const text = clean(value || randomUUID(), 160);
if (!/^[a-zA-Z0-9._:-]{8,160}$/.test(text)) fail('Idempotency-Key besitzt ein ungültiges Format.', 'IDEMPOTENCY_KEY_INVALID');
return text;
}
function normalizeBaseUrl(value) {
const text = clean(value, 1000).replace(/\/$/, '');
if (!text) return '';
let url;
try { url = new URL(text); } catch { fail('Coolify URL ist ungültig.'); }
if (!['https:', 'http:'].includes(url.protocol)) fail('Coolify URL muss HTTP oder HTTPS verwenden.');
if (url.username || url.password || url.search || url.hash) fail('Coolify URL darf keine Zugangsdaten, Query oder Fragment enthalten.');
if (process.env.NODE_ENV === 'production' && url.protocol !== 'https:' && !/^(1|true|yes)$/i.test(String(process.env.VENDOO_ALLOW_INSECURE_COOLIFY || 'false'))) {
fail('In Produktion muss die Coolify URL HTTPS verwenden.');
}
return url.toString().replace(/\/$/, '');
}
function normalizeWebhookUrl(value) {
const text = clean(value, 2000);
if (!text) return '';
let url;
try { url = new URL(text); } catch { fail('Coolify Deploy-Webhook ist ungültig.'); }
if (!['https:', 'http:'].includes(url.protocol)) fail('Coolify Deploy-Webhook muss HTTP oder HTTPS verwenden.');
if (url.username || url.password || url.hash) fail('Coolify Deploy-Webhook darf keine URL-Zugangsdaten oder Fragmente enthalten.');
if (process.env.NODE_ENV === 'production' && url.protocol !== 'https:' && !/^(1|true|yes)$/i.test(String(process.env.VENDOO_ALLOW_INSECURE_COOLIFY || 'false'))) {
fail('In Produktion muss der Coolify Deploy-Webhook HTTPS verwenden.');
}
return url.toString();
}
function normalizeEndpoint(value, fallback, placeholders = []) {
const text = clean(value || fallback, 500);
if (!text.startsWith('/') || text.startsWith('//') || text.includes('..')) fail('Coolify API-Endpunkt muss ein sicherer relativer Pfad sein.');
if (/\r|\n|\0/.test(text)) fail('Coolify API-Endpunkt ist ungültig.');
const unknown = [...text.matchAll(/\{([^}]+)\}/g)].map(match => match[1]).filter(name => !placeholders.includes(name));
if (unknown.length) fail(`Unbekannter Platzhalter im Coolify-Endpunkt: ${unknown.join(', ')}`);
return text;
}
function boolEnv(name, fallback = false) {
const raw = process.env[name];
if (raw === undefined || raw === '') return fallback;
return /^(1|true|yes|on)$/i.test(String(raw));
}
function intEnv(name, fallback, min, max) {
const value = Number(process.env[name]);
return Number.isFinite(value) ? Math.max(min, Math.min(max, Math.trunc(value))) : fallback;
}
function buildRevision() {
return clean(process.env.VENDOO_BUILD_REVISION || process.env.GIT_COMMIT || process.env.SOURCE_COMMIT || '', 160) || null;
}
function currentSettings() {
const provider = PROVIDERS.has(process.env.VENDOO_DEPLOY_PROVIDER) ? process.env.VENDOO_DEPLOY_PROVIDER : 'none';
const mode = MODES.has(process.env.COOLIFY_TRIGGER_MODE) ? process.env.COOLIFY_TRIGGER_MODE : 'webhook';
const channel = CHANNELS.has(process.env.VENDOO_UPDATE_CHANNEL) ? process.env.VENDOO_UPDATE_CHANNEL : 'stable';
const method = METHODS.has(process.env.COOLIFY_DEPLOY_METHOD) ? process.env.COOLIFY_DEPLOY_METHOD : 'GET';
return {
provider,
coolify_url: String(process.env.COOLIFY_API_URL || '').replace(/\/$/, ''),
resource_uuid: String(process.env.COOLIFY_RESOURCE_UUID || ''),
trigger_mode: mode,
deploy_method: method,
deploy_endpoint: String(process.env.COOLIFY_DEPLOY_ENDPOINT || '/api/v1/deploy?uuid={uuid}&force={force}'),
deployment_status_endpoint: String(process.env.COOLIFY_DEPLOYMENT_STATUS_ENDPOINT || '/api/v1/deployments/{deployment_id}'),
github_repository: String(process.env.VENDOO_GITHUB_REPOSITORY || 'Masterluke77/vendoo'),
github_branch: String(process.env.VENDOO_GITHUB_BRANCH || 'main'),
ghcr_image: String(process.env.VENDOO_GHCR_IMAGE || 'ghcr.io/masterluke77/vendoo'),
update_channel: channel,
backup_before_deploy: !/^(0|false|no)$/i.test(String(process.env.VENDOO_BACKUP_BEFORE_DEPLOY || 'true')),
backup_uploads: boolEnv('VENDOO_DEPLOY_BACKUP_UPLOADS', false),
deployment_timeout_seconds: intEnv('VENDOO_DEPLOY_TIMEOUT_SECONDS', 900, 60, 7200),
health_poll_seconds: intEnv('VENDOO_DEPLOY_HEALTH_POLL_SECONDS', 15, 5, 300),
api_token: getSecretMetadata('COOLIFY_API_TOKEN'),
deploy_webhook: getSecretMetadata('COOLIFY_DEPLOY_WEBHOOK_URL'),
};
}
function configuredState(settings) {
return settings.provider === 'coolify' && (
(settings.trigger_mode === 'webhook' && settings.deploy_webhook.configured) ||
(settings.trigger_mode === 'api' && settings.coolify_url && settings.resource_uuid && settings.api_token.configured)
);
}
function publicSettings(repository = deploymentRepository) {
const settings = currentSettings();
return {
...settings,
configured: configuredState(settings),
api_token: { configured: settings.api_token.configured, masked: settings.api_token.masked, provider: settings.api_token.provider },
deploy_webhook: { configured: settings.deploy_webhook.configured, masked: settings.deploy_webhook.masked, provider: settings.deploy_webhook.provider },
current_version: APP_VERSION,
build_revision: buildRevision(),
active_run: repository.getActiveRun(),
recent_runs: repository.listRuns(10).map(run => ({ ...run, events: repository.listEvents(run.id, 50) })),
docker_socket_access: false,
};
}
function buildApiUrl(settings, template, replacements) {
let path = template;
for (const [name, value] of Object.entries(replacements)) path = path.replaceAll(`{${name}}`, encodeURIComponent(String(value ?? '')));
return `${settings.coolify_url}${path}`;
}
function buildApiDeployUrl(settings, force) {
const endpoint = normalizeEndpoint(settings.deploy_endpoint, '/api/v1/deploy?uuid={uuid}&force={force}', ['uuid', 'force']);
return buildApiUrl(settings, endpoint, { uuid: settings.resource_uuid, force: force ? 'true' : 'false' });
}
function buildApiStatusUrl(settings, deploymentId) {
const endpoint = normalizeEndpoint(settings.deployment_status_endpoint, '/api/v1/deployments/{deployment_id}', ['deployment_id']);
return buildApiUrl(settings, endpoint, { deployment_id: deploymentId });
}
async function fetchWithTimeout(fetchImpl, url, options = {}, timeoutMs = 15000) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try { return await fetchImpl(url, { ...options, signal: controller.signal, redirect: 'error' }); }
catch (error) {
if (error?.name === 'AbortError') fail('Anfrage hat das Zeitlimit überschritten.', 'REQUEST_TIMEOUT', 504);
fail(`Ziel ist nicht erreichbar: ${error.message}`, 'REMOTE_UNREACHABLE', 502);
} finally { clearTimeout(timeout); }
}
async function readSafeResponse(response) {
const text = (await response.text()).slice(0, 64 * 1024);
try { return text ? JSON.parse(text) : null; } catch { return text || null; }
}
function redact(value, depth = 0) {
if (depth > 8) return '[depth-limit]';
if (Array.isArray(value)) return value.slice(0, 100).map(item => redact(item, depth + 1));
if (!value || typeof value !== 'object') return typeof value === 'string' ? value.slice(0, 4000) : value;
const result = {};
for (const [key, item] of Object.entries(value)) {
if (/token|secret|password|authorization|webhook|credential|key/i.test(key)) result[key] = '[redacted]';
else result[key] = redact(item, depth + 1);
}
return result;
}
function extractDeploymentId(value, depth = 0) {
if (depth > 6 || value == null) return null;
if (Array.isArray(value)) {
for (const item of value) { const found = extractDeploymentId(item, depth + 1); if (found) return found; }
return null;
}
if (typeof value !== 'object') return null;
for (const key of ['deployment_id', 'deployment_uuid', 'deploymentUuid', 'uuid', 'id']) {
const candidate = value[key];
if (candidate && /^[a-zA-Z0-9_-]{4,160}$/.test(String(candidate))) return String(candidate);
}
for (const item of Object.values(value)) { const found = extractDeploymentId(item, depth + 1); if (found) return found; }
return null;
}
function extractRemoteStatus(value) {
if (!value || typeof value !== 'object') return '';
const candidates = [value.status, value.state, value.deployment_status, value.data?.status, value.data?.state];
return String(candidates.find(Boolean) || '').trim().toLowerCase();
}
function semver(value) {
const match = String(value || '').trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
if (!match) return null;
return { raw: String(value).replace(/^v/, ''), major: +match[1], minor: +match[2], patch: +match[3], prerelease: match[4] || '' };
}
function compareVersions(a, b) {
const left = semver(a); const right = semver(b);
if (!left || !right) fail('Update-Manifest enthält eine ungültige SemVer-Version.', 'UPDATE_VERSION_INVALID', 502);
for (const key of ['major', 'minor', 'patch']) if (left[key] !== right[key]) return left[key] > right[key] ? 1 : -1;
if (left.prerelease === right.prerelease) return 0;
if (!left.prerelease) return 1;
if (!right.prerelease) return -1;
return left.prerelease.localeCompare(right.prerelease, undefined, { numeric: true });
}
function validateUpdateResult(result, channel) {
if (!result?.configured) return { ...result, current_version: APP_VERSION, channel };
const latest = String(result.latest_version || result.version || result.manifest?.version || '');
const parsed = semver(latest);
if (!parsed) fail('Update-Manifest enthält keine gültige SemVer-Version.', 'UPDATE_VERSION_INVALID', 502);
if (channel === 'stable' && parsed.prerelease) fail('Der Stable-Kanal akzeptiert keine Vorabversion.', 'UPDATE_CHANNEL_MISMATCH', 502);
const manifest = result.manifest && typeof result.manifest === 'object' ? result.manifest : {};
const manifestChannel = String(manifest.channel || channel).toLowerCase();
if (!CHANNELS.has(manifestChannel) || (channel === 'stable' && manifestChannel !== 'stable')) {
fail('Update-Manifest gehört nicht zum konfigurierten Update-Kanal.', 'UPDATE_CHANNEL_MISMATCH', 502);
}
const updateAvailable = compareVersions(latest, APP_VERSION) > 0;
const sha256 = String(manifest.sha256 || manifest.package_sha256 || result.sha256 || '').trim().toLowerCase();
if (updateAvailable && !/^[a-f0-9]{64}$/.test(sha256)) fail('Update-Manifest enthält keine gültige SHA-256-Prüfsumme.', 'UPDATE_CHECKSUM_MISSING', 502);
const downloadUrl = String(manifest.download_url || manifest.package_url || result.download_url || '').trim();
if (downloadUrl) {
let url;
try { url = new URL(downloadUrl); } catch { fail('Update-Manifest enthält eine ungültige Paket-URL.', 'UPDATE_URL_INVALID', 502); }
if (url.protocol !== 'https:') fail('Update-Pakete müssen über HTTPS bereitgestellt werden.', 'UPDATE_URL_INSECURE', 502);
}
return {
...result,
configured: true,
current_version: APP_VERSION,
latest_version: parsed.raw,
update_available: updateAvailable,
channel,
manifest: { ...manifest, sha256, download_url: downloadUrl || '' },
};
}
function directorySize(root, maxEntries = 100000) {
let bytes = 0; let files = 0; let truncated = false;
const queue = [root];
while (queue.length) {
const current = queue.pop();
if (!existsSync(current)) continue;
let entries;
try { entries = readdirSync(current, { withFileTypes: true }); } catch { continue; }
for (const entry of entries) {
if (++files > maxEntries) { truncated = true; queue.length = 0; break; }
const absolute = join(current, entry.name);
try {
if (entry.isDirectory()) queue.push(absolute);
else if (entry.isFile()) bytes += statSync(absolute).size;
} catch {}
}
}
return { path: root, bytes, files, truncated };
}
function fluxStatus(getPlatformSnapshot) {
try {
const module = getPlatformSnapshot?.()?.modules?.find(item => item.id === 'vendoo.flux-studio');
if (module) return { enabled: Boolean(module.enabled), state: module.state, source: 'platform-kernel' };
} catch {}
try {
if (existsSync(MODULE_STATE_PATH)) {
const state = JSON.parse(readFileSync(MODULE_STATE_PATH, 'utf8'));
const record = state?.modules?.['vendoo.flux-studio'];
if (record) return { enabled: record.enabled !== false, state: record.enabled === false ? 'disabled' : 'configured', source: 'module-state' };
}
} catch {}
const initiallyDisabled = String(process.env.VENDOO_INITIAL_DISABLED_MODULES || '').split(',').map(item => item.trim()).includes('vendoo.flux-studio');
return { enabled: !initiallyDisabled, state: initiallyDisabled ? 'disabled' : 'default-enabled', source: 'environment-default' };
}
function internalReadiness() {
const checks = [];
try {
const value = db.prepare('SELECT 1 AS ok').get();
checks.push({ name: 'database', ok: Number(value?.ok) === 1 });
} catch (error) { checks.push({ name: 'database', ok: false, error: error.message }); }
for (const item of verifyRuntimeWritable()) checks.push({ name: 'filesystem', ok: item.writable, path: item.path, error: item.error || null });
return { ok: checks.every(item => item.ok), checks };
}
async function probePublicReadiness(fetchImpl, timeoutMs = 8000) {
const base = String(process.env.PUBLIC_BASE_URL || `http://127.0.0.1:${process.env.PORT || 8124}`).replace(/\/$/, '');
const response = await fetchWithTimeout(fetchImpl, `${base}/readyz`, { headers: { Accept: 'application/json', 'User-Agent': `Vendoo/${APP_VERSION}` } }, timeoutMs);
const body = await readSafeResponse(response);
return { ok: response.ok && body?.ok !== false, http_status: response.status, version: body?.version || null, body: redact(body) };
}
function productionChecks({ getPlatformSnapshot } = {}) {
const results = [];
const add = (id, status, title, message, details = {}) => results.push({ id, status, title, message, details });
const publicBase = String(process.env.PUBLIC_BASE_URL || '').trim();
let publicUrl = null;
try { publicUrl = publicBase ? new URL(publicBase) : null; } catch {}
add('https', publicUrl?.protocol === 'https:' ? 'pass' : 'fail', 'Öffentliche HTTPS-URL', publicUrl?.protocol === 'https:' ? publicUrl.origin : 'PUBLIC_BASE_URL fehlt oder verwendet nicht HTTPS.');
const secureCookieMode = String(process.env.VENDOO_COOKIE_SECURE || 'auto').trim().toLowerCase();
const secureCookies = secureCookieMode === 'true' || (secureCookieMode === 'auto' && publicUrl?.protocol === 'https:');
add('secure-cookies', secureCookies ? 'pass' : 'fail', 'Sichere Cookies', secureCookies ? `Secure-Cookies sind aktiv (${secureCookieMode}).` : 'VENDOO_COOKIE_SECURE ist nicht aktiv oder Auto-Erkennung hat keine HTTPS-URL.');
add('trust-proxy', boolEnv('VENDOO_TRUST_PROXY', false) ? 'pass' : 'warning', 'Trust Proxy', boolEnv('VENDOO_TRUST_PROXY', false) ? 'Reverse-Proxy-Vertrauen ist aktiviert.' : 'VENDOO_TRUST_PROXY ist nicht aktiv.');
const expectedHost = publicUrl?.host || '';
const hosts = String(process.env.VENDOO_ALLOWED_HOSTS || '').split(',').map(item => item.trim()).filter(Boolean);
const origins = String(process.env.VENDOO_ALLOWED_ORIGINS || '').split(',').map(item => item.trim()).filter(Boolean);
add('allowed-host', expectedHost && hosts.includes(expectedHost) ? 'pass' : 'fail', 'Allowed Host', expectedHost && hosts.includes(expectedHost) ? expectedHost : 'Produktivhost fehlt in VENDOO_ALLOWED_HOSTS.', { expected: expectedHost, configured_count: hosts.length });
add('allowed-origin', publicUrl?.origin && origins.includes(publicUrl.origin) ? 'pass' : 'fail', 'Allowed Origin', publicUrl?.origin && origins.includes(publicUrl.origin) ? publicUrl.origin : 'Produktivorigin fehlt in VENDOO_ALLOWED_ORIGINS.', { expected: publicUrl?.origin || '', configured_count: origins.length });
const persistent = DATA_ROOT !== APP_ROOT && dirname(DB_PATH).startsWith(DATA_ROOT) && UPLOADS_DIR.startsWith(DATA_ROOT) && BACKUP_DIR.startsWith(DATA_ROOT);
add('persistent-paths', persistent ? 'pass' : 'fail', 'Persistente Pfade', persistent ? `Daten liegen unter ${DATA_ROOT}.` : 'Mindestens ein Laufzeitpfad liegt nicht im persistenten Datenbereich.', runtimePathSummary());
const writable = verifyRuntimeWritable();
add('writable', writable.every(item => item.writable) ? 'pass' : 'fail', 'Schreibrechte', writable.every(item => item.writable) ? 'Alle Laufzeitpfade sind beschreibbar.' : 'Mindestens ein Laufzeitpfad ist nicht beschreibbar.', { paths: writable });
let integrity = 'unknown';
try { integrity = String(db.pragma('integrity_check', { simple: true })); } catch (error) { integrity = error.message; }
add('sqlite-integrity', integrity === 'ok' ? 'pass' : 'fail', 'SQLite-Integrität', integrity === 'ok' ? 'SQLite meldet ok.' : `SQLite-Integritätsprüfung: ${integrity}`);
add('backup-target', existsSync(BACKUP_DIR) ? 'pass' : 'fail', 'Backupziel', existsSync(BACKUP_DIR) ? BACKUP_DIR : 'Backupziel fehlt.');
const flux = fluxStatus(getPlatformSnapshot);
add('flux-disabled', flux.enabled ? 'fail' : 'pass', 'FLUX Studio deaktiviert', flux.enabled ? 'FLUX Studio ist aktiv und widerspricht dem Produktions-Scope.' : 'FLUX Studio ist deaktiviert.', flux);
const settings = currentSettings();
add('deployment-secret', configuredState(settings) ? 'pass' : 'warning', 'Coolify-Zugang', configuredState(settings) ? 'Deployment-Zugang ist gesetzt; Secretwerte werden nicht angezeigt.' : 'Coolify-Deployment ist noch nicht vollständig konfiguriert.', {
mode: settings.trigger_mode,
api_token_configured: settings.api_token.configured,
webhook_configured: settings.deploy_webhook.configured,
});
const autoDeploy = String(process.env.VENDOO_COOLIFY_AUTO_DEPLOY || '').trim().toLowerCase();
add('auto-deploy', ['false', '0', 'off', 'no'].includes(autoDeploy) ? 'pass' : 'warning', 'Coolify Auto Deploy', ['false', '0', 'off', 'no'].includes(autoDeploy) ? 'Auto Deploy ist als deaktiviert dokumentiert.' : 'Vendoo kann den Coolify-Schalter nicht sicher auslesen. Auto Deploy manuell deaktiviert lassen.', { configured: autoDeploy || 'unknown' });
return results;
}
export function createDeploymentService(adapters = {}) {
const {
createBackup,
verifyStoredBackup,
checkForUpdates,
repository = deploymentRepository,
fetchImpl = globalThis.fetch,
getPlatformSnapshot,
} = adapters;
if (typeof createBackup !== 'function') fail('Backup-Adapter fehlt.', 'DEPLOYMENT_ADAPTER_MISSING', 500);
if (typeof verifyStoredBackup !== 'function') fail('Backup-Verifikationsadapter fehlt.', 'DEPLOYMENT_ADAPTER_MISSING', 500);
if (typeof fetchImpl !== 'function') fail('Fetch-Adapter fehlt.', 'DEPLOYMENT_ADAPTER_MISSING', 500);
let monitorTimer = null;
let monitoring = false;
async function checkUpdates() {
if (typeof checkForUpdates !== 'function') return { configured: false, current_version: APP_VERSION, message: 'Updateprüfung ist nicht verfügbar.' };
return validateUpdateResult(await checkForUpdates({ channel: currentSettings().update_channel }), currentSettings().update_channel);
}
async function testConnection() {
const settings = currentSettings();
if (settings.provider !== 'coolify') fail('Coolify ist nicht als Deployment-Provider aktiviert.');
if (!settings.coolify_url) fail('Coolify URL fehlt.');
const headers = { Accept: 'application/json', 'User-Agent': `Vendoo/${APP_VERSION}` };
if (settings.trigger_mode === 'api') {
const token = getSecret('COOLIFY_API_TOKEN');
if (!token) fail('Coolify API-Token fehlt.');
headers.Authorization = `Bearer ${token}`;
}
const response = await fetchWithTimeout(fetchImpl, `${settings.coolify_url}/api/health`, { headers }, 10000);
const body = await readSafeResponse(response);
if (!response.ok) fail(`Coolify Healthcheck antwortet mit HTTP ${response.status}.`, 'COOLIFY_HEALTH_FAILED', 502, { response: redact(body) });
return { ok: true, status: response.status, response: redact(body), url: settings.coolify_url };
}
async function requestCoolify(settings, force) {
let url;
const headers = { Accept: 'application/json', 'User-Agent': `Vendoo/${APP_VERSION}` };
const method = settings.deploy_method;
if (settings.trigger_mode === 'webhook') {
url = normalizeWebhookUrl(getSecret('COOLIFY_DEPLOY_WEBHOOK_URL'));
if (!url) fail('Coolify Deploy-Webhook ist nicht konfiguriert.');
} else {
const token = getSecret('COOLIFY_API_TOKEN');
if (!settings.coolify_url || !settings.resource_uuid || !token) fail('Coolify API URL, Resource UUID oder API Token fehlt.');
url = buildApiDeployUrl(settings, Boolean(force));
headers.Authorization = `Bearer ${token}`;
}
const response = await fetchWithTimeout(fetchImpl, url, { method, headers }, 20000);
const body = await readSafeResponse(response);
if (!response.ok) fail(`Coolify hat den Deployment-Auftrag mit HTTP ${response.status} abgewiesen.`, 'COOLIFY_DEPLOY_REJECTED', 502, { response: redact(body) });
return { method, http_status: response.status, body: redact(body), deployment_id: extractDeploymentId(body) };
}
async function trigger(input = {}, context = {}) {
if (input.confirmation !== 'DEPLOY') fail('Zur Bestätigung muss exakt DEPLOY übermittelt werden.', 'DEPLOYMENT_CONFIRMATION_REQUIRED');
const settings = currentSettings();
if (!configuredState(settings)) fail('Coolify ist nicht vollständig konfiguriert.', 'DEPLOYMENT_NOT_CONFIGURED');
const targetVersion = clean(input.target_version || APP_VERSION, 40).replace(/^v/, '');
if (!semver(targetVersion)) fail('Zielversion ist keine gültige SemVer-Version.', 'DEPLOYMENT_TARGET_VERSION_INVALID');
const idempotencyKey = cleanIdempotencyKey(input.idempotency_key || context.idempotencyKey);
const existing = repository.findByIdempotencyKey(idempotencyKey);
if (existing) return { ...existing, reused: true, events: repository.listEvents(existing.id) };
if (compareVersions(targetVersion, APP_VERSION) > 0) {
const update = await checkUpdates();
if (!update.configured || update.latest_version !== targetVersion || !update.update_available) {
fail('Zielversion ist nicht durch das freigegebene Update-Manifest bestätigt.', 'DEPLOYMENT_TARGET_NOT_APPROVED', 409, { target_version: targetVersion, update });
}
}
const timeoutAt = new Date(Date.now() + settings.deployment_timeout_seconds * 1000).toISOString();
const created = repository.createRun({
idempotencyKey,
provider: settings.provider,
triggerMode: settings.trigger_mode,
currentVersion: APP_VERSION,
targetVersion,
buildRevisionBefore: buildRevision(),
requestedBy: context.userId || null,
reason: clean(input.reason || 'manual', 120),
githubRepository: settings.github_repository,
githubBranch: settings.github_branch,
timeoutAt,
request: { force: Boolean(input.force), target_version: targetVersion, backup_required: settings.backup_before_deploy },
});
if (created.reused) return { ...created.run, reused: true, events: repository.listEvents(created.run.id) };
let run = created.run;
try {
if (settings.backup_before_deploy) {
run = repository.transition(run.id, 'backup_pending', { message: 'Vorab-Backup ist verpflichtend.' });
run = repository.transition(run.id, 'backup_running', { message: 'Vorab-Backup wird erstellt.' });
const backup = await createBackup({
kind: 'manual', includeUploads: settings.backup_uploads, includeConfig: true,
label: `pre-deploy-${APP_VERSION}-to-${targetVersion}`,
});
const verification = verifyStoredBackup(backup.id);
repository.recordBackupVerification({
backupId: backup.id,
runId: run.id,
status: verification.ok ? 'verified' : 'invalid',
sha256: backup.sha256,
fileSize: backup.file_size,
sqliteIntegrity: verification.ok ? 'ok' : 'failed',
manifest: verification.manifest || backup.manifest || {},
errors: verification.errors || [],
warnings: verification.warnings || [],
});
if (!verification.ok) fail(`Vorab-Backup ist nicht verifiziert: ${(verification.errors || []).join('; ')}`, 'DEPLOYMENT_BACKUP_INVALID', 409);
run = repository.transition(run.id, 'backup_verified', {
message: 'Vorab-Backup wurde erstellt und verifiziert.',
patch: { backup_id: backup.id },
details: { backup_id: backup.id, sha256: backup.sha256, file_size: backup.file_size },
});
}
run = repository.transition(run.id, 'deploy_requested', { message: 'Coolify-Deployment wird angefordert.' });
const remote = await requestCoolify(settings, Boolean(input.force));
run = repository.transition(run.id, settings.trigger_mode === 'api' ? 'deploy_running' : 'health_wait', {
message: settings.trigger_mode === 'api' ? 'Coolify hat den Deployment-Auftrag angenommen.' : 'Webhook wurde angenommen; externe Bestätigung oder neue Zielversion wird abgewartet.',
eventType: 'coolify.accepted',
patch: { deployment_id: remote.deployment_id || null, result_json: remote },
details: { http_status: remote.http_status, deployment_id: remote.deployment_id || null },
});
scheduleMonitor(1000);
return { ...run, reused: false, events: repository.listEvents(run.id), message: 'Deployment-Auftrag angenommen. Erfolg wird erst nach Status- und Readinessprüfung gemeldet.' };
} catch (error) {
const latest = repository.getRun(run.id);
if (latest && !['failed', 'succeeded'].includes(latest.state)) {
try {
repository.transition(run.id, 'failed', {
message: error.message || 'Deployment-Auftrag ist fehlgeschlagen.',
eventType: 'run.failed',
patch: { error_code: error.code || 'DEPLOYMENT_FAILED', error_message: String(error.message || error).slice(0, 2000) },
});
} catch {}
}
throw error;
}
}
async function pollCoolifyStatus(run, settings) {
if (settings.trigger_mode !== 'api' || !run.deployment_id || !settings.coolify_url) return { available: false };
const token = getSecret('COOLIFY_API_TOKEN');
if (!token) return { available: false, error: 'API-Token fehlt.' };
const url = buildApiStatusUrl(settings, run.deployment_id);
const response = await fetchWithTimeout(fetchImpl, url, {
headers: { Accept: 'application/json', Authorization: `Bearer ${token}`, 'User-Agent': `Vendoo/${APP_VERSION}` },
}, 10000);
const body = await readSafeResponse(response);
if (!response.ok) return { available: true, ok: false, http_status: response.status, body: redact(body) };
const status = extractRemoteStatus(body);
return { available: true, ok: true, http_status: response.status, status, body: redact(body) };
}
async function refreshRun(runId = null, { externalConfirmed = false } = {}) {
const run = runId ? repository.getRun(runId) : repository.getActiveRun();
if (!run) return { state: 'idle', message: 'Kein aktives Deployment.' };
if (['succeeded', 'failed', 'rollback_required'].includes(run.state)) return { ...run, events: repository.listEvents(run.id) };
if (run.timeout_at && Date.parse(run.timeout_at) <= Date.now()) {
const timedOut = repository.transition(run.id, 'rollback_required', {
message: 'Deployment-Zeitlimit überschritten. Rollback oder manuelle Prüfung erforderlich.',
eventType: 'run.timeout',
patch: { error_code: 'DEPLOYMENT_TIMEOUT', error_message: 'Zeitlimit überschritten.' },
});
return { ...timedOut, events: repository.listEvents(run.id) };
}
const settings = currentSettings();
let remote = { available: false };
try { remote = await pollCoolifyStatus(run, settings); }
catch (error) {
repository.patchRun(run.id, {}, { eventType: 'coolify.status_error', message: 'Coolify-Status konnte nicht gelesen werden.', details: { error: error.message } });
}
if (remote.status && TERMINAL_COOLIFY_FAILURE.has(remote.status)) {
const failed = repository.transition(run.id, 'rollback_required', {
message: `Coolify meldet den Fehlerstatus ${remote.status}.`,
eventType: 'coolify.failed',
patch: { result_json: remote, error_code: 'COOLIFY_DEPLOYMENT_FAILED', error_message: `Coolify-Status: ${remote.status}` },
});
return { ...failed, remote, events: repository.listEvents(run.id) };
}
if (run.state === 'deploy_running' && remote.available && remote.ok) {
repository.transition(run.id, 'health_wait', { message: 'Deploymentstatus wurde gelesen; Readiness und Zielversion werden geprüft.', eventType: 'health.wait', details: { remote_status: remote.status || 'unknown' } });
}
let readiness;
try { readiness = await probePublicReadiness(fetchImpl, 8000); }
catch (error) { readiness = { ok: false, error: error.message }; }
const targetMatches = readiness.ok && String(readiness.version || '').replace(/^v/, '') === String(run.target_version).replace(/^v/, '');
const remoteSucceeded = remote.available && remote.ok && TERMINAL_COOLIFY_SUCCESS.has(remote.status);
const persistedConfirmation = repository.listEvents(run.id).some(event => event.event_type === 'external.succeeded');
const confirmed = Boolean(externalConfirmed || persistedConfirmation);
const deploymentConfirmed = settings.trigger_mode === 'api' ? (remoteSucceeded || confirmed) : confirmed;
if (readiness.ok && targetMatches && deploymentConfirmed) {
const succeeded = repository.transition(run.id, 'succeeded', {
message: 'Deployment, Readiness und Zielversion wurden erfolgreich bestätigt.',
eventType: 'run.succeeded',
patch: { build_revision_after: buildRevision(), result_json: { remote, readiness } },
details: { target_version: run.target_version, readiness_version: readiness.version, remote_status: remote.status || null },
});
return { ...succeeded, remote, readiness, events: repository.listEvents(run.id) };
}
repository.patchRun(run.id, { result_json: { remote, readiness } }, {
eventType: 'health.pending',
message: readiness.ok ? 'Readiness ist grün, aber Zielversion oder externe Bestätigung fehlt.' : 'Readiness ist noch nicht grün.',
details: { readiness_ok: Boolean(readiness.ok), readiness_version: readiness.version || null, target_version: run.target_version, remote_status: remote.status || null, external_confirmation: confirmed, deployment_confirmed: deploymentConfirmed },
});
return { ...repository.getRun(run.id), remote, readiness, events: repository.listEvents(run.id) };
}
async function confirmExternal(runId, input = {}) {
if (input.confirmation !== 'CONFIRM') fail('Zur externen Bestätigung muss exakt CONFIRM übermittelt werden.', 'DEPLOYMENT_EXTERNAL_CONFIRMATION_REQUIRED');
const run = repository.getRun(runId);
if (!run) fail('Deployment-Auftrag wurde nicht gefunden.', 'DEPLOYMENT_RUN_NOT_FOUND', 404);
if (!['deploy_requested', 'deploy_running', 'health_wait', 'rollback_required'].includes(run.state)) fail('Dieser Deployment-Auftrag kann nicht extern bestätigt werden.', 'DEPLOYMENT_CONFIRMATION_STATE_INVALID', 409);
const outcome = clean(input.outcome || 'succeeded', 20);
if (outcome === 'failed') {
const nextState = run.state === 'rollback_required' ? 'failed' : 'rollback_required';
return repository.transition(run.id, nextState, {
message: clean(input.message || 'Externes Deployment wurde als fehlgeschlagen bestätigt.', 500),
eventType: 'external.failed',
patch: { error_code: 'EXTERNAL_DEPLOYMENT_FAILED', error_message: clean(input.message || 'Externe Fehlermeldung.', 1000) },
});
}
if (outcome === 'running') {
if (run.state === 'deploy_requested') repository.transition(run.id, 'deploy_running', { message: 'Externes System bestätigt laufendes Deployment.', eventType: 'external.running' });
return refreshRun(run.id);
}
if (outcome !== 'succeeded') fail('Externe Bestätigung muss running, succeeded oder failed verwenden.');
repository.addEvent(run.id, run.state, 'external.succeeded', clean(input.message || 'Externes System meldet Abschluss.', 500), {});
return refreshRun(run.id, { externalConfirmed: true });
}
function scheduleMonitor(delayMs = null) {
if (monitorTimer) clearTimeout(monitorTimer);
const interval = currentSettings().health_poll_seconds * 1000;
monitorTimer = setTimeout(async () => {
if (monitoring) return scheduleMonitor(interval);
monitoring = true;
try { await refreshRun(); } catch (error) { console.error('Deployment-Monitor:', error.message); }
finally {
monitoring = false;
const active = repository.getActiveRun();
if (active && active.state !== 'rollback_required') scheduleMonitor(interval);
}
}, delayMs ?? interval);
monitorTimer.unref?.();
}
async function status() {
const settings = publicSettings(repository);
const lastBackup = db.prepare(`SELECT id, kind, label, file_name, file_size, sha256, status, verified_at, created_at
FROM operation_backups WHERE status = 'verified' ORDER BY created_at DESC LIMIT 1`).get() || null;
return {
...settings,
production: {
version: APP_VERSION,
build_revision: buildRevision(),
deployment_provider: settings.provider,
domain: (() => { try { return new URL(String(process.env.PUBLIC_BASE_URL || '')).host; } catch { return ''; } })(),
public_base_url: String(process.env.PUBLIC_BASE_URL || ''),
readiness: internalReadiness(),
database: { path: DB_PATH, integrity: (() => { try { return db.pragma('integrity_check', { simple: true }); } catch (error) { return error.message; } })() },
storage: {
data: directorySize(DATA_ROOT), uploads: directorySize(UPLOADS_DIR), backups: directorySize(BACKUP_DIR),
operations: directorySize(OPERATIONS_DIR), logs: directorySize(LOG_DIR), modules: directorySize(MODULES_DIR),
},
last_backup: lastBackup,
last_deployment: repository.listRuns(1)[0] || null,
flux: fluxStatus(getPlatformSnapshot),
},
latest_production_check: repository.latestProductionCheck(),
};
}
function update(input = {}) {
const provider = clean(input.provider || 'none', 20);
const mode = clean(input.trigger_mode || 'webhook', 20);
const channel = clean(input.update_channel || 'stable', 20);
const method = clean(input.deploy_method || 'GET', 10).toUpperCase();
if (!PROVIDERS.has(provider)) fail('Unbekannter Deployment-Provider.');
if (!MODES.has(mode)) fail('Unbekannter Coolify Trigger-Modus.');
if (!CHANNELS.has(channel)) fail('Unbekannter Update-Kanal.');
if (!METHODS.has(method)) fail('Coolify Deploy-Methode muss GET oder POST sein.');
const values = {
VENDOO_DEPLOY_PROVIDER: provider,
COOLIFY_API_URL: normalizeBaseUrl(input.coolify_url || ''),
COOLIFY_RESOURCE_UUID: cleanUuid(input.resource_uuid || ''),
COOLIFY_TRIGGER_MODE: mode,
COOLIFY_DEPLOY_METHOD: method,
COOLIFY_DEPLOY_ENDPOINT: normalizeEndpoint(input.deploy_endpoint, '/api/v1/deploy?uuid={uuid}&force={force}', ['uuid', 'force']),
COOLIFY_DEPLOYMENT_STATUS_ENDPOINT: normalizeEndpoint(input.deployment_status_endpoint, '/api/v1/deployments/{deployment_id}', ['deployment_id']),
VENDOO_GITHUB_REPOSITORY: clean(input.github_repository || 'Masterluke77/vendoo', 200),
VENDOO_GITHUB_BRANCH: clean(input.github_branch || 'main', 120),
VENDOO_GHCR_IMAGE: clean(input.ghcr_image || 'ghcr.io/masterluke77/vendoo', 300),
VENDOO_UPDATE_CHANNEL: channel,
VENDOO_BACKUP_BEFORE_DEPLOY: input.backup_before_deploy === false ? 'false' : 'true',
VENDOO_DEPLOY_BACKUP_UPLOADS: input.backup_uploads === true ? 'true' : 'false',
VENDOO_DEPLOY_TIMEOUT_SECONDS: String(Math.max(60, Math.min(7200, Number(input.deployment_timeout_seconds) || 900))),
VENDOO_DEPLOY_HEALTH_POLL_SECONDS: String(Math.max(5, Math.min(300, Number(input.health_poll_seconds) || 15))),
};
let content = readRuntimeConfig();
for (const [key, value] of Object.entries(values)) {
content = setConfigValue(content, key, value);
process.env[key] = value;
}
writeRuntimeConfig(content);
if (input.api_token !== undefined && input.api_token !== '' && input.api_token !== MASKED_SECRET) {
setSecret('COOLIFY_API_TOKEN', clean(input.api_token, 16384), { source: 'deployment-ui' });
}
if (input.deploy_webhook_url !== undefined && input.deploy_webhook_url !== '' && input.deploy_webhook_url !== MASKED_SECRET) {
setSecret('COOLIFY_DEPLOY_WEBHOOK_URL', normalizeWebhookUrl(input.deploy_webhook_url), { source: 'deployment-ui' });
}
return publicSettings(repository);
}
function runProductionCheck(context = {}) {
const results = productionChecks({ getPlatformSnapshot });
return repository.createProductionCheck({ createdBy: context.userId || null, results });
}
scheduleMonitor(2500);
return Object.freeze({
status,
update,
testConnection,
trigger,
checkUpdates,
refreshRun,
confirmExternal,
listRuns(limit) { return repository.listRuns(limit).map(run => ({ ...run, events: repository.listEvents(run.id) })); },
getRun(id) { const run = repository.getRun(id); return run ? { ...run, events: repository.listEvents(id) } : null; },
runProductionCheck,
latestProductionCheck: repository.latestProductionCheck,
health() {
const settings = publicSettings(repository);
return { ok: true, provider: settings.provider, configured: settings.configured, state: settings.active_run?.state || 'idle', docker_socket_access: false };
},
stop() { if (monitorTimer) clearTimeout(monitorTimer); monitorTimer = null; },
});
}
-41
View File
@@ -1,41 +0,0 @@
export const DEPLOYMENT_STATES = Object.freeze([
'checking', 'backup_pending', 'backup_running', 'backup_verified',
'deploy_requested', 'deploy_running', 'health_wait',
'succeeded', 'failed', 'rollback_required',
]);
export const ACTIVE_DEPLOYMENT_STATES = Object.freeze([
'checking', 'backup_pending', 'backup_running', 'backup_verified',
'deploy_requested', 'deploy_running', 'health_wait', 'rollback_required',
]);
const TRANSITION_ENTRIES = Object.freeze({
checking: ['backup_pending', 'deploy_requested', 'failed'],
backup_pending: ['backup_running', 'failed'],
backup_running: ['backup_verified', 'failed'],
backup_verified: ['deploy_requested', 'failed'],
deploy_requested: ['deploy_running', 'health_wait', 'failed', 'rollback_required'],
deploy_running: ['health_wait', 'succeeded', 'failed', 'rollback_required'],
health_wait: ['succeeded', 'failed', 'rollback_required'],
rollback_required: ['failed'],
succeeded: [],
failed: [],
});
export const DEPLOYMENT_TRANSITIONS = Object.freeze(Object.fromEntries(
Object.entries(TRANSITION_ENTRIES).map(([state, targets]) => [state, Object.freeze([...targets])]),
));
const ACTIVE_SET = new Set(ACTIVE_DEPLOYMENT_STATES);
export function isDeploymentState(state) {
return DEPLOYMENT_STATES.includes(state);
}
export function isActiveDeploymentState(state) {
return ACTIVE_SET.has(state);
}
export function canTransitionDeployment(from, to) {
return from === to || Boolean(DEPLOYMENT_TRANSITIONS[from]?.includes(to));
}
-20
View File
@@ -1,20 +0,0 @@
import manifest from './module.json' with { type: 'json' };
let runtime = null;
let started = false;
export function configureFluxStudioModule(deps = {}) {
runtime = { start: deps.start || (() => {}), stop: deps.stop || (() => {}) };
}
export function createFluxStudioModule() {
return {
manifest,
source: 'builtin',
lifecycle: {
async start() { if (!runtime) throw new Error('FLUX-Modul ist nicht konfiguriert.'); runtime.start(); started = true; },
async stop() { runtime?.stop(); started = false; },
async health() { return { ok: Boolean(runtime && started), native: true, worker: 'flux-prompt' }; },
},
};
}
-25
View File
@@ -1,25 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.flux-studio",
"name": "FLUX Studio",
"version": "1.0.0",
"type": "feature",
"status": "native",
"description": "Native FLUX-Studio-Verträge und kontrollierter Prompt-Job-Worker.",
"requires": [
"vendoo.ai"
],
"permissions": [
"generation.execute"
],
"provides": [
"flux.prompt-jobs"
],
"consumes": [
"ai.images"
],
"owns": [
"flux.generations",
"flux.history"
]
}
-6
View File
@@ -1,6 +0,0 @@
import manifest from './module.json' with { type:'json' };
import { InventoryService } from './service.mjs';
let service = null;
export function configureInventoryModule({ repository }) { service = new InventoryService(repository); }
export function getInventoryService(){ if (!service) throw new Error('Inventory-Modul ist nicht konfiguriert.'); return service; }
export function createInventoryModule(){ return { manifest, lifecycle:{ async start(){}, async stop(){}, async health(){ return {ok:Boolean(service),native:true,contracts:['locations','bulk-update']}; } } }; }
-27
View File
@@ -1,27 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.inventory",
"name": "Inventory",
"version": "1.0.0",
"type": "feature",
"status": "native",
"description": "Native Lagerort- und Bestandsverwaltung mit begrenzten Bulk-Aktionen.",
"requires": [
"vendoo.auth",
"vendoo.listings"
],
"permissions": [
"listings.view",
"inventory.edit"
],
"provides": [
"inventory.stock"
],
"consumes": [
"auth.identity",
"listings.catalog"
],
"owns": [
"inventory.locations"
]
}
-2
View File
@@ -1,2 +0,0 @@
import { getStorageLocations, getListingsByLocation, getUnassignedListings, bulkUpdateLocation } from '../../../lib/db.mjs';
export const inventoryRepository = Object.freeze({ locations:getStorageLocations, byLocation:getListingsByLocation, unassigned:getUnassignedListings, bulkUpdateLocation });
-7
View File
@@ -1,7 +0,0 @@
import { getInventoryService } from './index.mjs';
export function registerInventoryRoutes({app,routes,deps}){ const service=getInventoryService();
routes.register(app,{id:'inventory.locations.list',method:'GET',path:'/api/inventory/locations',owner:'vendoo.inventory',policy:'inventory.view',csrf:false,stability:'stable',handler:async(_q,r)=>r.json(service.locations())});
routes.register(app,{id:'inventory.unassigned.list',method:'GET',path:'/api/inventory/unassigned',owner:'vendoo.inventory',policy:'inventory.view',csrf:false,stability:'stable',handler:async(_q,r)=>r.json(service.unassigned())});
routes.register(app,{id:'inventory.location.read',method:'GET',path:'/api/inventory/location/:loc',owner:'vendoo.inventory',policy:'inventory.view',csrf:false,stability:'stable',handler:async(q,r)=>r.json(service.byLocation(q.params.loc))});
routes.register(app,{id:'inventory.location.bulk-update',method:'PUT',path:'/api/inventory/bulk-location',owner:'vendoo.inventory',policy:'inventory.manage',stability:'stable',input:body=>service.validateBulk(body),handler:async(q,r)=>{const result=service.bulk(q.validatedBody);deps.auditLog(q.user.id,q.user.email,'inventory.location_bulk_updated','listing',null,JSON.stringify({count:result.updated,location:q.validatedBody.location}),deps.getClientIp(q));r.json(result);}});
}
-10
View File
@@ -1,10 +0,0 @@
import { PlatformError } from '../../kernel/errors.mjs';
function cleanLocation(value){ const text=String(value ?? '').trim(); if(text.length>160 || /[\u0000\r\n]/.test(text)) throw new PlatformError('Lagerort ist ungültig.',{code:'INVENTORY_LOCATION_INVALID',status:400,expose:true}); return text; }
export class InventoryService {
constructor(repository) { this.repository = repository; }
locations(){ return this.repository.locations(); }
unassigned(){ return this.repository.unassigned(); }
byLocation(location){ return this.repository.byLocation(cleanLocation(location)); }
validateBulk(input={}){ for(const key of Object.keys(input||{})) if(!['ids','location'].includes(key)) throw new PlatformError(`Unbekanntes Lagerfeld: ${key}`,{code:'INVENTORY_INPUT_INVALID',status:400,expose:true}); const ids=Array.isArray(input.ids)?[...new Set(input.ids.map(Number).filter(Number.isInteger).filter(id=>id>0))]:[]; if(!ids.length) throw new PlatformError('Mindestens eine Artikel-ID ist erforderlich.',{code:'INVENTORY_IDS_REQUIRED',status:400,expose:true}); if(ids.length>500) throw new PlatformError('Maximal 500 Artikel pro Vorgang.',{code:'INVENTORY_BATCH_TOO_LARGE',status:400,expose:true}); return Object.freeze({ ids, location:cleanLocation(input.location) }); }
bulk(input){ const valid=this.validateBulk(input); return { ok:true, updated:this.repository.bulkUpdateLocation(valid.ids, valid.location) }; }
}
-6
View File
@@ -1,6 +0,0 @@
import manifest from './module.json' with { type: 'json' };
import { ListingsService } from './service.mjs';
let service = null;
export function configureListingsModule({ repository }) { service = new ListingsService(repository); }
export function getListingsService() { if (!service) throw new Error('Listings-Modul ist nicht konfiguriert.'); return service; }
export function createListingsModule() { return { manifest, lifecycle: { async start(){}, async stop(){}, async health(){ return { ok:Boolean(service), native:true, contracts:['catalog','trash'] }; } } }; }
-30
View File
@@ -1,30 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.listings",
"name": "Listings",
"version": "2.0.0",
"type": "feature",
"status": "native",
"description": "Native Artikelverwaltung mit validierten Payloads, Papierkorb, Duplikaten und Audit-Verträgen.",
"requires": [
"vendoo.auth",
"vendoo.media"
],
"permissions": [
"listings.view",
"listings.edit",
"listings.delete",
"listings.permanent-delete"
],
"provides": [
"listings.catalog"
],
"consumes": [
"media.assets",
"auth.identity"
],
"owns": [
"listings.records",
"listings.trash"
]
}
-18
View File
@@ -1,18 +0,0 @@
import {
createListing, getListing, getListings, updateListing, softDeleteListing,
restoreListing, getDeletedListings, permanentlyDeleteListing, getTrashCount,
duplicateListing,
} from '../../../lib/db.mjs';
export const listingsRepository = Object.freeze({
create: createListing,
get: getListing,
list: getListings,
update: updateListing,
softDelete: softDeleteListing,
restore: restoreListing,
listDeleted: getDeletedListings,
permanentDelete: permanentlyDeleteListing,
trashCount: getTrashCount,
duplicate: duplicateListing,
});
-19
View File
@@ -1,19 +0,0 @@
import { getListingsService } from './index.mjs';
export function registerListingsRoutes({ app, routes, deps }) {
const service = getListingsService();
routes.register(app, { id:'listings.catalog.create', method:'POST', path:'/api/listings', owner:'vendoo.listings', policy:'listings.edit', stability:'stable', input: body => service.validateCreate(body),
handler: async (req,res) => { const item = service.create(req.validatedBody, req.user.id); deps.auditLog(req.user.id, req.user.email, 'listing.created', 'listing', String(item.id), item.title, deps.getClientIp(req)); res.json(item); } });
routes.register(app, { id:'listings.catalog.list', method:'GET', path:'/api/listings', owner:'vendoo.listings', policy:'listings.view', csrf:false, stability:'stable',
handler: async (req,res) => res.json(service.list(req.query || {})) });
routes.register(app, { id:'listings.catalog.read', method:'GET', path:'/api/listings/:id', owner:'vendoo.listings', policy:'listings.view', csrf:false, stability:'stable', handler: async (req,res)=>res.json(service.get(req.params.id)) });
routes.register(app, { id:'listings.catalog.update', method:'PUT', path:'/api/listings/:id', owner:'vendoo.listings', policy:'listings.edit', stability:'stable', input: body => service.validateUpdate(Object.fromEntries(Object.entries(body || {}).filter(([k]) => k !== '_edit_lock'))),
handler: async (req,res) => { const lock = deps.validateResourceLock({ resourceType:'listing', resourceId:req.params.id, userId:req.user.id, token:req.get('x-edit-lock') || req.body?._edit_lock }); if (!lock.valid) return res.status(409).json({ error:`${lock.lock?.user_name || lock.lock?.user_email || 'Ein anderer Benutzer'} bearbeitet diesen Artikel gerade.`, code:'EDIT_LOCKED', lock:lock.lock }); const item=service.update(req.params.id, req.validatedBody, req.user.id); deps.auditLog(req.user.id, req.user.email, 'listing.updated', 'listing', String(item.id), item.title, deps.getClientIp(req)); res.json(item); } });
routes.register(app, { id:'listings.catalog.delete', method:'DELETE', path:'/api/listings/:id', owner:'vendoo.listings', policy:'listings.delete', stability:'stable', handler: async (req,res)=>{ const result=service.softDelete(req.params.id); deps.auditLog(req.user.id, req.user.email, 'listing.soft_deleted', 'listing', req.params.id, null, deps.getClientIp(req)); res.json(result); } });
routes.register(app, { id:'listings.catalog.duplicate', method:'POST', path:'/api/listings/:id/duplicate', owner:'vendoo.listings', policy:'listings.edit', stability:'stable', handler: async (req,res)=>{ const item=service.duplicate(req.params.id); deps.auditLog(req.user.id, req.user.email, 'listing.duplicated', 'listing', String(item.id), item.title, deps.getClientIp(req)); res.json(item); } });
routes.register(app, { id:'listings.trash.list', method:'GET', path:'/api/trash', owner:'vendoo.listings', policy:'listings.view', csrf:false, stability:'stable', handler:async(_req,res)=>res.json(service.trash()) });
routes.register(app, { id:'listings.trash.count', method:'GET', path:'/api/trash/count', owner:'vendoo.listings', policy:'listings.view', csrf:false, stability:'stable', handler:async(_req,res)=>res.json(service.trashCount()) });
routes.register(app, { id:'listings.trash.restore', method:'POST', path:'/api/trash/:id/restore', owner:'vendoo.listings', policy:'listings.edit', stability:'stable', handler:async(req,res)=>{ const result=service.restore(req.params.id); deps.auditLog(req.user.id, req.user.email, 'listing.restored', 'listing', req.params.id, null, deps.getClientIp(req)); res.json(result); } });
routes.register(app, { id:'listings.trash.permanent-delete', method:'DELETE', path:'/api/trash/:id', owner:'vendoo.listings', policy:'listings.permanent-delete', stability:'stable', handler:async(req,res)=>{ const result=service.permanentDelete(req.params.id); deps.auditLog(req.user.id, req.user.email, 'listing.permanently_deleted', 'listing', req.params.id, null, deps.getClientIp(req)); res.json(result); } });
routes.register(app, { id:'listings.trash.empty', method:'DELETE', path:'/api/trash', owner:'vendoo.listings', policy:'listings.permanent-delete', stability:'stable', handler:async(req,res)=>{ const result=service.emptyTrash(); deps.auditLog(req.user.id, req.user.email, 'trash.emptied', null, null, `${result.deleted} Einträge`, deps.getClientIp(req)); res.json(result); } });
}
-68
View File
@@ -1,68 +0,0 @@
import { PlatformError } from '../../kernel/errors.mjs';
import { sanitizeRichHtml } from '../../../lib/html-templates.mjs';
const ALLOWED_FIELDS = new Set([
'platform','ai_provider','title','description','description_html','tags','photos','seller_notes',
'brand','category','size','color','condition','price','status','language','sku','storage_location',
]);
const ARRAY_LIMITS = { tags: 50, photos: 100 };
function fail(message, field, status = 400, code = 'LISTING_INPUT_INVALID') {
throw new PlatformError(message, { code, status, expose: true, details: { field } });
}
function htmlToPlainText(html) {
return String(html || '').replace(/<\s*br\s*\/?>/gi, '\n').replace(/<\/(p|div|h[1-6]|li|blockquote|pre)>/gi, '\n')
.replace(/<[^>]+>/g, '').replace(/&nbsp;/gi, ' ').replace(/&amp;/gi, '&').replace(/&lt;/gi, '<')
.replace(/&gt;/gi, '>').replace(/&quot;/gi, '"').replace(/&#39;/gi, "'").replace(/\n{3,}/g, '\n\n').trim();
}
function cleanText(value, max, field, { required = false } = {}) {
const text = String(value ?? '').trim();
if (required && !text) fail(`${field} ist erforderlich.`, field);
if (text.length > max) fail(`${field} ist zu lang.`, field);
if (/\u0000/.test(text)) fail(`${field} enthält ungültige Steuerzeichen.`, field);
return text;
}
function normalizePayload(input = {}, { partial = false } = {}) {
const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {};
for (const key of Object.keys(source)) if (!ALLOWED_FIELDS.has(key)) fail(`Unbekanntes Artikelfeld: ${key}`, key);
const out = {};
const textRules = {
platform:64, ai_provider:64, title:240, description:12000, seller_notes:6000, brand:160,
category:240, size:120, color:120, condition:120, status:64, language:16, sku:80, storage_location:160,
};
for (const [field,max] of Object.entries(textRules)) if (source[field] !== undefined) out[field] = cleanText(source[field], max, field, { required: field === 'title' && !partial });
if (!partial && source.title === undefined) fail('title ist erforderlich.', 'title');
if (source.description_html !== undefined) {
const html = String(source.description_html || '');
if (html.length > 50000) fail('description_html ist zu lang.', 'description_html');
out.description_html = sanitizeRichHtml(html);
if (source.description === undefined) out.description = htmlToPlainText(out.description_html).slice(0, 12000);
}
for (const field of ['tags','photos']) if (source[field] !== undefined) {
if (!Array.isArray(source[field])) fail(`${field} muss eine Liste sein.`, field);
out[field] = source[field].map(v => cleanText(v, field === 'photos' ? 500 : 120, field)).filter(Boolean).slice(0, ARRAY_LIMITS[field]);
}
if (source.price !== undefined && source.price !== null && source.price !== '') {
const price = Number(source.price);
if (!Number.isFinite(price) || price < 0 || price > 100000000) fail('price ist ungültig.', 'price');
out.price = price;
} else if (source.price !== undefined) out.price = null;
return Object.freeze(out);
}
export class ListingsService {
constructor(repository) { this.repository = repository; }
validateCreate(input) { return normalizePayload(input, { partial: false }); }
validateUpdate(input) { return normalizePayload(input, { partial: true }); }
create(input, actorId) { return this.repository.create({ ...this.validateCreate(input), created_by: actorId || null, updated_by: actorId || null }); }
list(filters = {}) { return this.repository.list(cleanText(filters.q || '', 240, 'q'), cleanText(filters.platform || '', 64, 'platform'), cleanText(filters.status || '', 64, 'status')); }
get(id) { const item = this.repository.get(Number(id)); if (!item) throw new PlatformError('Artikel nicht gefunden.', { code:'LISTING_NOT_FOUND', status:404, expose:true }); return item; }
update(id, input, actorId) { this.get(id); return this.repository.update(Number(id), { ...this.validateUpdate(input), updated_by: actorId || null }); }
softDelete(id) { this.get(id); this.repository.softDelete(Number(id)); return { ok:true }; }
duplicate(id) { const copy = this.repository.duplicate(Number(id)); if (!copy) throw new PlatformError('Artikel nicht gefunden.', { code:'LISTING_NOT_FOUND', status:404, expose:true }); return copy; }
trash() { return this.repository.listDeleted(); }
trashCount() { return { count: this.repository.trashCount() }; }
restore(id) { const item = this.repository.listDeleted().find(entry => Number(entry.id) === Number(id)); if (!item) throw new PlatformError('Artikel befindet sich nicht im Papierkorb.', { code:'LISTING_TRASH_NOT_FOUND', status:404, expose:true }); this.repository.restore(Number(id)); return { ok:true }; }
permanentDelete(id) { const item = this.repository.listDeleted().find(entry => Number(entry.id) === Number(id)); if (!item) throw new PlatformError('Artikel befindet sich nicht im Papierkorb.', { code:'LISTING_TRASH_NOT_FOUND', status:404, expose:true }); this.repository.permanentDelete(Number(id)); return { ok:true }; }
emptyTrash() { const items = this.repository.listDeleted(); for (const item of items) this.repository.permanentDelete(item.id); return { ok:true, deleted:items.length }; }
}
-1
View File
@@ -1 +0,0 @@
import manifest from './module.json' with {type:'json'}; import { MediaService } from './service.mjs'; let service=null; export function configureMediaModule(deps){service=new MediaService(deps);} export function getMediaService(){if(!service)throw new Error('Media-Modul ist nicht konfiguriert.');return service;} export function createMediaModule(){return {manifest,lifecycle:{async start(){},async stop(){},async health(){return {ok:Boolean(service),native:true,contracts:['library','reference-protection']};}}};}
-28
View File
@@ -1,28 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.media",
"name": "Media Library",
"version": "2.0.0",
"type": "feature",
"status": "native",
"description": "Native Medienbibliothek mit sicherer Pfadbehandlung, Referenzschutz, Favoriten und Metadaten.",
"requires": [
"vendoo.auth"
],
"permissions": [
"media.view",
"media.edit",
"media.delete"
],
"provides": [
"media.assets",
"media.derivatives"
],
"consumes": [
"auth.identity"
],
"owns": [
"media.library",
"media.uploads"
]
}
-2
View File
@@ -1,2 +0,0 @@
import { listMediaLibraryItems, setMediaLibraryFavorite, getListingReferencedMediaPaths, removeMediaLibraryRecords } from '../../../lib/db.mjs';
export const mediaRepository=Object.freeze({list:listMediaLibraryItems,setFavorite:setMediaLibraryFavorite,referenced:getListingReferencedMediaPaths,removeRecords:removeMediaLibraryRecords});
-2
View File
@@ -1,2 +0,0 @@
import { getMediaService } from './index.mjs';
export function registerMediaRoutes({app,routes,deps}){const service=getMediaService();routes.register(app,{id:'media.library.list',method:'GET',path:'/api/media-library',owner:'vendoo.media',policy:'media.view',csrf:false,stability:'stable',handler:async(q,r)=>r.json(await service.list(q.query||{}))});routes.register(app,{id:'media.library.favorite',method:'PATCH',path:'/api/media-library/favorite',owner:'vendoo.media',policy:'media.edit',stability:'stable',input:body=>service.validateFavorite(body),handler:async(q,r)=>{const result=service.favorite(q.validatedBody);if(!result.ok)return r.status(400).json({error:'Nur FLUX-Bilder können als Favorit markiert werden.'});deps.auditLog(q.user.id,q.user.email,'media.favorite_updated','media',q.validatedBody.image_path,String(q.validatedBody.favorite),deps.getClientIp(q));r.json(result);}});routes.register(app,{id:'media.library.delete',method:'DELETE',path:'/api/media-library/items',owner:'vendoo.media',policy:'media.delete',stability:'stable',input:body=>service.validateDelete(body),handler:async(q,r)=>{const result=service.delete(q.validatedBody);deps.auditLog(q.user.id,q.user.email,'media.deleted','media',null,JSON.stringify({deleted:result.deleted,protected:result.protected.length}),deps.getClientIp(q));r.json(result);}});}
-8
View File
@@ -1,8 +0,0 @@
import { existsSync, statSync, unlinkSync } from 'node:fs'; import { resolve, sep, extname } from 'node:path'; import { PlatformError } from '../../kernel/errors.mjs'; function normalizePath(value){const p=String(value||'').replace(/\\/g,'/').replace(/^\/+/, '').trim();if(!p||p.includes('..')||/[\u0000\r\n]/.test(p)) throw new PlatformError('Ungültiger Medienpfad.',{code:'MEDIA_PATH_INVALID',status:400,expose:true});return p;}
export class MediaService { constructor({uploadRoot,sharp,repository}){this.uploadRoot=resolve(uploadRoot);this.sharp=sharp;this.repository=repository;}
async list(query={}){const source=String(query.source||'all').slice(0,32),search=String(query.q||'').slice(0,240),favoritesOnly=String(query.favorites||'0')==='1',sort=['newest','oldest','name'].includes(String(query.sort))?String(query.sort):'newest';const all=this.repository.list({source,search,favoritesOnly,sort});const pageSize=Math.max(12,Math.min(96,Number(query.page_size)||24)),total=all.length,totalPages=Math.max(1,Math.ceil(total/pageSize)),page=Math.min(Math.max(1,Number(query.page)||1),totalPages);const pageItems=all.slice((page-1)*pageSize,page*pageSize);const items=await Promise.all(pageItems.map(async item=>{const imagePath=normalizePath(item.image_path),absolute=resolve(this.uploadRoot,imagePath);let fileSize=null,width=Number(item.width)||null,height=Number(item.height)||null,format=item.format||extname(imagePath).replace('.','').toLowerCase()||null;if(absolute!==this.uploadRoot&&absolute.startsWith(this.uploadRoot+sep)&&existsSync(absolute)){try{fileSize=statSync(absolute).size;}catch{}if((!width||!height)&&this.sharp){try{const m=await this.sharp(absolute,{failOn:'none'}).metadata();width=m.width||width;height=m.height||height;format=m.format||format;}catch{}}}return {...item,image_path:imagePath,public_url:`/uploads/${imagePath}`,file_size:fileSize,width,height,format};}));const summary=this.repository.list({source:'all',search,favoritesOnly,sort});const counts=summary.reduce((acc,item)=>{for(const s of item.sources||[])acc[s]=(acc[s]||0)+1;return acc;},{});return {items,pagination:{page,page_size:pageSize,total,total_pages:totalPages},summary:{total:summary.length,sources:counts}};}
validateFavorite(input={}){for(const key of Object.keys(input||{}))if(!['image_path','favorite'].includes(key))throw new PlatformError(`Unbekanntes Medienfeld: ${key}`,{code:'MEDIA_INPUT_INVALID',status:400,expose:true});return Object.freeze({image_path:normalizePath(input.image_path),favorite:input.favorite!==false});}
favorite(input={}){const valid=this.validateFavorite(input);return this.repository.setFavorite(valid.image_path,valid.favorite);}
validateDelete(input={}){for(const key of Object.keys(input||{}))if(!['image_paths'].includes(key))throw new PlatformError(`Unbekanntes Medienfeld: ${key}`,{code:'MEDIA_INPUT_INVALID',status:400,expose:true});if(!Array.isArray(input.image_paths))throw new PlatformError('image_paths muss eine Liste sein.',{code:'MEDIA_PATHS_REQUIRED',status:400,expose:true});const paths=[...new Set(input.image_paths.map(normalizePath))].slice(0,500);if(!paths.length)throw new PlatformError('Keine Medien ausgewählt.',{code:'MEDIA_PATHS_REQUIRED',status:400,expose:true});return Object.freeze({image_paths:paths});}
delete(input={}){const requested=this.validateDelete(input).image_paths,referenced=this.repository.referenced(),protectedPaths=requested.filter(p=>referenced.has(p)),deletable=requested.filter(p=>!referenced.has(p)),deletedFiles=[];for(const p of deletable){const absolute=resolve(this.uploadRoot,p);if(absolute===this.uploadRoot||!absolute.startsWith(this.uploadRoot+sep))continue;try{if(existsSync(absolute)&&statSync(absolute).isFile()){unlinkSync(absolute);deletedFiles.push(p);}}catch{}}this.repository.removeRecords(deletable);return {ok:true,deleted:deletable.length,deleted_files:deletedFiles.length,protected:protectedPaths};}
}
-28
View File
@@ -1,28 +0,0 @@
import manifest from './module.json' with { type: 'json' };
import { ModuleManagerService } from './service.mjs';
let service = null;
export function createModuleManagerModule() {
return {
manifest,
source: 'builtin',
lifecycle: {
async start(context) {
service = new ModuleManagerService({
modules: context.modules,
stateStore: context.moduleStateStore,
packageStore: context.modulePackageStore,
context,
});
},
async stop() { service = null; },
async health() { return { ok: Boolean(service), protected: true }; },
},
};
}
export function getModuleManagerService() {
if (!service) throw new Error('Module Manager ist noch nicht gestartet.');
return service;
}
-14
View File
@@ -1,14 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.module-manager",
"name": "Module Manager",
"version": "1.0.0",
"type": "core",
"status": "native",
"description": "Sichere Verwaltung eingebauter und installierter Vendoo-Module mit Aktivierung, Export, Quarantäne und kontrollierter Entfernung.",
"requires": ["vendoo.security", "vendoo.settings"],
"permissions": ["modules.manage"],
"provides": ["modules.management", "modules.packages"],
"consumes": ["security.policies", "settings.configuration"],
"owns": ["modules.state", "modules.packages"]
}
-62
View File
@@ -1,62 +0,0 @@
import { objectSchema } from '../../kernel/input-schema.mjs';
import { getModuleManagerService } from './index.mjs';
const toggleSchema = objectSchema({
cascade: { type: 'boolean' },
});
export function registerModuleManagerRoutes({ app, routes, upload, deps }) {
routes.register(app, {
id: 'modules.catalog.read', method: 'GET', path: '/api/admin/modules', owner: 'vendoo.module-manager',
policy: 'modules.manage', audit: 'modules.catalog.read', rateLimit: 'administration', csrf: false, stability: 'stable',
handler: async (_req, res) => res.json(getModuleManagerService().catalog()),
});
routes.register(app, {
id: 'modules.enable', method: 'POST', path: '/api/admin/modules/:id/enable', owner: 'vendoo.module-manager',
policy: 'modules.manage', audit: 'modules.enabled', rateLimit: 'administration', csrf: true, stability: 'stable',
handler: async (req, res) => {
const module = await getModuleManagerService().enable(req.params.id);
deps.auditLog(req.user.id, req.user.email, 'module.enabled', 'module', req.params.id, null, deps.getClientIp(req));
res.json({ module });
},
});
routes.register(app, {
id: 'modules.disable', method: 'POST', path: '/api/admin/modules/:id/disable', owner: 'vendoo.module-manager',
policy: 'modules.manage', audit: 'modules.disabled', rateLimit: 'administration', csrf: true, stability: 'stable', input: toggleSchema,
handler: async (req, res) => {
const result = await getModuleManagerService().disable(req.params.id, req.validatedBody || {});
deps.auditLog(req.user.id, req.user.email, 'module.disabled', 'module', req.params.id, JSON.stringify(result.disabled), deps.getClientIp(req));
res.json(result);
},
});
routes.register(app, {
id: 'modules.install', method: 'POST', path: '/api/admin/modules/install', owner: 'vendoo.module-manager',
policy: 'modules.manage', audit: 'modules.installed', rateLimit: 'administration', csrf: true, stability: 'stable',
middleware: [upload.single('module')],
handler: async (req, res) => {
const module = getModuleManagerService().install(req.file?.buffer);
deps.auditLog(req.user.id, req.user.email, 'module.installed', 'module', module.id, module.version, deps.getClientIp(req));
res.status(201).json({ module });
},
});
routes.register(app, {
id: 'modules.remove', method: 'DELETE', path: '/api/admin/modules/:id', owner: 'vendoo.module-manager',
policy: 'modules.manage', audit: 'modules.removed', rateLimit: 'administration', csrf: true, stability: 'stable',
handler: async (req, res) => {
const result = await getModuleManagerService().remove(req.params.id);
deps.auditLog(req.user.id, req.user.email, 'module.removed', 'module', req.params.id, null, deps.getClientIp(req));
res.json(result);
},
});
routes.register(app, {
id: 'modules.export', method: 'GET', path: '/api/admin/modules/:id/export', owner: 'vendoo.module-manager',
policy: 'modules.manage', audit: 'modules.exported', rateLimit: 'administration', csrf: false, stability: 'stable',
handler: async (req, res) => {
const payload = getModuleManagerService().export(req.params.id);
deps.auditLog(req.user.id, req.user.email, 'module.exported', 'module', req.params.id, null, deps.getClientIp(req));
res.setHeader('Content-Type', 'application/vnd.vendoo.module+json; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="${String(req.params.id).replace(/[^a-z0-9.-]/gi, '_')}.vmod"`);
res.send(payload);
},
});
}
-233
View File
@@ -1,233 +0,0 @@
import { readFileSync } from 'fs';
import { PlatformError } from '../../kernel/errors.mjs';
import { buildModulePackage } from '../../core/modules/module-package-contract.mjs';
export const PROTECTED_MODULE_IDS = Object.freeze([
'vendoo.system',
'vendoo.security',
'vendoo.auth',
'vendoo.users',
'vendoo.sessions',
'vendoo.settings',
'vendoo.themes',
'vendoo.listings',
'vendoo.inventory',
'vendoo.media',
'vendoo.operations',
'vendoo.module-manager',
'vendoo.deployments',
]);
const PROTECTED_MODULES = new Set(PROTECTED_MODULE_IDS);
function asPublicModule(record, packagesById) {
const external = record.source === 'external';
const pkg = packagesById.get(record.id);
const protectedModule = PROTECTED_MODULES.has(record.id);
const canDisable = !protectedModule && record.enabled;
const canEnable = !record.enabled && (!external || pkg?.activatable === true);
return {
...record,
protected: protectedModule,
removable: external && !record.enabled,
exportable: true,
installSource: external ? 'installed' : 'builtin',
package: pkg ? {
runtime: pkg.runtime,
trust: pkg.trust,
activatable: pkg.activatable,
invalid: Boolean(pkg.invalid),
error: pkg.error || null,
} : null,
actions: {
enable: canEnable,
disable: canDisable,
remove: external && !record.enabled,
export: true,
},
};
}
export function createExternalLifecycle(pkg) {
return {
async start() {
if (pkg.runtime.mode === 'isolated') {
throw new PlatformError('Der isolierte Extension-Host ist für ausführbare Fremdmodule noch nicht aktiviert.', {
code: 'MODULE_EXTENSION_HOST_UNAVAILABLE', status: 409, expose: true,
});
}
},
async stop() {},
async health() {
return { ok: true, runtime: pkg.runtime.mode, external: true };
},
};
}
export class ModuleManagerService {
#modules;
#stateStore;
#packageStore;
#context;
constructor({ modules, stateStore, packageStore, context }) {
this.#modules = modules;
this.#stateStore = stateStore;
this.#packageStore = packageStore;
this.#context = context;
}
registerInstalledPackages() {
const installed = this.#packageStore.list();
for (const entry of installed) {
if (entry.invalid || this.#modules.has(entry.id)) continue;
const pkg = this.#packageStore.get(entry.id);
const enabled = entry.activatable && this.#stateStore.isEnabled(entry.id, false);
this.#modules.register({ manifest: pkg.manifest, lifecycle: createExternalLifecycle(pkg), source: 'external' }, { enabled });
}
return installed.length;
}
catalog() {
const packages = this.#packageStore.list();
const packagesById = new Map(packages.filter(item => item.id).map(item => [item.id, item]));
const modules = this.#modules.snapshot().map(record => asPublicModule(record, packagesById));
for (const pkg of packages) {
if (!pkg.invalid || modules.some(item => item.id === pkg.id)) continue;
modules.push({
id: pkg.id,
name: pkg.id,
version: null,
type: 'extension-host',
status: 'invalid',
source: 'external',
enabled: false,
state: 'invalid',
protected: false,
removable: true,
exportable: false,
installSource: 'installed',
package: { invalid: true, error: pkg.error, activatable: false },
actions: { enable: false, disable: false, remove: true, export: false },
});
}
return {
modules: modules.sort((a, b) => String(a.name || a.id).localeCompare(String(b.name || b.id), 'de')),
summary: {
total: modules.length,
enabled: modules.filter(item => item.enabled).length,
disabled: modules.filter(item => !item.enabled && item.state !== 'invalid').length,
native: modules.filter(item => item.status === 'native').length,
legacy: modules.filter(item => item.status === 'legacy-bridge').length,
external: modules.filter(item => item.source === 'external').length,
protected: modules.filter(item => item.protected).length,
},
safety: {
unsignedDeclarativeAllowed: true,
executableRequiresTrustedSignature: true,
isolatedHostAvailable: false,
directMainProcessExecution: false,
},
};
}
async enable(id) {
const record = this.#modules.get(id);
if (!record) throw new PlatformError('Modul nicht gefunden.', { code: 'MODULE_NOT_FOUND', status: 404, expose: true });
if (record.source === 'external') {
const pkg = this.#packageStore.describe(id);
if (!pkg?.activatable) {
throw new PlatformError('Dieses Fremdmodul ist nicht aktivierbar. Ausführbarer Code benötigt eine vertrauenswürdige Signatur und den isolierten Extension-Host.', {
code: 'MODULE_NOT_ACTIVATABLE', status: 409, expose: true,
});
}
if (pkg.runtime.mode === 'isolated') {
throw new PlatformError('Der isolierte Extension-Host ist noch nicht verfügbar.', { code: 'MODULE_EXTENSION_HOST_UNAVAILABLE', status: 409, expose: true });
}
}
const enabling = new Set();
const enableWithDependencies = async moduleId => {
if (enabling.has(moduleId)) throw new PlatformError('Zyklische Modulabhängigkeit erkannt.', { code: 'MODULE_DEPENDENCY_CYCLE', status: 409, expose: true });
const target = this.#modules.get(moduleId);
if (!target) throw new PlatformError(`Fehlende Modulabhängigkeit: ${moduleId}`, { code: 'MODULE_DEPENDENCY_MISSING', status: 409, expose: true });
if (target.enabled && target.state === 'running') return this.#modules.describe(moduleId);
enabling.add(moduleId);
for (const dependency of target.manifest.requires) await enableWithDependencies(dependency);
const enabled = await this.#modules.enable(moduleId, this.#context);
this.#stateStore.setEnabled(moduleId, true, { source: moduleId === id ? 'module-manager' : 'module-manager-dependency' });
enabling.delete(moduleId);
return enabled;
};
return enableWithDependencies(id);
}
async disable(id, { cascade = false } = {}) {
if (PROTECTED_MODULES.has(id)) {
throw new PlatformError('Dieses Kernmodul ist geschützt und kann nicht deaktiviert werden.', {
code: 'MODULE_PROTECTED', status: 409, expose: true,
});
}
const dependents = this.#modules.dependentsOf(id, { enabledOnly: true, recursive: true });
const protectedDependents = dependents.filter(dependent => PROTECTED_MODULES.has(dependent));
if (protectedDependents.length) {
throw new PlatformError('Das Modul wird von geschützten Kernmodulen benötigt.', {
code: 'MODULE_REQUIRED_BY_PROTECTED', status: 409, expose: true, details: { dependents: protectedDependents },
});
}
const result = await this.#modules.disable(id, this.#context, { cascade: Boolean(cascade) });
for (const disabledId of result.disabled) this.#stateStore.setEnabled(disabledId, false, { source: 'module-manager' });
return result;
}
install(buffer) {
if (!Buffer.isBuffer(buffer) || !buffer.length) throw new PlatformError('Modulpaket fehlt.', { code: 'MODULE_PACKAGE_REQUIRED', status: 400, expose: true });
let parsed;
try { parsed = JSON.parse(buffer.toString('utf8')); }
catch (error) { throw new PlatformError('Das .vmod-Paket enthält kein gültiges JSON.', { code: 'MODULE_PACKAGE_JSON_INVALID', status: 400, expose: true, cause: error }); }
const id = String(parsed?.manifest?.id || '');
if (this.#modules.has(id)) throw new PlatformError('Eine Modul-ID mit diesem Namen ist bereits registriert.', { code: 'MODULE_DUPLICATE', status: 409, expose: true });
const installed = this.#packageStore.install(parsed);
const pkg = this.#packageStore.get(installed.id);
try {
this.#modules.register({ manifest: pkg.manifest, lifecycle: createExternalLifecycle(pkg), source: 'external' }, { enabled: false });
this.#stateStore.setEnabled(installed.id, false, { source: 'module-install' });
} catch (error) {
this.#packageStore.remove(installed.id);
this.#stateStore.remove(installed.id);
throw error;
}
return this.catalog().modules.find(item => item.id === installed.id);
}
async remove(id) {
const record = this.#modules.get(id);
if (!record) {
const removedInvalid = this.#packageStore.remove(id);
if (removedInvalid) this.#stateStore.remove(id);
return { ok: removedInvalid, id };
}
if (record.source !== 'external') {
throw new PlatformError('Eingebaute Vendoo-Module können nicht gelöscht werden.', { code: 'MODULE_BUILTIN_NOT_REMOVABLE', status: 409, expose: true });
}
if (record.enabled) throw new PlatformError('Das Modul muss vor dem Löschen deaktiviert werden.', { code: 'MODULE_DISABLE_BEFORE_REMOVE', status: 409, expose: true });
await this.#modules.unregister(id, this.#context);
this.#packageStore.remove(id);
this.#stateStore.remove(id);
return { ok: true, id };
}
export(id) {
const record = this.#modules.get(id);
if (!record) throw new PlatformError('Modul nicht gefunden.', { code: 'MODULE_NOT_FOUND', status: 404, expose: true });
if (record.source === 'external') return this.#packageStore.export(id);
const pkg = buildModulePackage({
manifest: record.manifest,
runtime: { mode: 'builtin-reference', entry: null },
files: [{ path: 'module.json', content: `${JSON.stringify(record.manifest, null, 2)}\n` }],
});
return Buffer.from(`${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
}
stateSnapshot() {
return this.#stateStore.snapshot();
}
}
-26
View File
@@ -1,26 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.operations",
"name": "Operations Center",
"version": "1.0.0",
"type": "feature",
"status": "legacy-bridge",
"description": "Vertrag für den bestehenden Vendoo-Bereich Operations Center; schrittweise Migration ohne Funktionsbruch.",
"requires": [
"vendoo.security"
],
"permissions": [
"operations.manage"
],
"provides": [
"operations.backup",
"operations.update"
],
"consumes": [
"security.audit"
],
"owns": [
"operations.backups",
"operations.updates"
]
}
-13
View File
@@ -1,13 +0,0 @@
import manifest from './module.json' with { type: 'json' };
export function createProductImageToolsModule() {
return {
manifest,
source: 'builtin',
lifecycle: {
async start() {},
async stop() {},
async health() { return { ok: true, native: true, optional: true, contracts: ['ghost-mannequin', 'flatlay'] }; },
},
};
}
@@ -1,28 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.product-image-tools",
"name": "Produktbild Studio",
"version": "1.0.0",
"type": "feature",
"status": "native",
"defaultEnabled": false,
"description": "Optionale, sichere Produktbild-Varianten für Ghost-Mannequin und Flatlay. Das Modul ist standardmäßig deaktiviert und kann im Modulmanager bewusst zugeschaltet werden.",
"requires": [
"vendoo.ai",
"vendoo.media"
],
"permissions": [
"generation.execute"
],
"provides": [
"product-images.variants"
],
"consumes": [
"ai.images",
"media.assets"
],
"owns": [
"product-images.jobs",
"product-images.variants"
]
}
-23
View File
@@ -1,23 +0,0 @@
import manifest from './module.json' with { type: 'json' };
let runtime = null;
let started = false;
export function configurePublishingModule(deps = {}) {
runtime = {
start: deps.start || (() => {}),
stop: deps.stop || (() => {}),
};
}
export function createPublishingModule() {
return {
manifest,
source: 'builtin',
lifecycle: {
async start() { if (!runtime) throw new Error('Publishing-Modul ist nicht konfiguriert.'); await runtime.start(); started = true; },
async stop() { await runtime?.stop?.(); started = false; },
async health() { return { ok: Boolean(runtime && started), native: true, queue: 'publishing-jobs' }; },
},
};
}
-30
View File
@@ -1,30 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.publishing",
"name": "Publishing",
"version": "1.0.0",
"type": "feature",
"status": "native",
"description": "Native Publishing-Jobs, Plattformadapter und kontrollierter Queue-Lifecycle.",
"requires": [
"vendoo.listings",
"vendoo.security"
],
"permissions": [
"publishing.view",
"publishing.execute",
"publishing.manage"
],
"provides": [
"publishing.jobs",
"publishing.adapters"
],
"consumes": [
"listings.catalog",
"security.policies"
],
"owns": [
"publishing.queue",
"publishing.events"
]
}
-13
View File
@@ -1,13 +0,0 @@
import manifest from './module.json' with { type: 'json' };
export function createQualityModule() {
return {
manifest,
source: 'builtin',
lifecycle: {
async start() {},
async stop() {},
async health() { return { ok: true, native: true, contracts: ['reports', 'ai-suggestions'] }; },
},
};
}
-26
View File
@@ -1,26 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.quality",
"name": "Quality Center",
"version": "1.0.0",
"type": "feature",
"status": "native",
"description": "Native Qualitätsanalyse, Verlauf und bestätigte AI-Vorschläge.",
"requires": [
"vendoo.listings",
"vendoo.ai"
],
"permissions": [
"quality.execute"
],
"provides": [
"quality.reports"
],
"consumes": [
"listings.catalog",
"ai.text"
],
"owns": [
"quality.history"
]
}
-26
View File
@@ -1,26 +0,0 @@
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { secretStoreStatus } from '../../../lib/secret-store.mjs';
import { getRequestTelemetry } from '../../../lib/structured-logger.mjs';
const here = dirname(fileURLToPath(import.meta.url));
const manifest = JSON.parse(readFileSync(join(here, 'module.json'), 'utf8'));
export function createSecurityModule() {
return {
manifest,
lifecycle: {
async start() {},
async stop() {},
async health() {
const secrets = secretStoreStatus();
return { ok: true, provider: secrets.provider, encrypted_secrets: secrets.encrypted };
},
},
};
}
export function getSecurityFoundationStatus() {
return { secrets: secretStoreStatus(), requests: getRequestTelemetry() };
}
-31
View File
@@ -1,31 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.security",
"name": "Security Core",
"version": "2.0.0",
"type": "core",
"status": "native",
"description": "Nativer Sicherheitskern für verschlüsselte Secrets, Request-Telemetrie, Sicherheitsheader und Security-Dashboard.",
"requires": [
"vendoo.system"
],
"permissions": [
"security.manage"
],
"provides": [
"security.policies",
"security.audit",
"security.secrets",
"security.telemetry",
"security.headers"
],
"consumes": [
"platform.events"
],
"owns": [
"security.permissions",
"security.rate-limits",
"security.secret-store",
"security.request-telemetry"
]
}

Some files were not shown because too many files have changed in this diff Show More