Compare commits

..
Author SHA1 Message Date
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
94d898e445 chore(deps): bump the npm-production group across 1 directory with 6 updates (#30)
Bumps the npm-production group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@anthropic-ai/sdk](https://github.com/anthropics/anthropic-sdk-typescript) | `0.39.0` | `0.111.0` |
| [dotenv](https://github.com/motdotla/dotenv) | `16.6.1` | `17.4.2` |
| [express](https://github.com/expressjs/express) | `4.22.2` | `5.2.1` |
| [helmet](https://github.com/helmetjs/helmet) | `8.2.0` | `8.3.0` |
| [openai](https://github.com/openai/openai-node) | `4.104.0` | `6.46.0` |
| [sharp](https://github.com/lovell/sharp) | `0.33.5` | `0.35.3` |



Updates `@anthropic-ai/sdk` from 0.39.0 to 0.111.0
- [Release notes](https://github.com/anthropics/anthropic-sdk-typescript/releases)
- [Changelog](https://github.com/anthropics/anthropic-sdk-typescript/blob/main/CHANGELOG.md)
- [Commits](https://github.com/anthropics/anthropic-sdk-typescript/compare/sdk-v0.39.0...sdk-v0.111.0)

Updates `dotenv` from 16.6.1 to 17.4.2
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/compare/v16.6.1...v17.4.2)

Updates `express` from 4.22.2 to 5.2.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/v4.22.2...v5.2.1)

Updates `helmet` from 8.2.0 to 8.3.0
- [Changelog](https://github.com/helmetjs/helmet/blob/main/CHANGELOG.md)
- [Commits](https://github.com/helmetjs/helmet/compare/v8.2.0...v8.3.0)

Updates `openai` from 4.104.0 to 6.46.0
- [Release notes](https://github.com/openai/openai-node/releases)
- [Changelog](https://github.com/openai/openai-node/blob/main/CHANGELOG.md)
- [Commits](https://github.com/openai/openai-node/compare/v4.104.0...v6.46.0)

Updates `sharp` from 0.33.5 to 0.35.3
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.33.5...v0.35.3)

---
updated-dependencies:
- dependency-name: "@anthropic-ai/sdk"
  dependency-version: 0.111.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-production
- dependency-name: dotenv
  dependency-version: 17.4.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-production
- dependency-name: express
  dependency-version: 5.2.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-production
- dependency-name: helmet
  dependency-version: 8.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-production
- dependency-name: openai
  dependency-version: 6.46.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-production
- dependency-name: sharp
  dependency-version: 0.35.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 17:40:41 +02:00
Masterluke77 0a3d0b86e5 revert: remove unintended UX placeholder 2026-07-11 02:55:04 +02:00
Masterluke77 20f2e9a299 placeholder 2026-07-11 02:54:56 +02:00
Masterluke77andGitHub 637275effc Updater 3.4 – Lifecycle Hardening (#28)
Production-ready updater lifecycle hardening with final-head checks, Windows PowerShell 5.1 compatibility, reproducible package generation, provenance validation, payload rollback protection and documented acceptance.
2026-07-10 19:54:36 +02:00
Masterluke77andGitHub 40071f718e Vendoo 1.43.0 – Production Update
Manifestbasierter Production-Updater 3.3, Modulnavigation, optionales Produktbild Studio, Listing-Studio-Aktionsleiste sowie plattformübergreifende Windows-/Linux-Release-Gates.
2026-07-10 13:16:21 +02:00
Masterluke77andGitHub 6f48827f4d Vendoo 1.41.2 – Git Ignore Boundary & Module Tracking Hotfix (#18)
* docs: prepare Vendoo 1.41.2 git-ignore boundary hotfix

* fix: track native source modules with root-anchored runtime ignores
2026-07-09 17:09:00 +02:00
645 changed files with 87355 additions and 210 deletions
+27
View File
@@ -0,0 +1,27 @@
.git
.gitignore
.env
.env.*
!.env.example
node_modules
npm-debug.log*
*.log
*.db
*.db-*
*.sqlite
*.sqlite3
backups
operations
logs
uploads
runtime
data
local-ai/comfyui
local-ai/downloads
local-ai/.env.local-flux
MockupDesign
*.zip
FILE_CHECKSUMS_SHA256.txt
release-output
.vendoo-github-state.json
operations/first-admin-code.txt
+6
View File
@@ -0,0 +1,6 @@
root = true
[*.{bat,cmd}]
charset = latin1
end_of_line = crlf
insert_final_newline = true
+98 -22
View File
@@ -1,33 +1,109 @@
NODE_ENV=development
HOST=127.0.0.1
PORT=8124
PUBLIC_BASE_URL=http://localhost:8124
TRUST_PROXY=0
COOKIE_SECURE=false
COOKIE_SAMESITE=lax
ALLOWED_ORIGINS=http://localhost:8124
# Vendoo 1.37.0 Beispielkonfiguration für Windows, Docker und NAS
# Diese Datei enthält ausschließlich Platzhalter. Für den Betrieb als .env kopieren.
# Niemals eine produktive .env in Release-ZIPs, Git oder Support-Logs aufnehmen.
DATA_DIR=./data
DB_PATH=./db/vendoo.db
UPLOAD_DIR=./uploads
BACKUP_DIR=./backups
SESSION_SECRET=
ANTHROPIC_API_KEY=
OPENAI_API_KEY=
OPENROUTER_API_KEY=
OLLAMA_URL=http://localhost:11434
SMTP_HOST=
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=
LOCAL_IMAGE_ENABLED=false
LOCAL_IMAGE_URL=http://127.0.0.1:8188
LOCAL_IMAGE_TOKEN=
LOCAL_IMAGE_WORKFLOW_PATH=local-ai/workflows/vendoo_flux_schnell_api.json
LOCAL_IMAGE_INSTALL_PATH=local-ai/comfyui
LOCAL_IMAGE_REQUIRE_TOKEN=true
LOCAL_IMAGE_ALLOW_REMOTE=false
ETSY_API_KEY=
ETSY_CLIENT_SECRET=
EBAY_CLIENT_ID=
EBAY_CLIENT_SECRET=
EBAY_RUNAME=
EBAY_RU_NAME=
EBAY_SANDBOX=true
SMTP_HOST=smtp.example.de
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=
SMTP_PASS=
SMTP_FROM=Vendoo <vendoo@example.de>
PORT=8124
# Docker veröffentlicht standardmäßig nur an 127.0.0.1. Für NAS-Reverse-Proxy ggf. bewusst anpassen.
VENDOO_PUBLISH_ADDRESS=127.0.0.1
VENDOO_HOST_PORT=8124
# Vom Smartphone erreichbare Basis-URL für QR-Fotouploads.
# Beispiel im LAN: http://192.168.178.50:8124
# Öffentlich ausschließlich per HTTPS verwenden.
PUBLIC_BASE_URL=
# Nur für den geführten VPS-Modus; setup.sh trägt die Domain automatisch ein.
VENDOO_DOMAIN=
# Optional: öffentliche JSON-Manifest-URL für die Updateprüfung
VENDOO_UPDATE_MANIFEST_URL=
# Server-, Mehrbenutzer- und Deployment-Sicherheit (1.37.0)
# Standardmäßig nur auf diesem PC erreichbar. Für LAN bewusst auf 0.0.0.0 setzen.
VENDOO_BIND_HOST=127.0.0.1
# Kommagetrennte zusätzliche Hostnamen/IPs, z. B. vendoo.intern,192.168.178.50
VENDOO_ALLOWED_HOSTS=
# Kommagetrennte zusätzliche Browser-Ursprünge, jeweils mit Protokoll und Port
VENDOO_ALLOWED_ORIGINS=
# Nur hinter einem korrekt konfigurierten Reverse Proxy aktivieren
VENDOO_TRUST_PROXY=false
VENDOO_SESSION_DAYS=3
VENDOO_SESSION_IDLE_MINUTES=120
VENDOO_READ_RATE_LIMIT=360
VENDOO_MUTATION_RATE_LIMIT=120
# Produktionsbereitstellung 1.37.0
# Mindestens 32 zufällige Zeichen. Wird beim ersten Admin-Setup abgefragt.
VENDOO_SETUP_TOKEN=
VENDOO_REQUIRE_SETUP_TOKEN=true
VENDOO_COOKIE_SECURE=auto
VENDOO_COOKIE_SAMESITE=lax
# Konkrete Extension-Origins, z. B. chrome-extension://abcdefghijklmnop...
VENDOO_EXTENSION_ORIGINS=
VENDOO_ALLOW_ANY_EXTENSION_ORIGIN=false
# Optionale getrennte Laufzeitpfade. Windows kann die Standardwerte leer lassen.
VENDOO_DATA_DIR=
VENDOO_DB_PATH=
VENDOO_UPLOADS_DIR=
VENDOO_BACKUP_DIR=
VENDOO_OPERATIONS_DIR=
VENDOO_LOG_DIR=
# Docker setzt automatisch /app/data/config/vendoo.env; lokal bleibt dieser Wert leer.
VENDOO_CONFIG_PATH=
# Security, Secrets & Observability (1.37.0)
# Normalerweise automatisch verwaltet; keine manuelle Bearbeitung erforderlich.
# VENDOO_SECRETS_PATH=/app/data/config/secrets.enc.json
# VENDOO_MASTER_KEY_FILE=/app/data/config/master.key
# Optional: externer 32-Byte-Master-Key als Base64 oder 64 Hex-Zeichen.
# VENDOO_MASTER_KEY=
VENDOO_JSON_LOGS=true
SMTP_TLS_REJECT_UNAUTHORIZED=true
# Coolify / GitHub production deployment
VENDOO_INITIAL_DISABLED_MODULES=vendoo.flux-studio
VENDOO_DEPLOY_PROVIDER=none
COOLIFY_API_URL=
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
VENDOO_UPDATE_CHANNEL=stable
VENDOO_BACKUP_BEFORE_DEPLOY=true
VENDOO_DEPLOY_BACKUP_UPLOADS=false
# COOLIFY_API_TOKEN and COOLIFY_DEPLOY_WEBHOOK_URL belong in the encrypted secret store.
+3
View File
@@ -0,0 +1,3 @@
* text=auto eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
+1
View File
@@ -0,0 +1 @@
* @Masterluke77
+27
View File
@@ -0,0 +1,27 @@
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
day: monday
time: '05:00'
timezone: Europe/Berlin
open-pull-requests-limit: 5
groups:
npm-production:
dependency-type: production
- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
time: '05:30'
timezone: Europe/Berlin
open-pull-requests-limit: 5
- package-ecosystem: docker
directory: /
schedule:
interval: monthly
time: '06:00'
timezone: Europe/Berlin
open-pull-requests-limit: 5
+20
View File
@@ -0,0 +1,20 @@
## Änderung
<!-- Was wurde geändert und warum? -->
## Sicherheit und Daten
- [ ] Keine `.env`, Tokens, Datenbanken, Uploads, Backups, Logs oder Modelle enthalten
- [ ] Bestehende Benutzer- und Artikeldaten bleiben erhalten
- [ ] Originalbilder werden nicht überschrieben
- [ ] FASHN VTON / Virtual Try-on wurde nicht wieder eingeführt
## Prüfung
- [ ] `npm ci`
- [ ] `npm run verify:git`
- [ ] `npm run check`
- [ ] `npm run verify:architecture`
- [ ] `npm run verify:deployment`
- [ ] `npm run verify:db`
- [ ] Windows-/Docker-/Browser-Abnahme dokumentiert oder ausdrücklich als offen markiert
+238
View File
@@ -0,0 +1,238 @@
name: Vendoo CI
on:
push:
branches: [main, develop, 'feature/**', 'release/**', 'hotfix/**']
pull_request:
branches: [main, develop]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: vendoo-ci-${{ github.ref }}
cancel-in-progress: true
jobs:
quality:
name: Syntax, Sicherheit und Release-Gates
runs-on: ubuntu-latest
timeout-minutes: 25
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: 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 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
- 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: Vollständigen Modul-Capability-Graph prüfen
run: npm run verify:capabilities
- name: Coolify-Deployment und Web-Updates prüfen
run: npm run verify:coolify
- name: Initial deaktivierte Module prüfen
run: npm run verify:initial-modules
- name: Plattformarchitektur und Design Tokens prüfen
run: npm run verify:architecture
- name: Theme Manager, Accessibility und Komponenten-Tokens prüfen
run: npm run verify:themes
- name: Security, Secrets und Observability prüfen
run: npm run verify:security
- name: Auth, Benutzer, Sitzungen und Einstellungen prüfen
run: npm run verify:identity
- name: Artikel, Lager und Medien prüfen
run: npm run verify:catalog
- name: Modulmanager, Pakete und Aktivierungsgrenzen prüfen
run: npm run verify:modules
- name: Deployment-Gate
run: npm run verify:deployment
- name: Persistenten Konfigurationsspeicher prüfen
run: npm run verify:config
- name: Geführte Shell-Installer simulieren
run: npm run verify:installer-shell
- name: Datenbankmigrationen prüfen
run: npm run verify:db
- name: Docker-Compose-Dateien validieren
run: |
cp .env.example .env
python - <<'PY'
from pathlib import Path
p=Path('.env')
s=p.read_text()
s=s.replace('VENDOO_SETUP_TOKEN=', 'VENDOO_SETUP_TOKEN=ci-only-not-a-production-secret-1234567890')
s=s.replace('VENDOO_DOMAIN=', 'VENDOO_DOMAIN=vendoo.example.invalid')
p.write_text(s)
PY
docker compose -f compose.yaml config -q
docker compose -f compose.yaml -f compose.bind-mounts.example.yaml config -q
docker compose -f compose.vps.yaml config -q
rm -f .env
- 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
+158
View File
@@ -0,0 +1,158 @@
name: Vendoo Release
on:
release:
types: [published]
permissions:
contents: read
concurrency:
group: vendoo-release-${{ github.event.release.tag_name }}
cancel-in-progress: false
env:
REGISTRY: ghcr.io
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
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Repository auschecken
uses: actions/checkout@v6
- name: Node.js 22 aktivieren
uses: actions/setup-node@v4
with:
node-version: '22.x'
cache: npm
- name: Abhängigkeiten installieren
run: npm ci
- name: Release-Tag gegen Paketversion prüfen
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
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
- name: Manifestbasiertes Release-Staging erzeugen
run: node tools/build-release.mjs
- name: ZIP und SHA-256 erzeugen
id: package
shell: bash
run: |
PACKAGE_NAME="$(cat release-output/RELEASE_NAME.txt)"
cd release-output
zip -r -9 "${PACKAGE_NAME}.zip" "${PACKAGE_NAME}"
sha256sum "${PACKAGE_NAME}.zip" > "${PACKAGE_NAME}.zip.sha256.txt"
echo "package_name=${PACKAGE_NAME}" >> "$GITHUB_OUTPUT"
- name: Assets an GitHub Release anhängen
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.event.release.tag_name }}
PACKAGE_NAME: ${{ steps.package.outputs.package_name }}
run: |
gh release upload "$TAG" \
"release-output/${PACKAGE_NAME}.zip" \
"release-output/${PACKAGE_NAME}.zip.sha256.txt" \
--clobber
- name: Build-Artefakte sichern
uses: actions/upload-artifact@v4
with:
name: ${{ steps.package.outputs.package_name }}
path: |
release-output/${{ steps.package.outputs.package_name }}.zip
release-output/${{ steps.package.outputs.package_name }}.zip.sha256.txt
if-no-files-found: error
retention-days: 30
container:
name: GHCR-Container veröffentlichen
permissions:
contents: read
packages: write
attestations: write
id-token: write
runs-on: ubuntu-latest
timeout-minutes: 35
steps:
- name: Repository auschecken
uses: actions/checkout@v6
- name: Docker Buildx aktivieren
uses: docker/setup-buildx-action@v3
- name: Bei GHCR anmelden
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Container-Metadaten erzeugen
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}},value=${{ github.event.release.tag_name }}
type=semver,pattern={{major}}.{{minor}},value=${{ github.event.release.tag_name }}
type=raw,value=stable,enable=${{ !github.event.release.prerelease }}
type=raw,value=latest,enable=${{ !github.event.release.prerelease }}
- name: Container bauen und veröffentlichen
id: push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
provenance: true
sbom: true
- name: Container-Provenienz attestieren
uses: actions/attest@v4
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
@@ -0,0 +1,71 @@
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."
@@ -0,0 +1,83 @@
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 .
@@ -0,0 +1,125 @@
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
+75 -37
View File
@@ -1,4 +1,4 @@
# Environment and secrets
# Secrets and local configuration
.env
.env.*
!.env.example
@@ -8,51 +8,89 @@
*.p12
*.pfx
*.crt
*.cer
# Node.js
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Databases and SQLite runtime files
db/*.db
db/*.db-shm
db/*.db-wal
db/*.sqlite
db/*.sqlite3
*.db-shm
*.db-wal
# SQLite runtime data
/db/*.db
/db/*.db-shm
/db/*.db-wal
/db/*.sqlite
/db/*.sqlite3
# Runtime data
uploads/*
!uploads/.gitkeep
backups/*
!backups/.gitkeep
logs/
# User and runtime data
/uploads/*
!/uploads/.gitkeep
/backups/
/logs/
/tmp/
/temp/
/sessions/
/data/
/docker-data/
*.log
tmp/
temp/
sessions/
data/
docker-data/
# Generated files and local state
coverage/
dist/
.cache/
*.pid
*.seed
# Editors and operating systems
.vscode/
.idea/
# OS and editor files
.DS_Store
Thumbs.db
desktop.ini
.vscode/
.idea/
# Local Docker overrides
# Local overrides
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/
# Historical handover and feature-package archives (never source)
/Archiv/
/archive/
/archives/
# Local update, rollback and slice archives
/updates/
# Operations Center runtime
/operations/staging/
/operations/updates/
/operations/rollback/
/operations/apply-*/
/operations/pending-operation.json
/operations/last-operation.json
# Deployment assistant state
/.vendoo-install-mode
/runtime/
# Generated deployment and release output
/release-output/
*.release.zip
*.release.sha256.txt
/.vendoo-github-state.json
# First-run and local installer state
/operations/first-admin-code.txt
/config/local/
# Installed Vendoo module packages (runtime)
/modules/
# Python cache
__pycache__/
*.pyc
+1
View File
@@ -0,0 +1 @@
Vendoo 1.20.0 image-stack cleanup completed.
+9
View File
@@ -0,0 +1,9 @@
registry=https://registry.npmjs.org/
audit=false
fund=false
strict-ssl=true
prefer-offline=true
fetch-retries=5
fetch-retry-factor=2
fetch-retry-mintimeout=10000
fetch-retry-maxtimeout=120000
+36
View File
@@ -0,0 +1,36 @@
# 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`.
+975 -11
View File
@@ -1,21 +1,985 @@
## 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
Alle wesentlichen Änderungen an Vendoo werden in dieser Datei dokumentiert.
## 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.
- Historische Ordner `Archiv/`, `archive/` und `archives/` aus Git-Staging, Release-Staging und Sicherheitsprüfung ausgeschlossen.
- Vier produktive Listings-/Inventory-Dateien als Pflichtdateien abgesichert.
- Neues Source-Root-Gate erkennt echte fehlende Module weiterhin, ignoriert aber archivierte Altstände.
- GitHub CI und Release Workflow führen das neue Gate verpflichtend aus.
## 1.41.0 Coolify Production Deployment & Controlled Web Updates
### Neu
- natives, geschütztes Modul `vendoo.deployments` für kontrollierte Produktionsdeployments.
- Coolify-Unterstützung per Deploy-Webhook oder minimal berechtigtem API-Token.
- Administratoroberfläche im Update Center für Provider, Repository, Branch, Kanal, Backup und Deploymentstatus.
- verschlüsselte Secrets `COOLIFY_API_TOKEN` und `COOLIFY_DEPLOY_WEBHOOK_URL`.
- Coolify-Produktionsvorlage `deploy/coolify.env.example` für `vendoo.flatlined.de`.
- GitHub-/Merge-/Release-Anleitung und Coolify-Klickstrecke.
- GitHub Release Workflow veröffentlicht GHCR-Tags `1.41.0`, `1.41`, `stable` und `latest` sowie SBOM und Provenienz.
- 17 registrierte Module: 15 nativ, 2 Legacy Bridges.
### Betrieb und Sicherheit
- FLUX Studio wird beim ersten Coolify-Start über `VENDOO_INITIAL_DISABLED_MODULES=vendoo.flux-studio` deaktiviert.
- persistente Administratorentscheidungen überschreiben den Initialwert bei späteren Starts.
- Deploymentauftrag nur mit Berechtigung `updates.manage` und exakter Bestätigung `DEPLOY`.
- vor einem Web-Deployment wird standardmäßig ein verifiziertes Backup erzeugt.
- Vendoo führt weder `git pull` noch Docker-Kommandos aus und erhält keinen Docker-Socket.
- Coolify-Endpunkte müssen im Produktionsmodus HTTPS verwenden.
- keine Datenbankmigration und kein Reset bestehender Daten.
## 1.40.2 Capability Graph Contract Hotfix
### Behoben
- `vendoo.module-manager` verlangte `settings.configuration`, obwohl `vendoo.settings` diese Capability nicht bereitstellte.
- Der Plattformstart brach deshalb mit `MODULE_CAPABILITY_MISSING` ab.
- `vendoo.settings` stellt nun den übergeordneten Konfigurationsvertrag zusätzlich zu `settings.application` und `settings.smtp` bereit.
### Qualitätssicherung
- neues Capability-Graph-Gate validiert den real aktivierten Gesamtgraph aller 16 Module.
- `ModuleRegistry.validateGraph()` stellt Startreihenfolge und Capability-Menge ohne Lifecycle-Start für Tests bereit.
- Architektur-, CI-, Release- und Deployment-Gates prüfen den realen Modulgraph statt ausschließlich künstlicher Testmodule.
- keine Datenbankmigration, kein Reset und keine Änderung an Nutzdaten.
## 1.40.1 FLUX Worker Lifecycle & ESM Export Contract Hotfix
### Behoben
- `server.mjs` konnte wegen des fehlenden Exports `stopFluxPromptJobLoop` nicht instanziiert werden.
- FLUX-Prompt-Job-Worker besitzt jetzt einen idempotenten Stop-Lifecycle mit `clearInterval()` und Zustandsreset.
### Qualitätssicherung
- neues generisches ESM-Export-Vertragsgate für lokale benannte Imports.
- separates FLUX-Loop-Hotfix-Gate in lokaler Prüfung, GitHub CI und Release-Workflow.
- keine Datenbankmigration, kein Reset und keine Änderung an Nutzdaten.
## 1.40.0 Native Publishing, AI, FLUX, Quality & Secure Module Manager
### Neu
- `vendoo.ai`, `vendoo.flux-studio`, `vendoo.quality`, `vendoo.publishing` und `vendoo.module-manager` als native Module.
- 16 registrierte Module: 14 nativ, 2 Legacy Bridges.
- administrative Modulmanager-Oberfläche für Aktivieren, Deaktivieren, Exportieren, Installieren und Entfernen.
- persistenter Modulstatus und separater Modulpaketspeicher.
- `.vmod`-Paketformat mit SHA-256-Integrität, sicherer Pfadprüfung und optionaler Ed25519-Signatur.
- deklarative Fremdmodule ohne ausführbaren Code; ausführbare Pakete bleiben bis zum isolierten Extension-Host in Quarantäne.
- Modulabhängigkeiten werden beim Aktivieren automatisch gestartet und beim Deaktivieren kontrolliert behandelt.
- Modulrouten werden bei deaktiviertem Besitzer mit `MODULE_DISABLED` gesperrt.
- Windows-, Docker-, NAS- und VPS-Backups enthalten Modulpakete und Aktivierungszustand.
- GitHub Draft Pull Request #13 für `release/1.40.0-module-manager`.
### Sicherheit
- geschützte Kernmodule können weder deaktiviert noch gelöscht werden.
- eingebaute Module können exportiert, aber nicht als Fremdmodule installiert werden.
- Fremdmodule werden nicht durch bloßes Kopieren aktiviert.
- der lokale Laufzeitordner `modules/` wird von Git- und Release-Staging ausgeschlossen.
- keine ungeprüfte Ausführung von Drittcode im Vendoo-Hauptprozess.
## 1.39.0 Listings, Inventory & Media Native Module Migration
### Neu
- `vendoo.listings`, `vendoo.inventory` und `vendoo.media` als native Plattformmodule.
- fünfzehn registrierte Module: neun nativ, sechs Legacy Bridges.
- öffentliche Artikel-, Papierkorb-, Lager- und Medienrouten aus `server.mjs` entfernt und über Modulverträge registriert.
- Repository-Injektion verhindert Datenbankzugriffe als Import-Seiteneffekt.
- eigenes `verify:catalog`-Gate mit Service-Regressionstests.
- GitHub Draft Pull Request #12 für `release/1.39.0-catalog-media`.
### Sicherheit und Datenintegrität
- Artikel-Payload-Allowlist blockiert unbekannte Felder, ungültige Preise, überlange Texte und Listen.
- Rich-HTML wird weiterhin serverseitig bereinigt.
- bestehende Bearbeitungssperren bleiben für Artikelupdates verbindlich.
- endgültiges Löschen ist ausschließlich für Artikel im Papierkorb möglich.
- Lager-Bulk-Aktionen sind auf 500 eindeutige positive IDs begrenzt.
- Medienpfade blockieren Traversal, Nullbytes und Zeilenumbrüche.
- Bilder, die von aktiven Artikeln referenziert werden, werden nicht gelöscht.
- unbekannte Medien- und Lagerfelder werden serverseitig abgewiesen.
### Kompatibilität
- keine destruktive Datenbankmigration und kein Reset.
- bestehende Artikel, Bilder, Uploads, Favoriten, Lagerorte und Publishing-Verknüpfungen bleiben erhalten.
- API-Pfade und bestehendes Frontend bleiben kompatibel.
- `lib/db.mjs` bleibt vorübergehend Kompatibilitätsschicht für AI, Publishing, Quality und Dashboard.
## 1.37.0 Security, Secrets & Observability Foundation
### Neu
- `vendoo.security` als natives Kernmodul.
- AES-256-GCM-verschlüsselter Secret-Speicher mit separatem Master-Key.
- Automatische Migration vorhandener Runtime- und SMTP-Secrets mit Sicherungskopie.
- Strikte Script-CSP ohne Inline-Skripte und Inline-Eventhandler.
- Request- und Correlation-IDs sowie strukturierte JSON-Telemetrie.
- Zentrale Input-Schema-Validierung für neue Kernel-Routen.
- Erweitertes Security-Dashboard für Secret-Provider, CSP und Requeststatus.
- SMTP-Zertifikatsprüfung standardmäßig aktiv.
- GitHub Draft Pull Request #10 für `release/1.37.0-security-foundation`.
### Kompatibilität
- Keine Datenbankablösung und keine destruktive Migration.
- Windows, Docker, NAS und VPS bleiben unterstützt.
- Theme Manager und vorhandene Daten bleiben erhalten.
## 1.36.0 Secure Theme Manager, Accessibility & Component Token Foundation
### Neu
- `vendoo.themes` als erstes natives Fachmodul auf dem Plattformkernel.
- Persönliche Theme-Präferenzen pro Benutzer mit Theme, UI-Dichte und reduzierter Bewegung.
- Sichere Theme-Profile für Administratoren mit Basis-Theme, Token-Overrides, Import, Export und Systemstandard.
- Live-Vorschau im Browser; Speichern erst nach expliziter Bestätigung.
- Serverseitige Validierung aller Theme-Werte und WCAG-orientierte Kontrastprüfung zentraler Text-/Oberflächenpaare.
- Light, Dark, System und High Contrast als integrierte Modi.
- 42 typisierte Design Tokens, darunter erste Komponenten-, Fokus- und Touch-Target-Tokens.
- Tokenisierte Buttons, Eingabefelder, Karten, Dialoge und Fokuszustände.
- Deny-by-default API-Verträge für Theme-Routen sowie getrennte Benutzer- und Administrator-Policies.
- Audit-Protokoll für persönliche Präferenzen, Theme-Profile und Systemstandard.
- Eigenes `verify:themes`-Gate in lokaler Prüfung, GitHub CI und Release-Workflow.
- GitHub Draft Pull Request #9 für `release/1.36.0-theme-manager`.
### Sicherheit und Kompatibilität
- Keine freie CSS-, JavaScript-, URL- oder Dateipfad-Eingabe.
- Unbekannte Tokens und Theme-IDs werden abgewiesen.
- Normale Benutzer dürfen ausschließlich ihre eigene Darstellung ändern.
- Systemstandard und benutzerdefinierte Profile erfordern administrative Berechtigung.
- Bestehende Daten, Benutzer, Artikel, Uploads, Backups und FLUX-Daten bleiben erhalten.
- Kein Reset, keine Framework-Migration und keine Datenbankablösung.
## 1.35.0 Modular Platform Kernel, Design-System Contracts & Security Architecture Foundation
### Neu
- Modularer Plattformkernel mit Lifecycle, Event Bus, Feature Flags, Modul- und Policy-Registry.
- Zwölf bestehende Fachbereiche als validierte `legacy-bridge`-Module mit expliziten Abhängigkeiten, Capabilities und Ownership.
- Deny-by-default Route Registry: neue Kernel-Routen benötigen Besitzer und explizite Policy.
- Administrativ geschützte Plattform-, Modul- und Theme-Vertragsendpunkte.
- Kernel-Healthcheck in `/readyz` und geordneter Kernel-Shutdown.
- Versionierte Design-Token-Quelle mit 33 Tokens für Light, Dark und High Contrast.
- Deterministischer Token-Generator und browserseitige Theme Runtime.
- Sichere Theme-Definitionen: nur freigegebene Farb- und Dimensionswerte, kein freies CSS/JS.
- Modul-JSON-Schema, Architekturgrenzen, Migrationsroadmap und Security-Architekturdokumentation.
- Neues `npm run verify:architecture` mit Zyklus-, Policy-, Event-, Theme- und Token-Regressionstests.
- GitHub Draft Pull Request #7 für `release/1.35.0-platform-kernel`.
### Kompatibilität
- Kein Reset, keine Framework-Migration, keine Datenbankablösung.
- Windows-Lokalbetrieb, Docker, NAS, VPS, Zero-Edit-Installer und Git-Sicherheitsgate bleiben erhalten.
- Bestehende CSS-Variablen bleiben über eine Token-Kompatibilitätsbrücke funktionsfähig.
- Bestehende Routen werden in 1.35.0 noch nicht massenhaft verschoben; die Migration erfolgt fachmodulweise.
## 1.34.3 Git-Staging Update-Archiv-Ausschluss Hotfix
### Behoben
- Der lokale Ordner `updates/` wird als Update-, Rollback- und Slice-Artefaktspeicher behandelt und nicht nach GitHub oder in Release-Staging-Verzeichnisse übernommen.
- Alte Sicherungskopien wie `updates/backups/.../setup.bat`, `setup.ps1` oder `scripts/install-local-flux.ps1` lösen das Git-Sicherheitsgate nicht mehr aus, weil sie bereits vor dem Scan ausgeschlossen werden.
- Die aktiven Installationsdateien im Projektroot und unter `scripts/` bleiben Teil des Quellimports.
- Das Git-Sicherheitsgate blockiert `updates/` weiterhin, falls ein fremder Kopierweg den Ordner dennoch in das Staging einschleust.
- `.gitignore` schützt den lokalen Updateverlauf zusätzlich vor versehentlichen Commits.
### Qualitätssicherung
- Regressionstest mit den drei konkret gemeldeten Pfaden aus `slice22_1_20260707-194953`.
- Release-Builder und GitHub-Deploy-Assistent verwenden weiterhin dieselbe Filterfunktion.
- Neuer Importbranch `release/1.34.3-initial-import` und Draft Pull Request #6.
## 1.34.2 Git-Sicherheitsdiagnose & gemeinsamer Staging-Builder
### Behoben
- Die Meldung `Git-Sicherheitsgate fehlgeschlagen (4)` wird nicht mehr ohne Detailzeilen ausgegeben. Der Windows-Assistent fängt stdout und stderr getrennt ab und zeigt jeden konkreten Treffer an.
- Der vollständige Fehlerbericht wird unter `logs/github-deploy-safety-report.txt` gespeichert und unter Windows automatisch im Editor geöffnet.
- Windows, Linux und macOS verwenden denselben Node-basierten Staging-Builder statt unterschiedlicher Robocopy-/rsync-Regeln.
- Lokale `.env.*`-Dateien, Operations-Daten, Uploads, Backups, Logs, Datenbanken, Schlüsseldateien und FLUX-/ComfyUI-Laufzeitdaten werden vor dem Sicherheitsgate automatisch aus dem temporären Git-Staging ausgeschlossen.
- `.env.example` und `.env.docker.example` bleiben als sichere Vorlagen enthalten.
- Der Staging-Builder leert nur den Arbeitsbaum des temporären Klons und erhält dessen `.git`-Verzeichnis vollständig.
### Qualitätssicherung
- Neuer plattformübergreifender Git-Staging-Regressionstest.
- Der Test belegt, dass lokale Secrets und Laufzeitdaten ausgeschlossen, öffentliche Vorlagen übernommen und `.git` nicht verändert werden.
- CI und Release-Workflow führen den Staging-Regressionstest zusätzlich zum Secret-Regressionstest aus.
- Neuer Importbranch `release/1.34.2-initial-import` und Draft Pull Request #5.
## 1.34.1 Git-Sicherheitsgate Windows Hotfix
### Behoben
- Windows-Pfade werden im Git-Sicherheitsgate mit `fileURLToPath(import.meta.url)` in echte Dateisystempfade umgewandelt.
- Der Sicherheitscheck scannt den vollständigen Import-Arbeitsbaum und nicht nur bereits von Git verfolgte Dateien.
- Noch unversionierte `.env.local`, `.env.production`, Token-Dateien und sonstige Secrets werden vor `git add` erkannt.
- Node.js ist für den sicheren Import verpflichtend; ein stilles Überspringen des erweiterten Scans ist nicht möglich.
## 1.34.0 GitHub CI/CD & Zero-Edit Installation Foundation
### Installation
- Grafischer Windows-Installationsassistent für lokalen Node.js-Betrieb und Docker Desktop.
- Geführte Linux-/NAS-/VPS-Konfiguration ohne manuelles Bearbeiten von `.env` oder Compose-Dateien.
- Automatische Erzeugung und sichere Ablage des Erst-Admin-Setup-Codes.
- VPS-Modus mit integriertem Caddy-Reverse-Proxy und automatischem HTTPS.
- Konfiguration kann später über den Assistenten geändert werden, ohne bestehende Daten zu löschen.
- Persistenter Runtime-Konfigurationsspeicher für Eingabemasken im gehärteten read-only Container.
- Simulierbares Shell-Installer-Gate für Docker-, NAS- und VPS-Modus; dabei wurde die Standardwert-Abfrage gegen Prompt-Vermischung gehärtet.
### GitHub und Releases
- Git-Sicherheitsgate gegen Secrets, Datenbanken, Uploads, Backups, Logs, Runtime- und Modellordner.
- GitHub Actions für CI, Docker-Build, Release-Pakete, SHA-256 und GHCR-Images.
- Windows- und Shell-GitHub-Deploy-Assistent für kontrollierten Erstimport auf einen Release-Branch.
- Privater Branch `release/1.34.0-initial-import` und Draft Pull Request #3 real vorbereitet.
- Release-Pakete werden aus einem bereinigten Staging-Verzeichnis erzeugt und nicht als Binärdateien in die Git-Historie aufgenommen.
### Sicherheit
- GitHub Actions besitzen jobbezogen minimale Berechtigungen.
- GHCR-Images erhalten eine Provenienz-Attestierung auf Basis des tatsächlich veröffentlichten Image-Digests.
- Container-Registry-Veröffentlichung erfolgt nur bei veröffentlichten GitHub Releases und passendem Versionstag.
- Produktions-Secrets bleiben in `.env` beziehungsweise GitHub Environments und niemals im Repository.
## 1.33.0 Multiplatform Deployment Wizard & Flux/Admin Fixes
- Installationsziel-Assistent für Windows lokal, Docker Desktop, NAS/Portainer und VPS/Linux
- neues `setup.sh` für Linux-, NAS- und VPS-Zielsysteme
- Installationsmodus wird für Start, Update, Reparatur, Status und Deinstallation gespeichert
- Docker-Host-Gateway für einen getrennten FLUX/ComfyUI-Dienst
- FLUX Studio startet ohne automatische Bildauswahl
- Reset-Button leert den Arbeitsbereich, ohne Verlauf, Bilder oder Jobs zu löschen
- Benutzerbearbeitung auf stabile Event-Listener und Cachelogik umgestellt
- Passwortvalidierung vor dem Update und sanitisierte Benutzerantwort
## 1.32.0 Docker, NAS & Production Deployment Foundation
- produktionsgeeignetes, mehrstufiges Dockerfile mit Lockfile-Installation
- Compose mit getrennten persistenten Volumes, non-root, read-only und Healthcheck
- zentrale, konfigurierbare DB-/Upload-/Backup-/Operations-/Log-Pfade bei voller Windows-Kompatibilität
- `/readyz` mit SQLite- und Schreibpfadprüfung
- geordneter SIGTERM-/SIGINT-Shutdown mit WAL-Checkpoint
- geschütztes Erst-Admin-Setup über `VENDOO_SETUP_TOKEN`; übrige API bleibt bis zum Setup gesperrt
- konfigurierbare Proxy- und Cookie-Regeln
- produktive Allowlist für Browser-Extension-Origins
- Uploads nur angemeldet abrufbar und strengere Dateitypprüfung
- NAS-, Portainer-, Backup-, Restore-, Update- und Rollback-Dokumentation
- FLUX/ComfyUI bleibt getrennt und ist im Hauptcontainer standardmäßig deaktiviert
## 1.31.0 Mehrbenutzer- und Server-Sicherheitsgrundlage
- Fünf feste Rollen mit serverseitig erzwungenen Berechtigungen: Administrator, Manager, Editor, Publisher und Leser.
- Sichere Sitzungen mit absoluter und inaktiver Laufzeit, Widerruf, Benutzer-Sitzungsverwaltung und Login-Sperre.
- Bearbeitungssperren für Artikel mit automatischer Verlängerung und kontrollierter Freigabe.
- Filterbares und exportierbares Audit-Protokoll.
- Rate Limits, Host-/Origin-Freigaben, Request-IDs, CSP/Helmet und standardmäßige Bindung an localhost.
- Benutzer- und Sicherheitsoberfläche mit Rollenmatrix, aktiven Sperren und Sicherheitsstatus.
- Additive Migrationen; bestehende Artikel, Datenbank, Bilder und Einstellungen bleiben erhalten.
## 1.30.0 Operations Center: Backup, Restore, Updates & Extension-Diagnose
- Neue Betriebszentrale unter Admin → System mit Backup-, Update- und Extension-Diagnose-Tabs.
- Konsistente SQLite-Backups über die Backup-API statt Kopieren der geöffneten Datenbank.
- Vollbackups mit Uploads, optionaler `.env`, Manifest, SHA-256-Dateiprüfsummen und SQLite-Integritätsprüfung.
- Tägliche rotierende automatische Backups mit konfigurierbarer Uhrzeit, Aufbewahrung und Upload-Option.
- Verifizierte Wiederherstellung mit automatischem Sicherheitsbackup und bewusstem Offline-Anwenden.
- Update Center prüft Version, Pflichtdateien und verbotene Laufzeitdaten, bevor ein Paket vorbereitet wird.
- Setup-Menüpunkt 11 wendet vorbereitete Update-/Restore-Operationen an; Menüpunkt 12 führt den Rollback aus.
- Migrationsstatus und letzte Systemoperation sind in der Oberfläche sichtbar.
- Vendoo Link 1.9.0 sendet Diagnose-Heartbeats; Chrome, Edge, Firefox und Safari werden getrennt ausgewertet.
- Keine Datenbank, Uploads, `.env`, API-Schlüssel oder Modelle werden durch ein Update-Paket gelöscht.
## 1.29.0 Slice 34: Publishing Hardening
- plattformübergreifende persistente Publishing-Queue
- Doppelveröffentlichungsschutz und Idempotenzprüfung
- automatische Retry-Strategie und Neustart-Wiederaufnahme
- Fehlerzentrale, Ereignisprotokoll, externe IDs und Links
- eBay Sync, Update und Beenden über offizielle Inventory API
- Etsy Statusabgleich
- Extension-Aufträge für Vinted und Kleinanzeigen
- Scheduler an die gehärtete Queue angebunden
- Vendoo Link 1.8.0
## 1.28.0 Slice 33: Artikel Quality Center
- Neuer Hauptmenüpunkt **Qualitätscenter** mit Score, Noten AE, Blockern, Warnungen und Empfehlungen.
- Serverseitige Prüfung von Titel, Beschreibung, Pflichtfeldern, Preis, Organisation, Suchbegriffen und plattformbezogenen Vendoo-Profilen.
- Vollständige Bildprüfung mit Dateiexistenz, Lesbarkeit, Abmessungen, Format und Auflösungswarnungen.
- Erkennung möglicher Duplikate über gleiche SKU und Titelähnlichkeit.
- Plattformbereitschaft, Gebühren und geschätzter Erlös gemeinsam im Prüfbericht.
- Persistenter Score-Verlauf je Artikel mit Vergleich zur vorherigen Analyse.
- Filter, Suche und Pagination für bereite, blockierte und zu prüfende Artikel.
- AI-Verbesserungen für Titel, Beschreibung und Tags über den aktuell aktiven Text-AI-Provider.
- AI erzeugt ausschließlich Vorschläge; Änderungen werden erst nach sichtbarem Vorher-/Nachher-Vergleich und ausdrücklicher Bestätigung gespeichert.
- Keine automatische Erfindung von Produktmerkmalen, Marken, Schäden oder Lieferumfang.
## 1.27.0 Slice 32: Batch Image Factory
- Neuer Hauptmenüpunkt **Bildproduktion** als persistente Batch Image Factory.
- Mehrere Quellbilder direkt aus der zentralen Bildergalerie auswählen oder aus der Galerie übergeben.
- Acht integrierte Produktionsprofile für eBay, Vinted, Kleinanzeigen, Etsy, Instagram, Webshop und optimierte Originale.
- Eigene Produktionsprofile mit Zielgröße, Anpassung, Hintergrund, Format, Qualität und Dateinamensschema anlegen, bearbeiten und löschen.
- Mehrere Profile pro Auftrag erzeugen mehrere Ausgaben je Originalbild.
- Optionale Hintergrundentfernung mit Intensität sowie Wasserzeichen mit Text, Position und Deckkraft.
- Persistentes Job-Center mit 5/10/15 Aufträgen pro Seite, Pause, Fortsetzen, Abbruch, Gesamt-Retry und Einzel-Retry.
- Neustart-Wiederaufnahme für wartende und laufende Aufträge; pausierte Aufträge bleiben pausiert.
- Ergebnisse werden nicht destruktiv als neue Bildversionen unter `uploads/batch/` gespeichert und erscheinen automatisch in der Bildergalerie.
- Fertige Ergebnisse eines Auftrags können gesammelt als ZIP heruntergeladen werden.
- Bildrenderer um `contain`, `cover`, exakte Ziel-Canvas, Ausgabeordner und kontrollierte Dateinamen erweitert.
## 1.26.1 Stabilitäts-, Responsive- und Regression-Gate
- Zentrale Systemprüfung im Adminbereich ergänzt: SQLite-Integrität, Schreibrechte, UI-Dateien, FLUX-Workflow, Browser-Extensions, AI-Konfiguration und optionale ComfyUI-Erreichbarkeit.
- Öffentlichen, geheimnisfreien `/healthz`-Endpunkt für lokale Verbindungsüberwachung ergänzt.
- API-Client um Request-IDs, definierte Zeitlimits, einmaligen GET-Retry bei Netzwerk-/Serverfehlern und strukturierte Fehlerprotokollierung erweitert.
- Sichtbare Verbindungsleiste bei Serverausfall mit automatischer Wiederholungsprüfung ergänzt.
- Unbehandelte JavaScript- und Promise-Fehler werden lokal mit Build, Seite und Request-ID für die Diagnose erfasst.
- Responsive-Verhalten auf die tatsächlich verfügbare Workspace-Breite umgestellt; `ResizeObserver` vergibt Breitenklassen unabhängig von der Browserbreite.
- Globale Schutzregeln gegen horizontales Überlaufen von Formularen, Bildern, Vorschauen, Werkzeugleisten und langen Inhalten ergänzt.
- Neue statische Release- und UI-Vertragsprüfungen verhindern doppelte IDs, tote Navigation, fehlende Diagnoseelemente und veraltete Buildmarker.
- Keine Datenbankstruktur, Nutzerdaten, API-Schlüssel, Extensions oder FLUX-Modelle verändert.
## 1.26.0 Artikel-Navigation, Publish-Reparatur, Zoom-Galerie & Extension Center
- „Neues Listing“ konsequent in „Neuer Artikel“ und „Listing generieren“ in „Artikel generieren“ umbenannt.
- „Neuer Artikel“ als eigener Hauptmenüpunkt direkt unter dem Dashboard ergänzt; „Listings“ heißt in der Navigation jetzt „Artikel“.
- Publish-Datenaufbereitung gegen fehlerhafte ältere JSON-Felder gehärtet und einen Scope-Fehler im direkten Publish-Workspace behoben.
- Smart Copy öffnet wieder zuverlässig als Popup; die mittlere Publish-Fläche zeigt Titel, Beschreibung, Tags, Metadaten und Fotos.
- Medienvorschau um Fit-, 100-%-, Stufen- und Mausrad-Zoom sowie Verschieben bei Vergrößerung erweitert.
- Galerie-Auswahl und Favoriten auf kleine, konsistente Lucide-basierte SVG-Icons umgestellt.
- Gebührenrechner für private Verkäufer aktualisiert: eBay.de, Vinted und normaler Kleinanzeigen-Privatverkauf ohne Verkäuferprovision; Etsy separat als Shop-Kalkulation.
- Neuer Menüpunkt „Extensions“ mit Browsererkennung, Ein-Klick-Vorbereitung, Pfadkopie und Downloadpaketen für Chrome, Edge, Firefox und Safari.
- Safari-WebExtension-Quelle samt Xcode-Konvertierungsskript ergänzt.
- Doppelte Settings-ID und doppelten Galerie-Event-Listener bereinigt.
## 1.25.1 Adaptive Bildergalerie
- Galerie-Raster vollständig auf automatische Kartenbreiten mit `auto-fit` und Container Queries umgestellt.
- Die gesamte Bildfläche öffnet jetzt die Lightbox; die übergroße Vorschau-/Öffnen-Schaltfläche wurde entfernt.
- Auswahl und Favorit skalieren mit der Kartenbreite und bleiben sauber in den Bildecken ausgerichtet.
- Aktionsleiste auf vier kompakte, adaptive Bildaktionen umgestellt; auf sehr schmalen Karten werden nur Symbole gezeigt.
- Auswahlstatus wird zusätzlich an Karte und Bildfläche sichtbar, ohne das Motiv zu verdecken.
- Seitennavigation mit direkter Seitenauswahl und Bildbereich wird oberhalb und unterhalb der Galerie angezeigt.
- Listenansicht und schmale Bildschirmbreiten wurden im selben Komponentenmodell neu gehärtet.
## 1.25.0 Responsive Detail Editor, Gallery UX, Smart Copy Modal & FLUX Prompt Lab
- Listing-Bearbeiten reagiert jetzt auf die tatsächlich verfügbare Inhaltsbreite per Container Queries und bleibt auch mit Sidebar, Quill, Vorschau und Kontextkarten ohne horizontalen Überlauf bedienbar.
- Die zentrale Bildergalerie erhielt sauber ausgerichtete Auswahlfelder, Favoriten-Overlay, eine eindeutige Vorschauaktion, zweizeilige Titel und ein stabiles 2×2-Aktionsraster.
- Publish-Aktualisieren rotiert nicht mehr; während des Ladens werden ruhige Textzustände und `aria-busy` verwendet.
- Smart Copy öffnet wieder als eigenes responsives Arbeitsfenster mit Kopierfeldern, Fotos, Plattformstart und Veröffentlichungsstatus.
- FLUX Ergebnisse & Favoriten besitzen Pagination mit 5, 10 oder 15 Bildern pro Seite.
- Das FLUX Job-Center ist auf 5, 10 oder 15 Aufträge pro Seite begrenzt.
- Neues FLUX Prompt Lab: lokale Strukturierung oder AI-Veredelung über den aktuell aktiven Claude-, OpenAI-, OpenRouter- oder Ollama-Provider.
- Prompt Lab schützt auf Wunsch Motivmerkmale und vermeidet erfundene Texte, Logos und Wasserzeichen.
- Zusätzliche Breitenhärtung für Publish, Medienbibliothek, FLUX, Einstellungen und Generator.
## 1.24.0 Responsive UI, FLUX-Auftragsarchiv & zentrale Bildergalerie
- Listing-Seite auf schmalen Breiten von einer starren Mindestbreite auf echte responsive Kartenzeilen umgestellt.
- Responsive Härtung für Filter, Batch-Aktionen, FLUX-Job-Center, Formulare, Aktionsleisten und Medienverwaltung ergänzt.
- Hintergrundentfernung im Advanced Editor um eine regelbare Intensität von 0 bis 100 erweitert.
- FLUX-Aufträge können einzeln oder gesammelt archiviert und aus dem Archiv wiederhergestellt werden.
- Im FLUX-Archiv können einzelne, mehrere oder alle Aufträge endgültig gelöscht werden; erzeugte Bilder bleiben erhalten.
- Serverseitige Pagination und Seitengröße für aktive und archivierte FLUX-Aufträge ergänzt.
- Neuer Navigationsbereich „Bilder“ als zentrale Galerie für FLUX-, bearbeitete, AI- und Listing-Bilder.
- Galerie mit Raster-/Listenansicht, Suche, Quellenfiltern, Favoriten, Sortierung, Pagination, Mehrfachauswahl, ZIP-Download, Lightbox, Editor-Übergabe und geschützter Löschung aktiver Listing-Bilder.
- Vinted-Titelfeldschutz aus Vendoo Link 1.6.1 unverändert beibehalten.
## 1.23.2 Slice 31.2 Hotfix: FLUX-Layout, Editor-Werkzeuge & kompakte Aufträge
- „System & ComfyUI“ aus der angehefteten FLUX-Seitenspalte entfernt und direkt unter „Prompt & Auftrag“ eingeordnet.
- FLUX Job-Center als Ganzes ein- und ausklappbar gemacht.
- Einzelne FLUX-Aufträge kompakt auf- und zuklappbar; aktive Aufträge sind standardmäßig geöffnet, abgeschlossene kompakt geschlossen.
- „Alle einklappen“ ergänzt, damit lange Verläufe die Seite nicht unnötig vergrößern.
- Separate Buttons für Hintergrundentfernung und Wasserzeichen aus Generator und Bildleisten entfernt.
- Hintergrundentfernung in den Advanced Image Editor integriert und als nicht destruktive Bildversion gespeichert.
- Wasserzeichen mit eigenem Text, fünf Positionen und einstellbarer Deckkraft in den Advanced Image Editor integriert.
- Editor-Werkzeuge gelten einheitlich für Generator-, Listing- und FLUX-Bilder.
- Vendoo Link 1.6.1 und der Vinted-Titelfeldschutz bleiben unverändert erhalten.
## 1.23.1 Slice 31.1 Hotfix: Editor-Sichtbarkeit & Vinted-Feldschutz
- Advanced Image Editor erhält im Generator einen deutlich sichtbaren eigenen Einstieg.
- `app.js` und `style.css` werden versionsgebunden geladen; HTML/JS/CSS werden vom Vendoo-Server nicht mehr aus veraltetem Browser-Cache ausgeliefert.
- Der Windows-Starter öffnet Vendoo mit Build-Kennung.
- Vinted-Auto-Fill ordnet Felder nur noch über eindeutige Selektoren bzw. tatsächlich zugeordnete Labels zu.
- Zustands-, Marken-, Größen- und Farbauswahl dürfen das Titelfeld nicht mehr als Fallback verwenden.
- Vendoo Link auf Version 1.6.1 angehoben.
# Changelog
## 1.40.2 Capability Graph Contract Hotfix
### Behoben
- `vendoo.module-manager` verlangte `settings.configuration`, obwohl `vendoo.settings` diese Capability nicht bereitstellte.
- Der Plattformstart brach deshalb mit `MODULE_CAPABILITY_MISSING` ab.
- `vendoo.settings` stellt nun den übergeordneten Konfigurationsvertrag zusätzlich zu `settings.application` und `settings.smtp` bereit.
### Qualitätssicherung
- neues Capability-Graph-Gate validiert den real aktivierten Gesamtgraph aller 16 Module.
- `ModuleRegistry.validateGraph()` stellt Startreihenfolge und Capability-Menge ohne Lifecycle-Start für Tests bereit.
- Architektur-, CI-, Release- und Deployment-Gates prüfen den realen Modulgraph statt ausschließlich künstlicher Testmodule.
- keine Datenbankmigration, kein Reset und keine Änderung an Nutzdaten.
## 1.23.0 Slice 31: Advanced Image Editor
- Bestehenden einfachen Fotoeditor durch einen nicht destruktiven Produkt- und FLUX-Bildeditor ersetzt.
- Undo/Redo und eine vollständige Bearbeitungshistorie innerhalb der aktuellen Sitzung ergänzt.
- Persistente Bildversionen in `image_edit_versions` eingeführt; das Original bleibt unverändert und jederzeit wiederherstellbar.
- Vorher-/Nachher-Ansicht und Auswahl älterer Bearbeitungsversionen integriert.
- Freien Zuschnitt sowie 1:1, 4:5, 3:4, 16:9 und Original-Seitenverhältnis ergänzt.
- Drehen, horizontales/vertikales Spiegeln und stufenloses Begradigen umgesetzt.
- Belichtung, Helligkeit, Kontrast, Lichter, Schatten, Temperatur, Sättigung und Schärfe ergänzt.
- Export als JPEG, WebP oder PNG mit Qualitätsregler und Zielgröße umgesetzt.
- Bearbeitungen werden serverseitig mit `sharp` in voller Auflösung gerendert und immer als neue Datei gespeichert.
- Hintergrundentfernung, Inpainting, Outpainting und Maskeneditor bleiben bewusst außerhalb dieses Slices; es gibt keine Platzhalterbuttons.
## 1.22.0 Slice 30: FLUX Studio Pro
- Persistentes FLUX-Job-Center mit SQLite-Aufträgen und einzelnen Varianten eingeführt.
- 1, 2 oder 4 Varianten, eindeutige Seeds und reproduzierbarer Seed Lock ergänzt.
- „Ähnlich erneut erzeugen“, Favoriten und vollständige Prompt-/Parameterhistorie umgesetzt.
- Abbruch, Wiederholen, Queue-Bereinigung und Neustart-Wiederaufnahme ergänzt.
- ComfyUI-Warteschlange, Historie, System-, GPU- und VRAM-Daten angebunden.
- Fortschrittsanzeige des FLUX Studios auf reale Phasen ohne geschätzte Prozentwerte umgestellt.
- Freier FLUX-Promptworkflow bleibt ohne Produktreferenz und ohne `LoadImage`.
- FASHN VTON und Virtual Try-on bleiben vollständig entfernt.
## 1.21.0 Slice 29: FLUX Quality & Settings Tabs
- Einstellungsbereiche als horizontale Top-Tabs neu gestaltet.
- FLUX Studio mit Prompt-Check, lokalem Prompt-Assistenten und Qualitätsprofilen erweitert.
- FLUX.1-schnell standardmäßig auf vier Schritte optimiert; Detailprofil mit sechs Schritten.
- Negative Prompt im reinen FLUX-Promptmodus entfernt.
- Startprofile Auto, Schnell, Ausgewogen und Wenig VRAM ergänzt.
- ComfyUI-Start verwendet je nach Profil Cache-/VRAM- und optionale Fast-Flags.
- Laufzeit und final strukturierter Prompt werden je Bild gespeichert.
- Vorschau und Bearbeitungsaktionen visuell aufgewertet.
## 1.20.1 FLUX Runtime Startdiagnose und Health-Check
- FLUX/ComfyUI-Start wartet jetzt bis zu 180 Sekunden auf den echten `/system_stats`-Health-Check statt nach 2,55 Sekunden vorschnell „gestoppt“ zu melden.
- Eigene Runtime-Statusdatei mit PID, Laufzeit, Startphase sowie bereinigten stdout-/stderr-Auszügen.
- ANSI-Farbcodes werden aus den angezeigten Runtime-Logs entfernt.
- Prozessabbruch während des Starts wird sofort erkannt und mit den letzten relevanten Logzeilen gemeldet.
- Das reine Vendoo-FLUX-Studio startet ComfyUI ohne benutzerdefinierte Nodes; der verwendete Workflow benötigt ausschließlich eingebaute Standard-Nodes.
- Start- und Stopp-Skripte liegen jetzt als feste Paketdateien vor und werden beim Drüberkopieren sofort aktualisiert.
- FLUX Studio und Einstellungen zeigen während des Starts laufend „Wird gestartet“ statt einen falschen Stoppstatus.
## 1.20.0 FASHN/VTO entfernt und eigenständiges FLUX Studio
- FASHN VTON, Modelbibliothek, lokale VTO-Dienste, API-Routen, UI, Installationsskripte und Einstellungen vollständig aus dem aktiven Produkt entfernt.
- Bestehende VTO-Runtime, Autostarts, Logs und alte VTO-Einstellungen werden beim ersten Setup-Lauf entfernt; eigene alte Modelbilder werden nach `uploads/ai-generated/legacy-models` gesichert.
- Neuer Hauptmenüpunkt **FLUX Studio** mit freiem Prompt, Stil, Format, Seed und Schritten.
- Live-Status und Start/Stopp-Steuerung für das lokale ComfyUI/FLUX.
- Vorschau, Download, Bildbibliothek, Löschen und Übernahme in den Listing-Generator.
- Nachträglicher Bildeditor für Zuschneiden, Drehen, Spiegeln, Helligkeit, Kontrast und Sättigung. Bearbeitungen werden als neue Version gespeichert.
- Neue Tabelle `flux_images`; aufgegebene VTO-Tabelle und `ai_vto_*`-Einstellungen werden aus bestehenden Datenbanken entfernt.
- FLUX bleibt ein reiner Prompt-Bildgenerator; Ghost Mannequin und Flatlay bleiben als separate Produktbild-Werkzeuge bestehen.
## [1.15.0] - 2026-07-07
### Neu
- Komplett neu strukturierte Einstellungszentrale mit Bereichen für Allgemein, AI & APIs, Marktplätze und lokales FLUX.
- Großer FLUX-Live-Status mit klarer Anzeige für Aktiv, Gestoppt, Deaktiviert und Nicht installiert.
- Lokales FLUX kann direkt aktiviert, deaktiviert, gestartet, gestoppt, repariert, aktualisiert und sicher deinstalliert werden.
- Sichere FLUX-Deinstallation entfernt ComfyUI und das Modell, behält standardmäßig aber die Installationsarchive für eine schnelle Neuinstallation.
- Einstellbare Produktreferenz-Treue für AI-Model-Fotos.
- Neuer Geschwindigkeitsmodus mit 768 × 768 Pixeln als Standard; 1024 × 1024 bleibt als Qualitätsoption verfügbar.
### Behoben
- Produktfotos werden bei lokalem FLUX jetzt tatsächlich an ComfyUI übertragen und als Image-to-Image-Referenz in den Workflow eingebunden.
- Hoodie, Farbe, Kapuze, Reißverschluss, Aufdrucke, Logos und Schnitt werden im Prompt zusätzlich als verbindliche Produktmerkmale geschützt.
- AI-Bildjobs aktualisieren ihren Fortschritt jetzt während Vorbereitung, Upload, Rendering, Sicherheitsprüfung und Speicherung statt dauerhaft bei 10 % zu stehen.
- Alte oder beschädigte Installationsstatusdateien können nicht mehr zu `unknown · Bereitschaft 100%` führen, wenn die reale Installation vollständig vorhanden ist.
- Der konfigurierte Installationspfad `local-ai/comfyui` wird korrekt auf den enthaltenen Portable-Ordner aufgelöst.
### Technik
- Neue ComfyUI-Referenzpipeline: UploadImage → LoadImage → ImageScale → VAEEncode → KSampler.
- Neue Datenbankspalte `ai_model_jobs.progress_message` als additive Migration.
- BOM-sicheres Lesen des FLUX-Installationsstatus.
- Neuer PowerShell-5.1-kompatibler Uninstaller `scripts/uninstall-local-flux.ps1`.
## [1.14.6] - 2026-07-07
### Behoben
- Lokaler FLUX-Workflow wird unter Windows PowerShell 5.1 als UTF-8 ohne BOM geschrieben.
- Bereits vorhandene Workflow-Dateien mit UTF-8-BOM werden beim Einlesen robust bereinigt.
- `Unexpected token '' ... is not valid JSON` bei der lokalen AI-Bildgenerierung behoben.
- Installer-Status-JSON wird ebenfalls ohne BOM geschrieben, damit Node.js und andere JSON-Parser es zuverlässig lesen können.
# 1.14.5 Slice 22.3 PowerShell 5.1 Parser Fix
- Behebt den PowerShell-5.1-Parserfehler `InvalidVariableReferenceWithDrive` im sichtbaren FLUX-Downloadstatus.
- Ersetzt die beiden ungültigen Zeichenfolgen `$Label:` durch die eindeutig begrenzte Form `${Label}:`.
- Der bestehende Resume-Download, die `.part`-Datei und der automatische Setup-Ablauf bleiben unverändert erhalten.
- Keine Datenbank-, Konfigurations-, Upload- oder Listing-Daten werden verändert.
# 1.14.4 Slice 22.2 Download Recovery
- Fortsetzbarer HTTP-Streaming-Download statt BITS/Invoke-WebRequest.
- Sichtbare Fortschrittsanzeige mit Prozent, MB, Geschwindigkeit und Restzeit.
- Automatische Wiederaufnahme über setup.bat.
# Changelog
## 1.40.2 Capability Graph Contract Hotfix
### Behoben
- `vendoo.module-manager` verlangte `settings.configuration`, obwohl `vendoo.settings` diese Capability nicht bereitstellte.
- Der Plattformstart brach deshalb mit `MODULE_CAPABILITY_MISSING` ab.
- `vendoo.settings` stellt nun den übergeordneten Konfigurationsvertrag zusätzlich zu `settings.application` und `settings.smtp` bereit.
### Qualitätssicherung
- neues Capability-Graph-Gate validiert den real aktivierten Gesamtgraph aller 16 Module.
- `ModuleRegistry.validateGraph()` stellt Startreihenfolge und Capability-Menge ohne Lifecycle-Start für Tests bereit.
- Architektur-, CI-, Release- und Deployment-Gates prüfen den realen Modulgraph statt ausschließlich künstlicher Testmodule.
- keine Datenbankmigration, kein Reset und keine Änderung an Nutzdaten.
## Slice 22.1 / 1.14.3 Setup-integrated PowerShell 5.1 recovery
- Fixed the false 7-Zip failure when `Process.ExitCode` is empty under Windows PowerShell 5.1 despite a successful extraction.
- A missing exit code is accepted only when `Everything is Ok` is present, stderr is empty and the extracted result passes file-count and size checks.
- Complete `local-ai\extract-temp` results are reused instead of being deleted and extracted again.
- `setup.bat` now starts with UTF-8 console handling, `-NoProfile` and explicit exit-code reporting.
- Normal `setup.bat` startup detects the exact confirmed Slice-22 false-failure state and offers automatic continuation.
- Automatic continuation installs ComfyUI, downloads the FLUX.1-schnell model and starts the local image server without a separate patch command.
- Installer intent flags are persisted in `install-state.json` for future resumable runs.
- No reset, database change or feature removal is performed.
## Slice 22 / 1.14.2 7-Zip extraction watchdog and real FLUX install telemetry
- Replaced the blocking 7-Zip invocation with a supervised process and explicit path quoting.
- Live extraction status now includes elapsed time, extracted file count, extracted MB, process ID and heartbeat.
- Added 7-Zip stdout/stderr logs and a visible UI panel for extraction details.
- Detects stalled installers instead of showing an endless running state.
- Reuses complete downloads and safely restarts only the failed extraction/install step.
- Handles nested ComfyUI portable archive layouts more robustly.
- Setup status now shows live extraction counters and stale heartbeat detection.
## Slice 21 FLUX Installer 7-Zip Hotfix & Detailed Status
- Fixed PowerShell 5.1 scalar-string bug where a single 7-Zip path such as `C:\Program Files\7-Zip\7z.exe` was indexed as the single character `C`.
- 7-Zip discovery now uses a real string list, refreshes PATH after winget installation and validates the final executable path.
- Partial ComfyUI and FLUX downloads now use `.part` files and are validated before being promoted to complete files.
- BITS downloads now report actual transfer percentage, bytes transferred and total bytes into `local-ai/install-state.json`.
- Installer failures preserve the last real progress value and include source position plus resumable-state metadata.
- Settings UI now shows detailed checks for 7-Zip, archive, Python runtime, ComfyUI, FLUX model, workflow and start/stop scripts.
- Install log tail, error details, readiness score and a resume-aware install button were added.
- Setup option 8 now prints phase, progress, file checks, runtime reachability and recent install log lines.
## Slice 20 Vollautomatischer lokaler FLUX-Installer
- Automatischer Download der aktuellen offiziellen ComfyUI-Windows-Portable-Version über die GitHub-Releases-API
- GPU-Erkennung für NVIDIA, AMD, Intel und CPU-Fallback
- Automatische 7-Zip-Installation über winget, falls erforderlich
- Optionaler Download von FLUX.1-schnell FP8 aus dem offiziellen Comfy-Org-Hugging-Face-Repository
- Fortschritts- und Statusdatei unter `local-ai/install-state.json`
- Fortsetzbare Downloads über BITS mit WebRequest-Fallback
- Sichere Startparameter: Bindung an `127.0.0.1`, kein Browser-Autostart, eigener Port 8188
- Optionale Windows-Autostart-Aufgabe `Vendoo Local FLUX`
- UI-Aktionen für Installieren, Aktualisieren, Starten, Stoppen und Status prüfen
- Setup-Menü schützt weiterhin vor versehentlicher Neuinstallation von Vendoo und erkennt vorhandene FLUX-Installationen
- Provider-Reihenfolge im Auto-Modus: lokal FLUX → OpenRouter Free → OpenAI
## Slice 19 Local FLUX / ComfyUI Bootstrap, safer setup install flow
- Setup hardened: Install checks whether Vendoo is already installed and suggests Start / Update / Repair instead of blindly reinstalling.
- New setup options: local FLUX / ComfyUI bootstrap and status checks.
- New local image provider architecture for AI model photos: Auto now supports Local FLUX -> OpenRouter Free -> OpenAI.
- New server routes for local image status, bootstrap install and token regeneration.
- Settings UI expanded with local FLUX URL, token, workflow path and install path.
- Added local workflow bootstrap template and helper scripts under `scripts/` and `local-ai/workflows/`.
## Vendoo 1.12.0 OpenRouter Diagnostics & Mobile Mediathek
- Mobiler QR-Upload besitzt jetzt getrennte Aktionen für **Kamera** und **Mediathek**
- Mehrfachauswahl aus der Smartphone-Mediathek ohne `capture`-Zwang
- HEIC/HEIF werden beim mobilen Upload nach Möglichkeit automatisch in JPG konvertiert
- OpenRouter-Free-Erkennung bewertet nur bildrelevante Preisfelder
- OpenRouter versucht je Modell mehrere kompatible Chat-Request-Varianten und danach die Images API
- Referenzbilder werden nur an Modelle gesendet, die Bildinput unterstützen
- Klare Diagnose, wenn aktuell **0 kostenlose Bildmodelle** verfügbar sind
- Bei Fehlern werden weiterhin alle passenden Free-Modelle nacheinander versucht; Auto fällt anschließend auf OpenAI zurück
## Vendoo 1.11.0 LAN QR Upload, OpenRouter Fallback & Mobile Nav Fix
- QR-Handy-Upload verwendet automatisch eine erreichbare LAN-IP statt localhost
- Vendoo lauscht für lokale Netzwerkzugriffe auf `0.0.0.0` und zeigt erkannte LAN-Adressen beim Start an
- Neue Einstellung für `PUBLIC_BASE_URL` inklusive automatischer LAN-IP-Erkennung
- OpenRouter probiert bei Fehlern automatisch weitere passende Free-Bildmodelle und danach im Auto-Modus OpenAI
- OpenRouter-Bildgenerierung unterstützt Chat-Completions und Images-API als zweistufigen Fallback
- Mobile Navigation vollständig gehärtet: Overlay, Close-Button, Fokus, Escape, Scroll-Lock und responsive Labels
## Vendoo 1.10.0 AI Jobs, 13 Varianten & History
- AI-Bildgenerierung läuft jetzt über eine Job-Queue statt nur synchron
- Mindestanzahl auf **1 Bild** gesenkt, Maximum bleibt **3 Bilder**
- Neue Modi: **Model**, **Ghost Mannequin**, **Flatlay**
- Listing-Editor zeigt jetzt eine **AI-Bild-Historie** zum schnellen Übernehmen früherer Generierungen
- Settings erweitert um Standard-Modus für AI-Bilder
## Vendoo 1.9.0 AI Provider Settings Center
- Neuer Settings-Bereich für AI-Model-Fotos mit Provider-Steuerung
- Admin kann jetzt `auto`, `openrouter` oder `openai` für Bildgenerierung wählen
- OpenRouter-Free-Modelle können direkt aus der UI geladen und geprüft werden
- Safety-Flags, Standard-Preset, Variantenanzahl und Fallback-Modell sind in der UI konfigurierbar
## Vendoo 1.8.0 OpenRouter Free Image Discovery
- AI-Model-Fotos unterstützen jetzt einen Provider-Modus `auto`
- OpenRouter Free-Bildmodelle werden dynamisch über die Models-API abgefragt und bei Verfügbarkeit bevorzugt genutzt
- Fallback auf OpenAI (`gpt-image-1` oder konfiguriertes Modell), wenn keine freien OpenRouter-Modelle verfügbar sind
- Neue API-Route für verfügbare OpenRouter-Bildmodelle sowie Persistenz von Provider und Modell je Generierung
## Vendoo 1.7.0 AI-Model-Fotos & Safety Layer
- Neuer sicherer AI-Model-Foto-Workflow für Kleidung ohne Person auf dem Originalfoto
- 23 KI-generierte Zusatzbilder mit fiktiven erwachsenen Models in Studio-, Indoor- oder Outdoor-Looks
- Precheck & Output-Safety: blockiert Unterwäsche, Bademode, transparente Erotik-Looks, Kinderkleidung und Quellen mit Personen
- Neue Backend-Routen, Datenbanktabellen und Listing-Integration für Generator und Detailansicht
# Changelog
## 1.40.2 Capability Graph Contract Hotfix
### Behoben
- `vendoo.module-manager` verlangte `settings.configuration`, obwohl `vendoo.settings` diese Capability nicht bereitstellte.
- Der Plattformstart brach deshalb mit `MODULE_CAPABILITY_MISSING` ab.
- `vendoo.settings` stellt nun den übergeordneten Konfigurationsvertrag zusätzlich zu `settings.application` und `settings.smtp` bereit.
### Qualitätssicherung
- neues Capability-Graph-Gate validiert den real aktivierten Gesamtgraph aller 16 Module.
- `ModuleRegistry.validateGraph()` stellt Startreihenfolge und Capability-Menge ohne Lifecycle-Start für Tests bereit.
- Architektur-, CI-, Release- und Deployment-Gates prüfen den realen Modulgraph statt ausschließlich künstlicher Testmodule.
- keine Datenbankmigration, kein Reset und keine Änderung an Nutzdaten.
## [Unreleased]
## [1.6.2] - 2026-07-07
### Branding
- neues Vendoo-Hauptlogo in die App-Sidebar integriert
- neues Branding auf Login und mobilem QR-Fotoupload integriert
- vollständiges Favicon-Paket mit ICO, PNG, Apple Touch Icon und Webmanifest eingebunden
- mobile App-Icon-Größen von 16 bis 512 Pixel ergänzt
- Chrome-, Edge- und Firefox-Erweiterungen auf die neue Vendoo-Icon-Familie aktualisiert
- Vinted Assistant und minimierter Assistant verwenden das neue Logo
## [1.6.1] - 2026-07-07
### Verbessert
- Dashboard ist jetzt der erste Menüpunkt und die Standard-Startseite.
- Dashboard-KPI-Karten verwenden eine fließende Auto-Fit-Struktur statt starrer Sechserspalten.
- Sparklines besitzen eine reservierte Diagrammfläche und überlagern keine Texte mehr.
- Umsatzdiagramme werden bei Größenänderungen automatisch neu berechnet.
- Dashboard-Charts passen sich an Notebook-, Tablet- und Mobilbreiten an.
- Studio-Flow-Karten verwenden proportionale Bildflächen mit vollständiger `contain`-Darstellung.
- Produktbilder werden nicht mehr abgeschnitten und erhalten einen sauberen Fehlerzustand.
- Ready-Karten verteilen sich automatisch auf die verfügbare Breite.
## [1.6.0] - 2026-07-07
### Hinzugefügt
- neuer Sidebar-Menüpunkt **Dashboard** mit interaktiven Kennzahlen, Umsatzverlauf, Plattform- und Kategorieauswertung
- Dashboard-Bereiche für Fokusaufgaben, Top Seller, Aktivität, Benachrichtigungen, AI-Qualität und Systemstatus
- Fotoarchiv-Endpunkt: mehrere Fotos werden serverseitig als gültiges ZIP erzeugt
- Foto-Download im Generator und vollständigen Listing-Editor
### Geändert
- Studio Flow vollständig neu strukturiert: Bereit, Geplant, Veröffentlicht, Verkauft und Benötigt Aufmerksamkeit
- die fachlich unpassenden sichtbaren Stufen „Neu erfasst“ und „AI-Prüfung“ wurden entfernt
- Studio Flow zeigt nur priorisierte Artikel statt einer überfüllten Spaltenwand
- geplante Listings, Probleme und Aktivitäten sind in klar getrennten Arbeitsbereichen organisiert
- Dashboard und Studio Flow für Notebook, iPad, Tablet und Smartphone neu priorisiert
### Behoben
- Publish-Center lädt Listings beim ersten Öffnen mit Wiederholungslogik und verhindert veraltete parallele Renderings
- Publish-Ladefehler zeigt jetzt eine konkrete Wiederholen-Aktion statt einer Sackgasse
- bei einem Foto erfolgt ein normaler Download, bei mehreren Fotos automatisch ein ZIP-Download
## [1.5.0] - 2026-07-07
### Hinzugefügt
- Browser-Erweiterungen in getrennten Ordnern für Chrome, Edge und Firefox
- vollständiger Design-Relaunch von Popup und Vinted Assistant
- neues Vendoo-Link-Icon in 16, 32, 48 und 128 Pixel
- QR-basierter Handy-Fotoupload für Generator und Listing-Editor
- sichere, zeitlich begrenzte Mobile-Upload-Sitzungen
- responsive Layout-Gates für Desktop, Notebook, iPad und Smartphone
- umschaltbare Plattformvorschau und HTML-Codeansicht
### Verbessert
- Produktbilder der Extension werden vollständig dargestellt
- CSRF-Unterstützung für schreibende Extension-Requests
- öffentliche Serveradresse statt festem localhost für Extension-Bilder
- Plattformvorschau und HTML-Code sind in Generator und Listing-Editor als Umschalter zusammengeführt
- Layouts für Desktop, Notebook, iPad und Smartphone neu priorisiert
- freigegebene Extension- und Icon-Mockups als verbindliche Designreferenz archiviert
### Behoben
- Studio Flow: „Prüfen & bearbeiten“ wechselt jetzt zuverlässig zur sichtbaren Listing-Detailansicht.
- Detailaufrufe aus Attention Queue und anderen Bereichen verwenden denselben sicheren Navigationspfad.
- Ungültige Listing-IDs werden abgefangen und als Hinweis angezeigt.
- Windows-Installer behandelt npm `ENOTEMPTY`/`EPERM` robust, bereinigt unvollstaendige `node_modules`, protokolliert npm-Ausgaben und wiederholt die Installation einmal.
- Installation nutzt mit vorhandenem Lockfile reproduzierbar `npm ci` statt eines stillen `npm install`.
### Geplant
- vollständiger Import des lokalen Vendoo-Quellprojekts
- Docker- und Online-Readiness-Foundation
- sichere Server-Konfiguration und Healthchecks
- Mandantenfähigkeits- und Rechtekonzept für externe Benutzer
- Lager, Vorlagen, Einstellungen und Admin gemäß Design-Freeze
- Docker- und Online-Readiness
## [1.0.0-baseline] - 2026-07-07
## [1.4.0-full-listing-editor] - 2026-07-07
### Dokumentiert
### Hinzugefügt
- bestehender Funktionsumfang mit 17 Feature-Bereichen
- verbindlicher Tech Stack und Projektstruktur
- Sicherheits- und Repository-Grundregeln
- privates GitHub-Repository als Versionsbasis
- Listings lassen sich jetzt vollständig in einem eigenen Arbeitsbereich bearbeiten
- Fotoverwaltung mit Hinzufügen, Entfernen, Cover-Auswahl, Drag-to-Sort und vollständigem Bildeditor ergänzt
- WYSIWYG-Editor und HTML-Code sind im Listing-Editor bidirektional und live synchronisiert
- Generator zeigt HTML-Code und Plattformvorschau gleichzeitig; Änderungen werden ohne Umschalten übernommen
- Plattform-HTML wird während der Bearbeitung live neu gerendert
- neue Datenbankspalte `description_html` mit Migration und Rückwärtskompatibilität
- neuer API-Endpunkt `POST /api/html-render` für nicht persistierende Live-Vorschauen
- Editor-HTML und vollständiges Plattform-HTML können getrennt kopiert werden
### Sicherheit
- Rich-Text-HTML wird client- und serverseitig auf eine begrenzte Tag- und Attributmenge reduziert
- Plattformvorschauen laufen in sandboxed iframes
- Listing-Updates verwenden jetzt eine feste Feld-Allowlist statt beliebiger SQL-Spaltennamen
### Behoben
- `photos` werden bei Listing-Updates korrekt als JSON gespeichert
- Bildbearbeitung funktioniert nun auch für bereits gespeicherte Listings
- formatierte Beschreibungen bleiben nach Speichern und erneutem Öffnen erhalten
## [1.3.0-publish-center] - 2026-07-07
### Geändert
- Publish-Bereich als durchgängiges operatives Publish-Center neu aufgebaut
- Smart Copy, Direkt-Publishing, eBay Queue und Zeitplanung in einer gemeinsamen Oberfläche zusammengeführt
- bildorientierte Listing-Auswahl mit Suche, Plattform-/Statusfilter, Sortierung und Mehrfachauswahl ergänzt
- internes Workspace-Modell statt separater Queue- und Smart-Copy-Modale eingeführt
- direkte Listing-Bearbeitung vor Etsy-/eBay-Veröffentlichung integriert
- Publish-Bereitschaft, Pflichtfelder, Integrationsstatus und Fotoprüfung sichtbar gemacht
- Queue-Status, Schnellaktionen und Publish-Aktivität in einer festen Statusleiste gebündelt
- eBay-Fehler, Wiederholungen und geplante Veröffentlichungen inline bedienbar gemacht
- responsive Darstellung und Dark Mode für das komplette Publish-Center ergänzt
### Behoben
- Batch-Smart-Copy öffnete denselben Vorgang teilweise doppelt
- nicht passende Etsy-Batch-Auswahl wird jetzt gefiltert statt blind veröffentlicht
- Produktbilder im Publish-Center werden vollständig und ohne Beschnitt dargestellt
- Queue- und Planungsaktionen bleiben im Vendoo-Kontext und verlieren nicht den aktuellen Arbeitsstand
## [1.2.0-listing-studio] - 2026-07-07
### Geändert
- Listing-Generator als dreigeteilter Studio-Arbeitsplatz neu aufgebaut
- Foto-Cover, Sortierung, Qualitätscheck und vollständige Bildanpassung integriert
- AI-Konfiguration mit Plattform, Provider, Modell, Sprache, Varianten und Vorlagen neu strukturiert
- artikelspezifische Verkäufernotizen mit Zeichenzähler ergänzt
- Ergebnisbereich mit Live-Zählern, Qualitätswert, Attributen, Gebühren und HTML-Vorschau modernisiert
- responsive Darstellung und Dark Mode für den Generator vollständig ergänzt
### API
- `POST /api/generate` akzeptiert `template_id` und `seller_notes`
- globale Hinweise, Vorlagenhinweise und Artikelhinweise werden dedupliziert kombiniert
- Standard-Tags aus Vorlagen werden mit AI-Tags zusammengeführt
### Behoben
- Upload begrenzt neue Bilder clientseitig auf die verbleibenden Plätze bis maximal zehn
- Upload-Fehler werden mit HTTP-Status und Servermeldung korrekt angezeigt
- Dateinamen in Vorschaubildern werden URL-sicher ausgegeben
## [1.0.1-ui-foundation.1] - 2026-07-07
### Hinzugefügt
- zentrale Vendoo-Design-Tokens für Light und Dark Mode
- neue kompakte App-Shell und Hauptnavigation
- Command-Search-Einstieg mit `Ctrl/⌘ + K`
- neue Startseite `Studio Flow`
- Flow-Stufen: Neu erfasst, AI-Prüfung, Bereit, Geplant, Veröffentlicht, Verkauft
- Attention Queue und Activity Stream
- kontextueller Artikel-Inspector
- neuer Endpunkt `GET /api/dashboard/workflow`
- Dokumentation `docs/UI_FOUNDATION_2026.md`
- sicherer `.gitignore`
### Sicherheit
- `.env`, SQLite-Laufzeitdateien, Uploads und `node_modules` aus dem Übergabepaket entfernt
- keine produktiven Zugangsdaten in der bereinigten Version
## [1.1.1-ui-polish] - 2026-07-07
### Geändert
- Etiketten und Artikeldatenblätter öffnen jetzt im internen Vendoo Print Studio statt in einem neuen Browserfenster
- maßstabsgetreue A4-Vorschau mit zwei Etikettenformaten, Exemplaren und Preisoption
- Druck erfolgt im aktuellen Vendoo-Fenster über den normalen Systemdruckdialog
- Studio-Flow-Produktbilder verwenden vollständige automatische Anpassung (`object-fit: contain`)
- Inspector- und Attention-Bilder werden ebenfalls ohne Beschnitt dargestellt
- Vinted-Extension auf die freigegebene Vendoo-Designsprache 2026 umgestellt
- Extension-Popup und eingeblendetes Vinted-Panel modernisiert
- Produktbilder der Extension werden sicher über den Background-Service-Worker als Data-URL geladen
- Pfade von Upload-Bildern werden korrekt URL-codiert und Bildfehler erhalten einen sichtbaren Zustand
- fehlerhaft doppelt codierte deutsche Texte in der Extension repariert
### Behoben
- Produktvorschaubilder in der Vinted-Extension wurden durch Seiten-CSP/Mixed-Content-Konstellationen teilweise nicht angezeigt
- große oder hochformatige Produktbilder wurden im Studio Flow abgeschnitten
## [1.1.0-ui-listings] - 2026-07-07
### Geändert
- Listings-/Historie-Seite vollständig an die freigegebene Vendoo-Designrichtung 2026 angepasst
- neue Filterleiste mit Suche, Plattform, Status und Zeitraum
- benutzerdefinierbare sichtbare Tabellenspalten mit lokaler Speicherung
- dichte, bildorientierte Listing-Tabelle mit SKU, Plattform, AI-Provider, Preis, Status, Lagerort und Publish-Status
- neue Batch-Aktionsleiste für Publish, Nachbearbeitung, Preis, Status, Duplizieren, Etiketten und Löschen
- moderne Seitennavigation und Seitengrößensteuerung
- neue Exportauswahl für CSV und JSON
- verbesserter leerer Zustand und responsive Darstellung
- bestehende CRUD-, Export-, Druck-, Status- und Papierkorb-Funktionen erhalten
## [1.6.1] - 2026-07-07
### Geändert
- Vendoo Assistant für Chrome, Edge und Firefox vollständig verdichtet und visuell neu strukturiert.
- Queue-Karten zeigen Bild, Status, SKU, Preis und direkte Aktionen ohne große Leerflächen.
- „Nur ausfüllen“ ist jetzt die primäre und sichere Einzelaktion.
- Daten können in Vinted eingetragen werden, ohne automatisch einen Entwurf anzulegen oder zu veröffentlichen.
- „Daten + Bilder ausfüllen“ überträgt Bilder und Felder, führt aber keinen Abschluss aus.
- Entwurf und Veröffentlichung liegen bewusst in einem getrennten, aufklappbaren Abschlussbereich.
- Popup öffnet Vinted mit einem vorgemerkten Listing und füllt es nach dem Laden aus.
- Extension-Version auf 1.4.0 angehoben.
+96
View File
@@ -0,0 +1,96 @@
# Vendoo — Projekt-Kontext
Du arbeitest am Projekt "Vendoo" — ein lokaler Multi-Plattform Listing-Generator mit AI, Lagerverwaltung und Publishing-System.
Lies zuerst `FEATURES.md` im Projektroot — das ist die vollstaendige Feature-Dokumentation mit Tech Stack, Projektstruktur, DB-Schema, allen 80+ API-Endpunkten und 17 Feature-Beschreibungen.
## Tech Stack
- **Backend:** Node.js + Express (ESM, `"type": "module"`)
- **Frontend:** Vanilla HTML/CSS/JS — kein Framework, kein Build-Step, kein TypeScript
- **Datenbank:** SQLite via `better-sqlite3` (WAL-Modus)
- **AI:** Anthropic Claude, OpenAI, OpenRouter, Ollama (lokal)
- **Bilder:** `sharp` (Resize, BG-Remove, Watermark)
- **Rich-Text:** Quill Editor
- **E-Mail:** Nodemailer (SMTP)
- **Extension:** Chrome Manifest v3
## Dateistruktur (wo was hingehoert)
| Aenderung | Datei(en) |
|---|---|
| Neue API-Route | `server.mjs` — Route hinzufuegen + Import erweitern |
| Neue DB-Funktion | `lib/db.mjs` — Funktion exportieren |
| Neue DB-Spalte | `lib/db.mjs` — MIGRATIONS-Array erweitern (try/catch ALTER TABLE Pattern) |
| Neues UI-Tab | `public/index.html` (Sidebar-Button + Section), `public/app.js` (TAB_TITLES + switchToTab + Logik), `public/style.css` |
| Neuer AI-Provider | `lib/ai-*.mjs` + `lib/ai-router.mjs` erweitern |
| Neue Plattform | `platforms/*.mjs` + `lib/categories.mjs` + `lib/fees.mjs` + `lib/html-templates.mjs` |
| Styling | `public/style.css` — CSS Custom Properties fuer Theming |
## Konventionen
- **Sprache:** UI ist komplett Deutsch
- **SKU-Format:** `VD-XXXX` (auto-generiert, 4-stellig aufsteigend)
- **Navigation:** Sidebar mit `data-tab` Attributen, Tab-Inhalte als `<section id="..." class="tab-content">`
- **Topbar-Titel:** `TAB_TITLES` Map in `app.js` muss fuer neue Tabs erweitert werden
- **Dark Mode:** CSS-Variablen unter `[data-theme="dark"]`, niemals hardcoded Farben
- **Auth:** Session-basiert, Rollen `admin`/`editor`, CSRF-Token bei state-changing Requests
- **DB-Migrationen:** Neue Spalten als Eintrag in `MIGRATIONS`-Array (Zeile ~17-21 in db.mjs):
```js
const MIGRATIONS = [
['spaltenname', 'TEXT'], // oder 'REAL', "TEXT DEFAULT 'wert'"
];
// Wird automatisch per try/catch ALTER TABLE ausgefuehrt
```
- **API-Pattern:** REST, JSON-Body, Fehler als `{ error: 'message' }`
- **Frontend-API-Aufrufe:** `api(method, path, body)` Helper in app.js (fuegt Auth + CSRF automatisch hinzu)
- **Toast-Benachrichtigungen:** `toast('Nachricht')` in app.js
- **Quill-Editor:** `initQuill(selector, placeholder)` — gibt Editor-Instanz zurueck
## Server starten
```bash
npm install # Dependencies installieren
npm run dev # Dev-Server mit --watch (Port 8124)
npm start # Produktiv-Server
```
Oder: `setup.bat` fuer gefuehrte Installation.
## Wichtige Dateien nach Groesse
| Datei | Zeilen | Inhalt |
|---|---|---|
| `public/app.js` | ~3240 | Gesamte Frontend-Logik |
| `server.mjs` | ~1250 | Alle API-Routen + Middleware |
| `public/style.css` | ~1200 | Komplettes Styling + Dark Mode + Responsive |
| `public/index.html` | ~845 | HTML-Struktur (Sidebar, Tabs, Modals, Forms) |
| `lib/db.mjs` | ~390 | Datenbank-Zugriff (alle CRUD-Funktionen) |
| `lib/categories.mjs` | ~360 | Kategorie-Baeume pro Plattform |
| `lib/auth.mjs` | ~290 | Auth-System (Users, Sessions, Audit) |
| `lib/ebay-api.mjs` | ~270 | eBay REST API + OAuth2 |
| `lib/mailer.mjs` | ~210 | SMTP E-Mail-Versand |
| `lib/etsy-api.mjs` | ~185 | Etsy REST API v3 + OAuth2 PKCE |
| `extension/content.js` | ~1170 | Chrome Extension Content Script |
## Aktueller UI-Stand
- Version `1.3.0`
- UI 2026 Slice 05: Publish-Center
- Smart Copy, direkte Veröffentlichung, eBay Queue und Scheduling arbeiten in einer gemeinsamen Oberfläche.
- Interne Publish-Arbeitsschritte dürfen keine separaten Browserfenster oder redundanten Modale öffnen.
## Bildarchitektur ab 1.23.0
- FLUX/ComfyUI ist der einzige lokale generative Bilddienst.
- Das FLUX Studio arbeitet textbasiert und getrennt von Produktbild-Werkzeugen.
- Keine VTO-, Python-, Torch- oder ONNX-Laufzeit wieder einführen, solange kein neuer ausdrücklicher Architekturentscheid dokumentiert ist.
- FLUX-Studio-Aufträge und Varianten sind in `flux_prompt_jobs` und `flux_prompt_job_variants` persistent.
- Fortschritt im FLUX Studio wird als reale Phase dargestellt; keine aus Laufzeit geschätzten Prozentwerte ergänzen.
- ComfyUI-Prompt-IDs müssen für Abbruch, Historie und Neustart-Wiederaufnahme erhalten bleiben.
- Der Advanced Image Editor arbeitet nicht destruktiv: Originaldateien niemals überschreiben oder löschen.
- Jede gespeicherte Bearbeitung muss eine neue Datei und einen Eintrag in `image_edit_versions` erzeugen.
- Bildpfade müssen strikt auf `uploads/` begrenzt und gegen Traversal geschützt bleiben.
- Hintergrundentfernung, Inpainting, Outpainting und Maskeneditor nicht als Platzhalter ergänzen; diese Funktionen benötigen eigene spätere Slices.
+36
View File
@@ -0,0 +1,36 @@
# syntax=docker/dockerfile:1.7
FROM node:22-bookworm-slim AS dependencies
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 make g++ ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY package.json package-lock.json .npmrc ./
RUN npm ci --omit=dev --no-audit --no-fund \
&& npm cache clean --force
FROM node:22-bookworm-slim AS runtime
ENV NODE_ENV=production \
PORT=8124 \
VENDOO_BIND_HOST=0.0.0.0 \
VENDOO_DATA_DIR=/app/data \
VENDOO_DB_PATH=/app/data/db/vendoo.db \
VENDOO_UPLOADS_DIR=/app/data/uploads \
VENDOO_BACKUP_DIR=/app/data/backups \
VENDOO_OPERATIONS_DIR=/app/data/operations \
VENDOO_LOG_DIR=/app/data/logs \
VENDOO_CONFIG_PATH=/app/data/config/vendoo.env \
VENDOO_MODULES_DIR=/app/data/modules \
VENDOO_MODULE_STATE_PATH=/app/data/config/modules-state.json \
VENDOO_INITIAL_DISABLED_MODULES=vendoo.flux-studio
WORKDIR /app
COPY --from=dependencies --chown=node:node /app/node_modules ./node_modules
COPY --chown=node:node . .
RUN mkdir -p /app/data/db /app/data/uploads /app/data/backups /app/data/operations /app/data/logs /app/data/config /app/data/modules \
&& chown -R node:node /app/data \
&& chmod +x /app/scripts/docker-entrypoint.sh
USER node
EXPOSE 8124
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:8124/readyz').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"
ENTRYPOINT ["/app/scripts/docker-entrypoint.sh"]
CMD ["node", "bootstrap.mjs"]
+1079 -84
View File
File diff suppressed because it is too large Load Diff
+544
View File
@@ -0,0 +1,544 @@
f344f75df655bc133e6c4abb2d215b2234fb22a4bb155c217317c36ee4f0f8fb .dockerignore
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
9bfa732952ad571d06527b32d96d79133fc23fa607df00de440b2802f543608c .npmrc
9d7c2a587c12f2b12ba2923e367ade8cdd4601dcac800ca2ac34afb302e9a6e1 app/architecture-boundaries.json
a6bdb9b6fdc97a0b2bbc052bd242a9885a75bcc46ee4f0414d8b7e215b6940de app/contracts/module.schema.json
67fcafb51d5d17a83a04e55ecf877c8d9ac928d11a1951d0395f137ca6913401 app/core/identity/identity-store.mjs
7cf2224714a10e337b4e8e6514de56d61200d27632440ce85c2ba20ccd7e9d7f app/core/modules/module-package-contract.mjs
1ac6208b0cc85a3b5e2f86d61bb59a7a39fd46f51b4d8278c2abe236b53fdab5 app/core/modules/module-package-store.mjs
c2f4ebceb06aa40d75c456f7448225436cedb8590228faff6da8f8107987e1b5 app/core/modules/module-state-store.mjs
ce74a28bb663f0dcfa9f11cd4375f763db088ee66128f64b90bb91e99f732642 app/core/security/secret-contract.mjs
7f38b3ac792e61e030c9f69355e2378f8f61e8e61c7b29c82cd0fa7ec930cfec app/core/themes/theme-contract.mjs
d84a573699e413e0a0803459e0d05c5c4e6e35d3499eb634f95302d151e75c4b app/kernel/errors.mjs
a34333cbc784d39b39a8071a437972c6175c551104dc2d82fc4c0e0506b18259 app/kernel/event-bus.mjs
0e8f691f96e1ef0a7a86d32cae7b7e1db04325fe94a32b8c1f484a5b6950f3b4 app/kernel/feature-flags.mjs
a4877c90f2c9040cb101b7fbb80385d4b1ea0814118a104873b891a580e88204 app/kernel/input-schema.mjs
a0223a00691908862aaf1fd9383b6d86d67f26325c509280b8c2daa5cfcee28e app/kernel/lifecycle.mjs
ba1f4f799a508ff113738cce7ac9f5af27739cb60d7f04d7a6c5878de3c5b7f4 app/kernel/module-contract.mjs
4da6fa8b6d1a3aa345c924f426c405faecba9dcfde5983f74de45df4eb233430 app/kernel/module-registry.mjs
35b305ae93b2d47ddf472e33a6ba7b285b0091926f977a03b37baf2d7ac85e10 app/kernel/platform-kernel.mjs
7944f68d995a5d70273f8a985973f668fc62a414d85636d92f3774e34d39a37f app/kernel/policy-engine.mjs
f98e4cd58b051efdf311dafa73569b1ecebe2a29431cd763380c9663035c1c2b app/kernel/route-registry.mjs
0ff879fb8cf392a3786c39f2541637b2d9f018fb03941066ed540a2880134f89 app/modules/ai/index.mjs
98e93589109cb34c7af71215a581fe40e80691884459c2d42260f6b001065e6e app/modules/ai/module.json
3ae563b7e6232ed89f5bc5dc49fe359ecfd696123aee2cb780032040c53f149e app/modules/auth/index.mjs
cae01362c58adac32badb0b102ce76a748c7301aafa64818a49456665b4de29d app/modules/auth/module.json
04a72689ecea04caf79eec9207116d1da374600150fe1966032fe972736f246f app/modules/auth/routes.mjs
53917fe8aa4832cc62f35d4746af9bfdfd1132448de0ca750f073a0a27cdbebb app/modules/auth/service.mjs
7580002b0f9236b23c0c4193b6e65c761db93bb30fc3277d762d9674f04250c4 app/modules/catalog.mjs
a3f88366d1f765c93f39e1f6a5b40da0690168d713be390ca6afaa28cbe97d2f app/modules/deployments/index.mjs
50ae28b3efe9296deb32399b35acc6cb27ead17a2f7e1526ff05c5e4da664ac0 app/modules/deployments/module.json
2f45596aa2cc7f5e532c3624156c5bdebfb0d79f94081990b06a33d4fca941af app/modules/deployments/routes.mjs
a91940a47181e148ab24040e807cedc281682abf6d4e8d7a14b2f27bdc9ab8b2 app/modules/deployments/service.mjs
99bde2c2871c48ff9e1d2895157197d9f1d5e194072a481abaa3495daaa683e0 app/modules/flux-studio/index.mjs
5f28a104ef923c0f01cf71d765cb603e3c9bf7a30d087321f58161d701896024 app/modules/flux-studio/module.json
fd798d294308dcb50d5ce90d3cfb969800cd1dfecf4805e69ec41f974e268829 app/modules/inventory/index.mjs
febca770c3ee71c398bec9e6afc3aaaa8850ce8c9a2840c190d93b2e8d25f790 app/modules/inventory/module.json
6925da30bd453695b82c8fcbc82c585b01cda71c410f59d62697a31c364954ae app/modules/inventory/repository.mjs
b3d0c8a4643ff09091d48c4e1dfcd90ed0884fe9713a7bb44160c34761b59590 app/modules/inventory/routes.mjs
94f3e5a3e6f3ed1120dad7c869b47978582717e231b57ae4c98e668d903f09bc app/modules/inventory/service.mjs
fbf8db46e0ae7b483f898588c39a4f74d72717588d93d2058ab9250f9ada0c88 app/modules/listings/index.mjs
79b21e2387b59eec5a23b3d547d954ddc1edf99e9ab691f6e410894811f1978c app/modules/listings/module.json
a76054a2eca06d8412c211fc8b75f7fbd9cb1ed256f38360a7031b1f8ed7d73c app/modules/listings/repository.mjs
d1e817f0c4251994bad0a7084e2e7dcd2873a0a099cb00a312f55aa2163c183d app/modules/listings/routes.mjs
e7c722e449284d8ec9ade461e2b8fc94723e35ebec25c51113b40643d12f28ef app/modules/listings/service.mjs
e12663a5b189660e34d72e034fa4b23b5bffdb0c8d5516b547020da11e70aefa app/modules/media/index.mjs
4efc183346859a6786250d40a9762f7457e00f659a7b2408209d85f3ef75d1c2 app/modules/media/module.json
67262d67fa85e5f2e09a51645432a2c7548a6dccdb0afbaaf63850e2fab67197 app/modules/media/repository.mjs
9f8e54cabbb7271bac7253a26b6ffbf553f834147500141c9e7427a26c9dde81 app/modules/media/routes.mjs
29a4e515e6ccc692b9cac78012e9571c4d6225f7e89bee2d4fefaf63f4d0fc3a app/modules/media/service.mjs
9d2afb4ba28a144b7f75c6c409f43d2f7a34f2674c8a1f717c2ab631516b0cca app/modules/module-manager/index.mjs
c4ab8673081ba03068cc5c76c1a0ea35c61771ad69de97cd1510dfc35f9bd93a app/modules/module-manager/module.json
b4250e7028bc1ff041166f4099a05e71fd4e0f71abaeee09ffdd3678ce9c52c7 app/modules/module-manager/routes.mjs
028fa76b052de388836ba75c14f45bcae67c9a99af6d727406674ee1fc7a4434 app/modules/module-manager/service.mjs
9723a9b12a9b472b2ed8c4635cdce663b2f0f65643d8ae0b6219715d853f740d app/modules/operations/module.json
8a2805286def5d0521765319eddcd44e680e32bd7065d45749edd31406228463 app/modules/publishing/index.mjs
53e1dcab6bb9c5046eba6daa17c0f1ffb193bed38ae248dd1702fee91bdd23bd app/modules/publishing/module.json
306a2c3c119b82f6f972fa503099705bd3ede6c2061f532a00dc208f0584445c app/modules/quality/index.mjs
61381a51b86b64d59dde8c1a749ae46b7990ec323adc71c7eb62f741eeb6f807 app/modules/quality/module.json
747febc12aec7d81ccfdf6e9ea7158655ef5fdc536c519ce447de329314612a8 app/modules/security/index.mjs
700be862cd70be6947919c1e1f0e25e7e88c313b1fcdddc104d54d88a74edbb2 app/modules/security/module.json
08a6015cd3588f08070d341ad8cc5c9be8e34d217cf8770e154d4571225b3e2e app/modules/sessions/index.mjs
da856dca2a4e2533a8a224806ccf10f792ed46e8266505a27339cf27e8a1edda app/modules/sessions/module.json
c2ed5ccf36a0c5398f3113cd79172bed917dfb6c9668f025e806e38d74f8c83b app/modules/sessions/routes.mjs
a33995a82e02beaaa820bebc2875d14d684b1d4baeeb79d27dc7237c899f3249 app/modules/sessions/service.mjs
edc35b82678ef00e24fc6cb6a6d135bbca9acc980a17e59b15ba90b0a79490de app/modules/settings/index.mjs
a9eb8f3d9e1fe5c271c10f340885b840d8f4a8cb0f79825d0d36d497c9347914 app/modules/settings/module.json
7b7ed59760da0338037b2d5038a9b422a62b4a818c62a74216d2fce215ac6490 app/modules/settings/repository.mjs
3717914c11da56a54dd0a22d139830a064f4c842c0f718517570470ecb9ed551 app/modules/settings/routes.mjs
07dcb652661f9c879877a5d0033f4388584b2c3a2eef58488c88cd7c5cadd78e app/modules/settings/service.mjs
452a5262c418b393bdd8754fe90f9ea14edf61338f8dcfd6438f0852920f608c app/modules/system/module.json
92d968c993ee5a2afc14df557b7cba552cae9a41722989007eb0b784514333ff app/modules/themes/index.mjs
1e062db871f9bf13b487c972f6cd4dd9cfe1b3046cd9b53ed43e0a0babc9d285 app/modules/themes/migrations/001_theme_profiles.sql
2af572390890e4d793aea9c1356c835917655c225a7848325d5a7c5ae3c08a09 app/modules/themes/module.json
3c3dc5974976672cd0e9141867a8a1d78a0dfaa54582bafb8f360bea571bf11a app/modules/themes/repository.mjs
4e2c348a184384739408894f26b02c56c6bfd4dd531eebb9c67d2fb7fbad3009 app/modules/themes/service.mjs
68cf3325307263022cd5704fd77664f7269896b8aa0570033c84b7abfba993ed app/modules/users/index.mjs
c6558b8a0cd9d7ef30d0e259747805e3d91a5359c2dbfbd79e6794bb403a9926 app/modules/users/module.json
8640accd1550932459ecd14738139284cbcdd93f31a61ab6e7748791fc82933f app/modules/users/routes.mjs
fc4af51a35fd08289ec4380a78c06b3236a2469041ecfc911b9e0d7d269afa19 app/modules/users/service.mjs
c197ce417fbce6dab1d8ee157b0a105a099dd2021ece4dac3891d0b2022a0d58 bootstrap.mjs
c525080bcf08aa43d694f5075c43c7ad9c06901b0aed453695b343393f7a8b9f CHANGELOG.md
61823fc9fdc036170cce6edeecbb612b442d1c65fc9f7a7eaba47e5016316ee5 CLAUDE.md
61199cecda2c84954de2b60c07eb9f3a2117fbaaa4c430319c461b2e76a10254 compose.bind-mounts.example.yaml
b8167851bcdbefa4a22e143cc6de8ab4441d3970752629bb2d8fdb03984f2904 compose.vps.yaml
b76a40463ce31202dbdc85db371eb9c6fcafce8cd263c685dac990311dbd50f4 compose.yaml
6323f984f1e04ab135356a14ec151941638041f6b305ed68772d5e1812972075 db/schema.sql
49690c4e4c8e84de8eb40ffe79823493b7be20fe46fa2ad9e6544f7f0afbfd2c deploy/Caddyfile
4ffc43e64b0e1e51a97d3f7f6a344ebcff537395008cf909c0fc4e3d3ab0c0cf deploy/coolify.env.example
6ce9932a68950033c7afc0969d4f9d75c9d26d1e536465e9cb5568bcd666268a Dockerfile
cc794bca282d79a27d860d0bfef4641769f4e9f31172778a1d9d8205558d948e docs/ABNAHMEBERICHT_1_32_0.md
de0d86f806a70195050613f5b56f6c4da1ee2eff02a9a991b6aacee229c46ca5 docs/ABNAHMEBERICHT_1_33_0.md
947745174897470de0bebf4f7c2b29a0c0ef3f1a762f277374c2614d9e3dec4f docs/ABNAHMEBERICHT_1_34_0.md
26c87fa38f804749a9363812127dc33ef69e201e4ef9b59206f6fe6e15c845b0 docs/ABNAHMEBERICHT_1_34_1.md
f792d366c13c5a4a9b4e8d4054e26190d9c327a7a770005d74bbe2b31bb84ab3 docs/ABNAHMEBERICHT_1_34_2.md
bc638fd21222ada7b15e40adff80a318aebec58139978109cbfd30040815a8d4 docs/ABNAHMEBERICHT_1_34_3.md
c0ae5c4176ba6f7510be047d4feaf383cd646152131003ebccc6bfbf8c8056f5 docs/ABNAHMEBERICHT_1_35_0.md
c050c8e11f2bdb0a742851423dcff864bafba9cc839c52b2f8d48b75f7b18633 docs/ABNAHMEBERICHT_1_36_0.md
1f3e96ba13844a3f1cefecca99b3d6313aebf86faae914897db0404ea031c491 docs/ABNAHMEBERICHT_1_37_0.md
361d19e6b7aeab3f22b337210f3a56221f2e42798c0fea75be978ebb7d72487f docs/ABNAHMEBERICHT_1_38_0.md
2f6518fd50d9088f5580cfcc03e92543321ac7feb97dcdd570c23c51fd8c5391 docs/ABNAHMEBERICHT_1_39_0.md
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
cc0edf92756a51414563b4407dedb21d1e3b36682f4bfbbc334aeb33a22ae1b4 docs/architecture/MIGRATION_ROADMAP_1_35_0.md
db659979dfd6e08f36a7aa9b4de42750ac39c536b11fad694a1846c902ea65bf docs/architecture/MODULE_MANAGER_1_40_0.md
eb47b055dd336e8360ec7c4712dee4ae37f109f3820e3f185969e2327d790cf8 docs/architecture/MODULE_SYSTEM_1_35_0.md
d7773faac0e93510a5a51c91de997df03547b51f3124267dcf137d4e99299562 docs/architecture/SECURITY_ARCHITECTURE_1_35_0.md
58cd43938b06e29520b92516a1e816d17d2c08d4847b99c1ee700b2185f46f20 docs/architecture/SECURITY_FOUNDATION_1_37_0.md
771c4771c4d7335750c5a2d976c765b4ed66ed936d8550d7af69de14a16705bb docs/architecture/THEME_MANAGER_1_36_0.md
67a1be91b0c2d2797716f785f2f770c881cf88d7fd617722468df8d949d0638a docs/AUDIT_1_31_0_IST_UND_LUECKENANALYSE.md
2acba8fcc5da42e5a808925a86628bc0c6fc817ddb082b0ead3857c6a43460f1 docs/BRANDING.md
2b833d05defcb782eea32fe5cb1b69d03322677a7babf6a36ade616da18fd6ab docs/COOLIFY_PRODUCTION_1_41_0.md
f23c643236c84b0e3715620598f358065748b6d090229003e23df8ad9320b54e docs/FULL_LISTING_EDITOR_2026.md
ba9a9b93622fadc399cd9067d80b4ea339193eb19825106264a3b86b7128d107 docs/GENERATOR_STUDIO_2026.md
54f45ae5c2f12b6314b3f1bc3e59e753c64ca7cfe39644725d765e9f02f637ef docs/GITHUB_DEPLOYMENT_1_34_0.md
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
c9c8f934a0c4192dae29df913588188aab1321364d1c46b9c66da5c754e9df41 docs/HOTFIX_1_23_2_FLUX_EDITOR_JOB_COLLAPSE.md
2306ff399000c07a9d7e260a58d92c748fa3219922a4dda25f6b2121a1ad2dbc docs/HOTFIX_STUDIO_FLOW_DETAIL.md
0584257245a29de62a6374eb8b114bd19fc6a36306ed990d820ed2062514d6a6 docs/INSTALLATION_1_32_0_WINDOWS_DOCKER_NAS.md
7c1afdefd1e3a75a5d6b49aedae80e266bab4e91cbfc657fb862ddfd737dc792 docs/INSTALLATION_1_33_0_MULTIPLATFORM.md
1475e484cccb0db67e02abbb05f8b8e676c85536ec4f5bda7dd99d4563fbc049 docs/INSTALLATION_1_34_0_ZERO_EDIT.md
1df2aed413865c03e541e66249c1df3eed6d3c3aea9f0ffdeaee40453665df14 docs/LISTINGS_UI_2026.md
4f74fef6877126d2579356b9731955e29f05ea8b55b59abb91569e47730a1c3f docs/PUBLISH_CENTER_2026.md
bae236366d6d2cb0139e4dbf8baf099599bbff27aacc9eeeba20fdff644f5fb0 docs/RELEASE_1_24_0_RESPONSIVE_MEDIA_FLUX_ARCHIVE.md
5223408e51f35ba5f6ce86155544311dd13f60522a6d482f372d64788ab466ac docs/RELEASE_1_24_0_VERIFICATION.json
b66524ffd19976f650c6b26b299898b182355950f545c131de16c89fcffe0ebe docs/RELEASE_1_25_0_RESPONSIVE_GALLERY_SMARTCOPY_PROMPTLAB.md
1c5ebf1a496c9571fe9d941793371f9f0be89d85a587c8fa9532f9b25bba92bc docs/RELEASE_1_25_0_VERIFICATION.json
1eef86e13d9429701e9d60894f36fe0eba82c974565b1a6687ed157ca6da680e docs/RELEASE_1_25_1_ADAPTIVE_GALLERY.md
14475ae4169f4bde4a5b7ae5512867f972ede69dcb621d7cc6a208d4fdc6a331 docs/RELEASE_1_25_1_VERIFICATION.json
f02b80194f434dd3d82b5c4c111e8cae65f1f053a267cec2793a1ff551daff9a docs/RELEASE_1_26_0_ARTIKEL_EXTENSIONS_PUBLISH_GALLERY.md
570522a6513134227122da1b5f2f56a421c62d3b0b5adba703872e9026fa2b58 docs/RELEASE_1_26_0_VERIFICATION.json
4cfc47f35765259cbd6a841f51a85fa86600991d1ca545b10756f15b6d332f7f docs/RELEASE_1_26_1_STABILITY_GATE.md
9646b913994b1bf8ce03eab57e7a6a87061540e44fdde63ad5b7eee93ca9f475 docs/RELEASE_1_26_1_VERIFICATION.json
7c6c3a2e4ce478f47e540a34a3505dbbe11b8f2175d0b274bec7643457997140 docs/RELEASE_1_30_0_OPERATIONS_CENTER.md
6f9c206869ba94c461b1d9f8e40847792b8891bb27809a0c6fa3d1d68a4e8598 docs/RELEASE_1_30_0_VERIFICATION.json
5da62e06d768ce832d18fb3ecb3ccca2f051a6218d2d3cb492d1e5aebbcccb52 docs/RELEASE_1_31_0_MULTIUSER_SECURITY.md
6288a31dfa61f71d13f79d40ab64bb7ca475de07ada154c002cd4b0d44a284dc docs/RELEASE_1_31_0_VERIFICATION.json
4ee88ff8c3c655cb31bc18192a37ffdd0eedf8e26039b252094d5ce169c1883c docs/RELEASE_1_32_0_DOCKER_NAS_PRODUCTION.md
628d4d1a6c7d837a0c8c22958253193642ddfeaab5f425f418f9f32b46e84340 docs/RELEASE_1_32_0_VERIFICATION.json
624919ea60969079f767068771800dbc04ea7a87eb1a53d7f0497447492c25cb docs/RELEASE_1_33_0_MULTIPLATFORM_DEPLOYMENT_FLUX_ADMIN.md
3f9ae39792bec600ddd534284ae62cb2b4f9db01fb54c09cee8ae161551caa2a docs/RELEASE_1_34_0_GITHUB_ZERO_EDIT.md
c724b699894d5ec3c050af0253c6de0c645a789219a344ad7ffe31e330d9921b docs/RELEASE_1_34_0_VERIFICATION.json
7caa9e7b1e51af362332e9c50165b60c54c9219c7384c8313d3735c387df1c01 docs/RELEASE_1_34_1_GIT_SAFETY_WINDOWS_HOTFIX.md
ac834ee57ab6bd8fe3dd0238837193e66e01d7e78590a289347b002591da3fb4 docs/RELEASE_1_34_1_VERIFICATION.json
51057d17e2636ef1bdfc6751dfad29ebd34fbe7f41b870f8f2f756a5c97d1c4a docs/RELEASE_1_34_2_GIT_SAFETY_DIAGNOSTICS_STAGING_FILTER.md
4c2c141ffc85b1d7517672abf2b28bb19fe4769505c0eb4d37e22bae06826cb3 docs/RELEASE_1_34_2_VERIFICATION.json
d55eaec79e10800a930fa030367ffa34037755e5757bc532e72d25711c5ca09a docs/RELEASE_1_34_3_UPDATE_ARCHIVE_EXCLUSION.md
4bb731af51e786a4827ec48bc7c4d4843c287456aedc09d8b74aa0eed3c0ec16 docs/RELEASE_1_34_3_VERIFICATION.json
e7bb3bb950e3c5303746f43139ca8f7c985d2d73896b44f5b707dd46a9152529 docs/RELEASE_1_35_0_PLATFORM_KERNEL.md
33eb9f0059a2ddc4b4debe5bf733720f2a5bb22f2710bab0a0cc9ac1b898ff11 docs/RELEASE_1_35_0_VERIFICATION.json
5a9fcfa0ea6e9c5c13f6d943e1de4d31c21e21fcff9186e8a714d7a16ed1c18f docs/RELEASE_1_36_0_SECURE_THEME_MANAGER.md
2dfe705857424865f101e861f4e633c8a7fb3c0e8dab874cb6620fd09b1ca902 docs/RELEASE_1_36_0_VERIFICATION.json
527806c00fb51334457726a9a7e9f6fc13f4dacd985fa2d2b97d5eb21c543853 docs/RELEASE_1_37_0_SECURITY_SECRETS_OBSERVABILITY.md
c3a8a21f5a3c3edb05ab1762a06d05b7f6eced3be84cb4f6f5e7f31bc627f06b docs/RELEASE_1_38_0_AUTH_USERS_SESSIONS_SETTINGS.md
c17771f19c2dfd6629fb91b66dfd661eed3459f820b71a427f0f98b07b45c56d docs/RELEASE_1_38_0_VERIFICATION.json
26355d15c188b1ce3bd03a539e1b27ee884352db48e5db423f60cdcd4d9d49f3 docs/RELEASE_1_39_0_LISTINGS_INVENTORY_MEDIA.md
c016becfaa0c1ae5bcee255476dd2a09fd465308f5ec2fba6d3c6809b009538b docs/RELEASE_1_39_0_VERIFICATION.json
837ac91a926330b800a027a61d4a2aa0386cfd61f9b7949231bbb09e1ca0e6c3 docs/RELEASE_1_40_0_MODULE_MANAGER.md
e52467c05ad08a262ff812989f2be19bfad3038f35f61e9deabcbf61b81fa5df docs/RELEASE_1_40_0_VERIFICATION.json
dd77564536e5d85eaa76c7a429a61f73ac78547d72734620bc4102b3452cd7b4 docs/RELEASE_1_40_2_CAPABILITY_GRAPH_HOTFIX.md
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
fe5756f788b2ab131f0832504d90d46d1ed744bfe470fd040803985234afa3cf docs/SLICE_02_ACCEPTANCE.md
05614ecf41becc399acfa6a3b2f681f37a8c80f5ce0130b98c578b2c8c60713c docs/SLICE_05_ACCEPTANCE.md
0838bcc6464a6beff39f0b4b056e513c1a390193521d9a8fc5d440d38034e9d0 docs/SLICE_06_ACCEPTANCE.md
ae0f7c6f6e614ec77e362db1076288596ec48d11b78f33f26908fbc40ae5dd18 docs/SLICE_07_EXTENSIONS_RESPONSIVE_MOBILE.md
b561983736639a08146d3642b718651907853d589ee32e5f421fc090f2c75a20 docs/SLICE_08_EXTENSION_AUTOFILL_UX.md
c5bf7a0c9f787f100e422d0deb27bffc2c8c86b0e7c1ce1f35d2de8d7d8ea774 docs/SLICE_11_DASHBOARD_RESPONSIVE_HOTFIX.md
876675bc3015838f568a6970ef6c01c9fcafe1d8d138c4379bc0f2ee1fd7445b docs/SLICE_22_1_SETUP_AUTO_RECOVERY.md
223f50d05096b3bfa3ac3a9f0880a2055fc2029b7d70fabd086612c3348cb34a docs/SLICE_22_2_DOWNLOAD_RECOVERY.md
95e1d560378120d7ccb0f363720ba832505631aa65616abd4df01baba5ecc269 docs/SLICE_22_3_POWERSHELL_51_PARSER_FIX.md
88f5a2d021874764b6a7f56c690a1e2112155b83cdf0d4d964b2c6e00ca756de docs/SLICE_22_3_SHA256.json
791a5d30f0d6ff9664722a2d00dff0db0bbdf2207e83ab391a40d6c4ab913bba docs/SLICE_22_4_SHA256.json
d14c938c6c9397290f654406d4b995b63db0f22586e78fc97fb5fef575748807 docs/SLICE_22_4_UTF8_BOM_FIX.md
f00c325ec3c879b47908dd5410cc943febb923200e9b9bd7d18197b1570a26fe docs/SLICE_22_4_VERIFICATION.json
e140f3ee3298b3894126e37900b04ff1073ed400e6414e35897815b3f674d93e docs/SLICE_23_FLUX_REFERENCE_PROGRESS_SETTINGS.md
b4b01b9f3751940d184d6fff269108798c7ec62ca7796b923cf82abbc142ff3b docs/SLICE_23_VERIFICATION.json
05e3fcf5aa9a0d8d53175a3d24c5feb36e55f73edfeadf05384e87a5de73353a docs/SLICE_28_1_FLUX_RUNTIME_HEALTHCHECK.md
43d867bb8b455f7450e5146648b28c759e50ebab45855d75a36dbc9f91d3f654 docs/SLICE_28_FLUX_STUDIO_VTO_CLEANUP.md
0bdbeed4b57767427afbebeddc30ea58a211f851663939d6301af38820cf098f docs/SLICE_28_VERIFICATION.json
1e20fd1abea3ce61f7f9cc88c6e8e4ddb0fe3513c5600e41b8f7c3f8d38e3ae9 docs/SLICE_29_FLUX_QUALITY_SETTINGS_TABS.md
1df7305e6a43212a582d5f83630e0ba04fbdd3da0d673d9dcb8f1c5f7ca37731 docs/SLICE_29_VERIFICATION.json
07d1ad0c19567cbdb05623567816f1506197a31e86fbcd1742f4ffa300335ae4 docs/SLICE_30_FLUX_STUDIO_PRO.md
008237388c4816d3cd67f3f0abfd0512f4ebf2dce75ab6790de41b0481ae1d97 docs/SLICE_30_VERIFICATION.json
db1674505e2ca7eb36c8752ea9b2d6fe2f2c12a5e6a1584e2722e20445b05e34 docs/SLICE_31_ADVANCED_IMAGE_EDITOR.md
3133a06e68cd98e615797665956f1bc401c0427045298b46c48ca590be3ef9da docs/SLICE_31_VERIFICATION.json
d425fc1724ac55853bf3433348486de8f8aab111f53fefee3c532beb1deb82af docs/SLICE_32_BATCH_IMAGE_FACTORY.md
0a8917412d000168956f7ac55e60ab79b54a8834deef0386a681bb8f920514cd docs/SLICE_32_VERIFICATION.json
25ff8492360d473e99c75c308be5f75cf84e0c11574d42aa6553d44f8bb35582 docs/SLICE_33_ARTICLE_QUALITY_CENTER.md
1d5411f69c62222d3155f0e89f2b4b5a3effe317ea1e59347efa7730a8e1b0e9 docs/SLICE_33_VERIFICATION.json
1161decfabd73c40ff1d2ffbc7d1b9b5168a8fb5fa392187c1b431e9a7f3a78f docs/SLICE_34_PUBLISHING_HARDENING.md
aa7ebc90470071566749c61b0dece4169f87666dcca4b88922f7fbafcfdd5a84 docs/SLICE_34_VERIFICATION.json
cc77a92ca268d3c444c6113a9cfaa38d3c805a579f6b1b21b7dd43a6ada58551 docs/superpowers/specs/2026-07-06-autolister-features-design.md
4b66a14b4515e38477f10325ab9d03d9782ad0d426845aefe2d508986ed2cd81 docs/UI_FOUNDATION_2026.md
5e655fcf3bb1182f8850b4104b56697ea952b01d2698af6d3d40b05e338b3506 docs/UI_UX_FIXES_SLICE_03.md
31b3d3ef6368e0b09b073d5641fd5649264fe78662de5d9ca4c1eef71f300e15 docs/WEB_UPDATE_CENTER_1_41_0.md
3873602c1529daad6647a7585c858d817821de0afbec93a26d1b3f5a7b9cb01e extension/background.js
b86f00c7afa1dc921dd66f5cfea8c817eebc8876b9daca6a55dfd03680156c79 extension/content.css
8ee8d1a94bbf83e5d09aee3c7d8c4ea5f216b2a51e73b6dbc74f7e6b99017a1c extension/content.js
409a96b169bc91efbcd7e923614a8cb30c6ca36cbd727f41e02c2ee4f89098cf extension/icon-128.png
8c031a533aeec86818d442ce96e859497817d16c2c9bf037c68955b90e8ced6b extension/icon-16.png
9963f60ab46d7f015a9400ee22e8e7a9fed31781a3907ef14b674d179d8f5f45 extension/icon-32.png
64d7b63beed9f972848daabc86484b5672b42ac7ed402a0d3effb47e200bf253 extension/icon-48.png
5904aad2263358a5be638db435d5a0576b8c2bf21d1a20114636d65dbdbc07a8 extension/icon.svg
c2fdc0530c71ef400fe9d52aed914f65d331b0d89dfae7ef1d156d54d1a22a6b extension/manifest.json
e61fd8e1264995fd633bd76e71329813dd02f361286329832a18dfd5a8bdd372 extension/popup.css
bdf5b794473cae818789cf95a28a9c9a9d50f81529d72fdf45a536247a783922 extension/popup.html
d7893cfdd686d4a6a1bea333468d4ba72cfff0e7166687181a2d37ba1faf3332 extension/popup.js
c7db96df5fed3c8336062204322d62b4870f5c8ab1d30ea48cc6e7e38ed3513b extension/README.txt
273322e8dc29b0c6b639a6cc7e44a9570ceece09306fd46003b05a8fc86c88b7 extension/vinted-categories.js
3873602c1529daad6647a7585c858d817821de0afbec93a26d1b3f5a7b9cb01e extensions/background.js
3873602c1529daad6647a7585c858d817821de0afbec93a26d1b3f5a7b9cb01e extensions/chrome/background.js
c5a0ca59a8858e9bd484d1e5fb7a2aca72db092fb062674f531bb76f5a0c952e extensions/chrome/content.css
22bd64723d279a6229bccba2a4cec56f1ae83f4cd5628bd1a1841930b0f442f6 extensions/chrome/content.js
409a96b169bc91efbcd7e923614a8cb30c6ca36cbd727f41e02c2ee4f89098cf extensions/chrome/icon-128.png
8c031a533aeec86818d442ce96e859497817d16c2c9bf037c68955b90e8ced6b extensions/chrome/icon-16.png
9963f60ab46d7f015a9400ee22e8e7a9fed31781a3907ef14b674d179d8f5f45 extensions/chrome/icon-32.png
64d7b63beed9f972848daabc86484b5672b42ac7ed402a0d3effb47e200bf253 extensions/chrome/icon-48.png
5904aad2263358a5be638db435d5a0576b8c2bf21d1a20114636d65dbdbc07a8 extensions/chrome/icon.svg
c2fdc0530c71ef400fe9d52aed914f65d331b0d89dfae7ef1d156d54d1a22a6b extensions/chrome/manifest.json
e3a3863b58a571a0f0180bdd333f9389b918415b4ea96b70f3ab6d4105d95c50 extensions/chrome/popup.css
559dc70841ebb186ff96f9f2ba32ef26a3a7f8edd08363f09c0d7163049eae5a extensions/chrome/popup.html
2bf2d5ccf02622cf1d62fd553d21c0ef1b744616742e7962ac302c10d37426e2 extensions/chrome/popup.js
273322e8dc29b0c6b639a6cc7e44a9570ceece09306fd46003b05a8fc86c88b7 extensions/chrome/vinted-categories.js
c5a0ca59a8858e9bd484d1e5fb7a2aca72db092fb062674f531bb76f5a0c952e extensions/content.css
22bd64723d279a6229bccba2a4cec56f1ae83f4cd5628bd1a1841930b0f442f6 extensions/content.js
3873602c1529daad6647a7585c858d817821de0afbec93a26d1b3f5a7b9cb01e extensions/edge/background.js
c5a0ca59a8858e9bd484d1e5fb7a2aca72db092fb062674f531bb76f5a0c952e extensions/edge/content.css
22bd64723d279a6229bccba2a4cec56f1ae83f4cd5628bd1a1841930b0f442f6 extensions/edge/content.js
409a96b169bc91efbcd7e923614a8cb30c6ca36cbd727f41e02c2ee4f89098cf extensions/edge/icon-128.png
8c031a533aeec86818d442ce96e859497817d16c2c9bf037c68955b90e8ced6b extensions/edge/icon-16.png
9963f60ab46d7f015a9400ee22e8e7a9fed31781a3907ef14b674d179d8f5f45 extensions/edge/icon-32.png
64d7b63beed9f972848daabc86484b5672b42ac7ed402a0d3effb47e200bf253 extensions/edge/icon-48.png
5904aad2263358a5be638db435d5a0576b8c2bf21d1a20114636d65dbdbc07a8 extensions/edge/icon.svg
c2fdc0530c71ef400fe9d52aed914f65d331b0d89dfae7ef1d156d54d1a22a6b extensions/edge/manifest.json
e3a3863b58a571a0f0180bdd333f9389b918415b4ea96b70f3ab6d4105d95c50 extensions/edge/popup.css
559dc70841ebb186ff96f9f2ba32ef26a3a7f8edd08363f09c0d7163049eae5a extensions/edge/popup.html
2bf2d5ccf02622cf1d62fd553d21c0ef1b744616742e7962ac302c10d37426e2 extensions/edge/popup.js
273322e8dc29b0c6b639a6cc7e44a9570ceece09306fd46003b05a8fc86c88b7 extensions/edge/vinted-categories.js
3873602c1529daad6647a7585c858d817821de0afbec93a26d1b3f5a7b9cb01e extensions/firefox/background.js
c5a0ca59a8858e9bd484d1e5fb7a2aca72db092fb062674f531bb76f5a0c952e extensions/firefox/content.css
22bd64723d279a6229bccba2a4cec56f1ae83f4cd5628bd1a1841930b0f442f6 extensions/firefox/content.js
409a96b169bc91efbcd7e923614a8cb30c6ca36cbd727f41e02c2ee4f89098cf extensions/firefox/icon-128.png
8c031a533aeec86818d442ce96e859497817d16c2c9bf037c68955b90e8ced6b extensions/firefox/icon-16.png
9963f60ab46d7f015a9400ee22e8e7a9fed31781a3907ef14b674d179d8f5f45 extensions/firefox/icon-32.png
64d7b63beed9f972848daabc86484b5672b42ac7ed402a0d3effb47e200bf253 extensions/firefox/icon-48.png
5904aad2263358a5be638db435d5a0576b8c2bf21d1a20114636d65dbdbc07a8 extensions/firefox/icon.svg
1954ca9f34b3afe109935173f685482f06bb685a9b8f0d4c344f0c6133ccfaaf extensions/firefox/manifest.json
e3a3863b58a571a0f0180bdd333f9389b918415b4ea96b70f3ab6d4105d95c50 extensions/firefox/popup.css
559dc70841ebb186ff96f9f2ba32ef26a3a7f8edd08363f09c0d7163049eae5a extensions/firefox/popup.html
2bf2d5ccf02622cf1d62fd553d21c0ef1b744616742e7962ac302c10d37426e2 extensions/firefox/popup.js
273322e8dc29b0c6b639a6cc7e44a9570ceece09306fd46003b05a8fc86c88b7 extensions/firefox/vinted-categories.js
409a96b169bc91efbcd7e923614a8cb30c6ca36cbd727f41e02c2ee4f89098cf extensions/icon-128.png
8c031a533aeec86818d442ce96e859497817d16c2c9bf037c68955b90e8ced6b extensions/icon-16.png
9963f60ab46d7f015a9400ee22e8e7a9fed31781a3907ef14b674d179d8f5f45 extensions/icon-32.png
64d7b63beed9f972848daabc86484b5672b42ac7ed402a0d3effb47e200bf253 extensions/icon-48.png
5904aad2263358a5be638db435d5a0576b8c2bf21d1a20114636d65dbdbc07a8 extensions/icon.svg
c2fdc0530c71ef400fe9d52aed914f65d331b0d89dfae7ef1d156d54d1a22a6b extensions/manifest.chromium.json
1954ca9f34b3afe109935173f685482f06bb685a9b8f0d4c344f0c6133ccfaaf extensions/manifest.firefox.json
e4ea31f91a4c8df18901bfc77fc351a8808d19d690243b80bc4faa140e18f851 extensions/packages/vendoo-link-chrome-1.9.0.zip
e4ea31f91a4c8df18901bfc77fc351a8808d19d690243b80bc4faa140e18f851 extensions/packages/vendoo-link-edge-1.9.0.zip
74c219587cf0f827a1a1e2b8adacac55477be717fd4ae80e2ae027a9ffb0744d extensions/packages/vendoo-link-firefox-1.9.0.zip
9e828844f41154c87ccfc5d8f3a19476396e2782fbd0f956465b42fb508cb098 extensions/packages/vendoo-link-safari-1.9.0.zip
e3a3863b58a571a0f0180bdd333f9389b918415b4ea96b70f3ab6d4105d95c50 extensions/popup.css
559dc70841ebb186ff96f9f2ba32ef26a3a7f8edd08363f09c0d7163049eae5a extensions/popup.html
2bf2d5ccf02622cf1d62fd553d21c0ef1b744616742e7962ac302c10d37426e2 extensions/popup.js
48b5f9f4ecc1c1c42f37321e9bb2fc857c646b9c16c34520fc7d4ca59a1036ac extensions/README.md
0b2a03d06faae7aaebeee8ec9fe1864422c8651f17ae90b185fd31474ac5eeb0 extensions/safari/build-safari-extension.command
afd76deccd8bcf26d39f8c86deb1de578873ddd8b09ac18eebacae33169bb420 extensions/safari/README_SAFARI.md
3873602c1529daad6647a7585c858d817821de0afbec93a26d1b3f5a7b9cb01e extensions/safari/web-extension/background.js
c5a0ca59a8858e9bd484d1e5fb7a2aca72db092fb062674f531bb76f5a0c952e extensions/safari/web-extension/content.css
22bd64723d279a6229bccba2a4cec56f1ae83f4cd5628bd1a1841930b0f442f6 extensions/safari/web-extension/content.js
409a96b169bc91efbcd7e923614a8cb30c6ca36cbd727f41e02c2ee4f89098cf extensions/safari/web-extension/icon-128.png
8c031a533aeec86818d442ce96e859497817d16c2c9bf037c68955b90e8ced6b extensions/safari/web-extension/icon-16.png
9963f60ab46d7f015a9400ee22e8e7a9fed31781a3907ef14b674d179d8f5f45 extensions/safari/web-extension/icon-32.png
64d7b63beed9f972848daabc86484b5672b42ac7ed402a0d3effb47e200bf253 extensions/safari/web-extension/icon-48.png
5904aad2263358a5be638db435d5a0576b8c2bf21d1a20114636d65dbdbc07a8 extensions/safari/web-extension/icon.svg
772f1e2e33f4d6d882fa0af663e315fd3b8208141227bb5beb9baaf1f4e08397 extensions/safari/web-extension/manifest.json
e3a3863b58a571a0f0180bdd333f9389b918415b4ea96b70f3ab6d4105d95c50 extensions/safari/web-extension/popup.css
559dc70841ebb186ff96f9f2ba32ef26a3a7f8edd08363f09c0d7163049eae5a extensions/safari/web-extension/popup.html
2bf2d5ccf02622cf1d62fd553d21c0ef1b744616742e7962ac302c10d37426e2 extensions/safari/web-extension/popup.js
273322e8dc29b0c6b639a6cc7e44a9570ceece09306fd46003b05a8fc86c88b7 extensions/safari/web-extension/vinted-categories.js
273322e8dc29b0c6b639a6cc7e44a9570ceece09306fd46003b05a8fc86c88b7 extensions/vinted-categories.js
4be8e17f83ebb805abb1beddf4d7a79e3a44ce58585cb6b8143bb01a47e16c6b FEATURES.md
7f78a0d512de717ba76c9a5e3e1bd2e52db9edf146e8eeb7297458fde5d92afa GitHub-Deploy-Assistent.bat
9a5bfcafdce53130d7bc36695090a8f87096b4ead9dd5ccc3d46d1d80c07a0fa lib/ai-claude.mjs
ad6ba4ef521557d5c06bc546cda70db57e710e8173daadae74e8f15f99836c1c lib/ai-flux-prompt-jobs.mjs
347778ce521a1919ec0679918171dc625d7f04d6dec26c8a4700e702bc78de67 lib/ai-local-images.mjs
aeb5103c0179232b9954b7854be8099cf4bb0f3ef1ee908eb5e4faf6a8f1205f lib/ai-local.mjs
29fbe09982b0b327b773bd119f57f50363424161815ac894ca4d677759e93e83 lib/ai-model-jobs.mjs
9f474f665d6d610fe00d9e707e85314a854abb906e31db2bb9a3a7c76785ff4f lib/ai-model-photos.mjs
3fb484456b9e6374f14b53a2276eea8533642b7c9073bb73a1bb0dbed7048bbd lib/ai-openai.mjs
581c10b8346d0aa10169ed23ed49c038951ecd5cac014722c1080bbd5aec1c5b lib/ai-openrouter.mjs
271696d7cbef3581b667a8ce3b07d0b17b1e8b467fbede4d8aa512ecc5225515 lib/ai-router.mjs
76a2f1fdf60053739d82962264219060807022dfc9a2b3a644946fddff02a8d9 lib/archive.mjs
2015974b9ceced554ecf0cfbdbf50a7d1db57459d50fb0241b8e9277559e70f4 lib/auth.mjs
3e2b75c0758703e590233cbcc9b589e3177134accecf5ed921791891cbfc8522 lib/categories.mjs
86793fb57a1dad32f4d93e34c27ffe9e77a95785dd30b2b55ffb67d0ad5e14fb lib/config-store.mjs
8c203e70e9ab07b996d4a53031892bbf37b10efd024f9e0013266cf2af0244a4 lib/db.mjs
54722d3f76ad9ee4fbe731d3a65c2a03d343eb82d98f8ea5a5996b1bec24d57f lib/ebay-api.mjs
524f1b48d467998da819f0b947f2551ad45784e588eb442304c5f6e925dffaee lib/etsy-api.mjs
5b2c566ff14fa502ba2cd4d4c918fe238a8e93c3283d521144093a897f9d8018 lib/fees.mjs
7dcb2fb5606140afb7a019094d20ef88e9b4e2c70041f77b06d67d8992fd25ce lib/html-templates.mjs
e41a13284429ac01652d979efd0734d5a2b29945e7e93a910b44eaefab9c00b6 lib/image-batch-jobs.mjs
5ca5fc65a7de7c27243646d291dc42c0b5267a47705a09b306cd2c2eae2a77c8 lib/image-editor.mjs
5e7aa66faddd3d19bc6ebb9ddc13abfc4ab4f17d8c51bb39b2c4e0279385d788 lib/images.mjs
606f4cb1013c711d971dd201ab06df84ecb128bf5d5277095ce2e2ea56029e45 lib/mailer.mjs
493895deb08e51ef1f47b55c88e3225bb267ade9fe305a7ed2c59a3f417fab16 lib/operations-center.mjs
f10c3951bc604392e9fdaef7ba51b227893266baaa93981aa480409fed722062 lib/publishing-jobs.mjs
3a728417dd668d60fd1526ec738cb9b302f86c01d62b2227f12b2e44b7d389c7 lib/quality-center.mjs
74c783c1e06e9cc2da0165261a49511aaf374dab3159cbc24c0d69b6b467dcb9 lib/runtime-paths.mjs
d3d862ab32fd276ecffb54c0307976ca680135d92f24d024c95d365a50257643 lib/secret-store.mjs
4ac2e7f7c2235109a2ea949b2ff7b649d05ef607b95704676a43c60ddb1ae4a0 lib/security.mjs
2fd970a6a357a26211f0b9bcf432e59e79f73dc9a4cd35f88cca939655eefdff lib/structured-logger.mjs
ddb9c7677600795a4adf8a19a7525b7551455459d16eed4cdfbe751ef9ddf0eb lib/zip.mjs
2924f4e8abe5ebbd099fe1ceb2bc6ceb4bdda55fa3e57dd299cfc05d310a9246 local-ai/start-local-flux.ps1
7c6176d8311b1ba9d511beb40edd41b8248e61c298528bbec3f9ef1171856c05 local-ai/stop-local-flux.ps1
5f220a2dca02cc0764f6d0157677611b26c88860b48367eb661b6e47a1b92265 local-ai/workflows/vendoo_flux_schnell_api.json
302ab1609d08b3059ba6563b676c64f0709651f2e3c531857bfca3840c535a79 MockupDesign/AdminDashboard.png
49f7625a1299a16576feabfd7824ec3511391bd9b7c53a1fba9232da7e8bd6c6 MockupDesign/ChatGPT Image 7. Juli 2026, 10_05_19 (1).png
7b8cc9c5db8718b0b653ec6aa9d2a8bfa1bba03810248c3ba17745b6f15cc412 MockupDesign/Dashboard.png
4e3e797ed5d3ed0b60d00d8e567fbe51543c08578b858ef71e77b274ae449fd0 MockupDesign/Einstellungen.png
c78fd6f673c0e59789ea313396b12c2f21f75a2ee6c7ad2de81a6b89f6d90588 MockupDesign/ExtensionDesignMockup.png
ed20bc04cab8e85245a855810a61b736a682f972eb85e23340edc9b558851a93 MockupDesign/Heute StudioFlow.png
1ad88f45f377ab9b3f35f96a0ceaaa7f75cb0a996e83370d76c54f527392a08d MockupDesign/IconLogoMockup.png
0e8b4a31d075f1f83ea62b5eeeb53e89859e395a696deb43155932a23e7cef86 MockupDesign/Lager.png
b1c62e118d9be316274e966c558cdcccdd41afb3a9f05a60ec1e4a3433d2ca0c MockupDesign/Listings.png
a52983b9ea2bf920d115485d10bc30226da88bc5cba8b00bebf801b21c096346 MockupDesign/Publish.png
8c8bf278d0c6725e1dbb97b3c3d0c36a9bfc0753c3c86433fdc294bec70dbc0e MockupDesign/Startseite.png
62f14221b1d71c9e631efdadd221397ff657adae4b737df52c8bff1610b90999 MockupDesign/Vorlagen.png
a111671a21576140185a0c4d01038e200f87cdcbf903efe57bb84f65c972fd1a package-lock.json
740a9ee1699aa8e24fab4ab66373bf4322bee0e60c6dd80dbc07e8f9d18a3771 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
e0e7b8ef01c36f0625b6157aa46bd69de05c17be0b054208fb635d6507b52154 public/apple-touch-icon.png
a2ea3f5f56dba8bd4d23fa54a26fd941567b4febdd1ed1b4e9f5bcc1c9786e53 public/brand/favicons/android-chrome-192x192.png
46ea6ba4ca6ca2352f37353b8bc4ca258935d29ecb8d3c9b133a48ea75c0c58f public/brand/favicons/android-chrome-512x512.png
e0e7b8ef01c36f0625b6157aa46bd69de05c17be0b054208fb635d6507b52154 public/brand/favicons/apple-touch-icon.png
a0f1e01de4e8215e690c1ce869fecaae9d7ba3cc95883340a53ff38f6be52ce0 public/brand/favicons/browserconfig.xml
8c031a533aeec86818d442ce96e859497817d16c2c9bf037c68955b90e8ced6b public/brand/favicons/favicon-16x16.png
9963f60ab46d7f015a9400ee22e8e7a9fed31781a3907ef14b674d179d8f5f45 public/brand/favicons/favicon-32x32.png
64d7b63beed9f972848daabc86484b5672b42ac7ed402a0d3effb47e200bf253 public/brand/favicons/favicon-48x48.png
15bf8dd655b34a8015447e834d387af12685238ede598f5f0e7927eea2933483 public/brand/favicons/favicon.ico
2daa93cca0ad2c0cab90a9e8ad4ea0ae4b64feeb68c422954020d17e2376a381 public/brand/favicons/html-snippet.txt
3798313cc939d9fa582a92638356abfcfc8f8b4f143d9434b7012446195e6937 public/brand/favicons/mstile-150x150.png
c372675942caa5010340f21fe521adc72af537d909008731976820fd84604d42 public/brand/favicons/site.webmanifest
b2dc767e7a01395d23a7f3f5915324db1a7d1912bac60470d2f7c10815895173 public/brand/icons/vendoo-icon-120x120.png
409a96b169bc91efbcd7e923614a8cb30c6ca36cbd727f41e02c2ee4f89098cf public/brand/icons/vendoo-icon-128x128.png
d276f50b298e4dfcbba9147550463aee498531280399167d1e9774aa8e5f7e21 public/brand/icons/vendoo-icon-144x144.png
3798313cc939d9fa582a92638356abfcfc8f8b4f143d9434b7012446195e6937 public/brand/icons/vendoo-icon-152x152.png
47122dc91010721e2692d11986f0b91d9b76634130a95723e92b2a070268d16f public/brand/icons/vendoo-icon-167x167.png
8c031a533aeec86818d442ce96e859497817d16c2c9bf037c68955b90e8ced6b public/brand/icons/vendoo-icon-16x16.png
e0e7b8ef01c36f0625b6157aa46bd69de05c17be0b054208fb635d6507b52154 public/brand/icons/vendoo-icon-180x180.png
a2ea3f5f56dba8bd4d23fa54a26fd941567b4febdd1ed1b4e9f5bcc1c9786e53 public/brand/icons/vendoo-icon-192x192.png
3f2517cfd215cb624ed639d60b064dfa5e88ce71eb7bbebbbfaa70b3d62556db public/brand/icons/vendoo-icon-256x256.png
9963f60ab46d7f015a9400ee22e8e7a9fed31781a3907ef14b674d179d8f5f45 public/brand/icons/vendoo-icon-32x32.png
cae3eb737c8ad3a82a679b09d33d991b501bf482e0132e99aa1b8ad064e472b4 public/brand/icons/vendoo-icon-384x384.png
64d7b63beed9f972848daabc86484b5672b42ac7ed402a0d3effb47e200bf253 public/brand/icons/vendoo-icon-48x48.png
46ea6ba4ca6ca2352f37353b8bc4ca258935d29ecb8d3c9b133a48ea75c0c58f public/brand/icons/vendoo-icon-512x512.png
bc85af557be532dcce6beb82cf9bd61af683666b00fb61e4b2069f78e60a9a33 public/brand/icons/vendoo-icon-64x64.png
01d59b665bc90dddbd34221e3a2ec3af11bff6fb328aab2c9947c03d74b0465d public/brand/icons/vendoo-icon-72x72.png
ed3dfa59e989b154fac88a3c72ac840290ee3e1565d44aa75596bf0055c74020 public/brand/icons/vendoo-icon-96x96.png
46ea6ba4ca6ca2352f37353b8bc4ca258935d29ecb8d3c9b133a48ea75c0c58f public/brand/icons/vendoo-icon-original-1254.png
8fd9901e07dc9172d162706511ac8dcce220a4c907cb3efa9940c9c13fa32526 public/brand/logo/vendoo-logo-cropped.png
3f39371abd1efa6cc8366aac9a6d067f3339b8f2a2da77ab8a26c6bdcf8c4fce public/brand/logo/vendoo-logo-original.png
b56d07515f6220de7fd0cb149fed598a7361c985ae6ca7cb14329a99e8947aee public/brand/logo/vendoo-logo-transparent.png
e83921d2af9930ab27df1abe64374cb9311189bfaaa951068109177443571a07 public/brand/logo/vendoo-wordmark-transparent.png
1711a9387f2d6e75797b77c8daa92b94d7a15e128476e4b8aa9d54ccb0692a0e public/core/api-client.js
f30395399a5c6d7e0a884b22f28f7648e6343695cee9960d6ff13b34de7b48f1 public/core/frontend-module-registry.js
fb3dbcd03f7ed15b773b582c2a2b785c3040d3319aba5b7c682a7e74e1cf3b4e public/core/safe-dom.js
38aa57d13aeb3ab70babcb903bfee71e47d80932ed24a73267c13ff81c24e3ba public/design-system/theme-contract.json
308f1c0e2ed1de34aa11545a1a660aab02631e422d6c6945e3203d11666784fc public/design-system/theme-runtime.js
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
e37558ee9612a36db8a19999fbb320f1c249a8e155f94c83a82926bec920f989 public/login.js
59291f75eca405e3856bafea76cc601d681060eb6b36bf82a4be3c3ab7eac290 public/mobile-upload-expired.html
f046f166d89fc32774b87deb02e6d144fd2f40c0925aac67487e5b7c489a0ec1 public/mobile-upload.css
6152e4fe715efbf8fd6a7c4286f23172bc3cf15dde085e5e4f7389ad5e65ad73 public/mobile-upload.html
f13846801a8493aa43df6ec3d78b826b21b59e8f21d47735f68fd0b278e440c7 public/mobile-upload.js
3132436d52caba719a3460c29a807e6f0703cb2118a80c9fae6c30360498d249 public/modules/module-manager/module-manager.css
bf10d5ccc53f287a8b61d488af50c3151c764f36a29ee5d4fe9ee321fa0eda06 public/modules/module-manager/module-manager.js
01f2bd124257220005032bf234d122b9942fdd8fd57ed8195748b2fb1f5a5cfb public/modules/README.md
2d7a004db3e22b30ad3a682ba445bcee3fc1c5f0ed5e8fee45b5892c5b335e51 public/modules/theme-manager/theme-manager.css
0edc61f0c2bb604c3adc40813b18fe5ca8d9b846ec2a3d9f406ae26ae313d3ee public/modules/theme-manager/theme-manager.js
e0a6c981d4231eab26d981be62647d0e6f89e05e8371baad121348202ad54f49 public/style.css
ff525e0af963e6f54636cfdc1bd2d045aaf82e7b0199755eb5cdf4c52110be2e public/vendoo-icons.js
9cf6628dd714347563fdc5925e323c0a274ee5967d78555757be85c56fcdc0b0 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
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
abb508872f30a0d6e4b6d33ab2f4cb14dea11838b15dbe502e6fac2ecc16cf13 server.mjs
eae7a799aaeec708ea28aca9fc6b08132909cf0b41d412e889064876ce4ba94b setup-gui.ps1
cf4519ce5adc01f05458e3286e86f3a419cceaff5fcdd869a1d07f1c1d1a0d8c setup.bat
9433d663b6537ab5bb52a80733b18ac97e932ec50650b2d6bae8d67d5f05cdb4 setup.ps1
9cf6480f9eeda7fecc82a6bf1d9bfb3d94b75ca42c92e7697679b9efd8816215 setup.sh
2851c914a1b50a036cb265ddd32e5545cc7e8e34ba9d682445926acc23eb8f28 THIRD_PARTY_NOTICES.md
90485a16e3468cb9a6750646700fbb036cb9ac410e9b8bd5c582f7b923bf3c31 tools/build-release.mjs
94f4ced84834fceeb4f797bf8aab0703c873b57fd5d4a5b3fb02fee12e42bd2e tools/check-syntax.mjs
ac8440bce2659a035302609722f1dfed5d125223376cd575ffc0636397c94b54 tools/generate-design-tokens.mjs
5e1716ecb03aefc626bb8258ceb75c9372bef5d401b798a16703a65d87a9e6a7 tools/prepare-git-staging.mjs
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
cbd91b45d97e11bd4f5ca2f9d2edf2b5931cb2aa336cb71afc30aad9cffbe1e0 tools/verify-deployment-1.34.0.mjs
01a43040a95cdac40703f49031acd9267cdba99d9027a51718f35ffb643237fc tools/verify-deployment-1.34.2.mjs
906767e574c06aedebb2d7797150bda2f1e4c4536e8f82f4c7de2e3cc8398c8e tools/verify-deployment-1.34.3.mjs
71d00227a59592cc0ad8ba321c63350ec25748ef965d8cd0eff1317738c7fba8 tools/verify-deployment-1.35.0.mjs
b65eec7fd54e1459b363d99d20f9438ae5bc4b518e775de8b97e33e89a382e42 tools/verify-deployment-1.36.0.mjs
22a5ba1951b2e8dbc924e60aaff8be7c41189ae7b30de9ecf50d0b741e84e13a tools/verify-deployment-1.37.0.mjs
84961aed097ee00496dd853677895614263baf425b00218cc0e1f3ced1b5b7b9 tools/verify-deployment-1.38.0.mjs
2ecf465073b7ec9e8ba38bf83364f681886d50440fbdd579a5df14df5d1bb3af tools/verify-deployment-1.39.0.mjs
4b37972998d87c1336590e2550895a826d63b47e4828d5bb3ba619376c287271 tools/verify-deployment-1.40.0.mjs
947942c84416b126f29281a8bef7b8230ecde09ea8b3bf3304cfacb0f9623fff tools/verify-deployment-1.40.1.mjs
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
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
2f43dd1a89a36527cd3b4873b8cc9f7386cdfc06ade96fcc05dd5deb15dbcec7 tools/verify-identity-db-1.40.0.py
1b5c21eb5688bc15895541968fa1ab69a2384ed611b01d23901abc040dcc3255 tools/verify-identity-settings-1.38.0.mjs
a1dddb35f4b6eeafe47ff3f40e5565bdb4477c8f63101d3c0d7f84c7393abe4c tools/verify-identity-settings-1.39.0.mjs
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
584ea4735937238b4c3bffa70364d0f45cf1f3d41bf34adde64c7cbb7b729efa tools/verify-platform-architecture-1.38.0.mjs
f8590c6986786cbbce95e58af1d1d3fec61d163c19fcf4c22aff57f5954c398e tools/verify-platform-architecture-1.39.0.mjs
c9e704c372cf6a973fcdb887b337ead6509fd03e78570e6d1c9e24b1d879fe6b tools/verify-platform-architecture-1.40.0.mjs
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
e31a055d2ebb8e439f5b0e0235a71dacecb891e299377cfbbcfa8a32f53a85b0 tools/verify-release-1.26.0-static.mjs
7e107b5f9a1b4ee381ff31105feb6d38dda5c40e4afe105a955badc36e6b8d10 tools/verify-release-1.26.1-static.mjs
d9a75c45f40d857c84790d51dca2fd088c3851ca250405871a7e32773918c66e tools/verify-release-1.27.0.mjs
3989e3ec236c1aa4c387b26cc15e04cf200a018e29c095321acf826c6bae4d66 tools/verify-release-1.28.0.mjs
7d196d913a697248ab1233907fed56c10329cf201e3bdc173bca73cb02755369 tools/verify-release-1.29.0.mjs
3f30260dabbdf0fa7b042381cb9e24f840d0c3ddbc44fe309763789a7d14afb7 tools/verify-release-1.30.0.mjs
b1ceed5dfc17dbd3b038b7168e61076e4d39a0f1d67e2b87677a91c4dbb2daff tools/verify-release-1.31.0.mjs
a372258b97aa68d59a1fe0fd3f380d019c59412d5c408a6064f437d5cfc6e709 tools/verify-security-1.31.0.py
08820d9bb7be1dec496c04e9ea4a39b2ebb84d373a9481136610d16dea0edde3 tools/verify-security-foundation-1.37.0.mjs
a415d294679adab8ea76b86235a522e73ad9dd9ea0e2348691b1de3191255b81 tools/verify-security-foundation-1.38.0.mjs
805aa61d7f9cba8c20afda0dd871189108b02f3751f8a75963784cc12fd6cf76 tools/verify-security-foundation-1.39.0.mjs
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
41033ba36a11a14ab74a80d65c1124848eddee21a5f3fef8bd5ab001699e2e4b tools/verify-theme-manager-1.39.0.mjs
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
+4
View File
@@ -0,0 +1,4 @@
@echo off
rem Alter Dateiname bleibt als Kompatibilitaetsstarter erhalten.
call "%~dp0UPDATE_VENDOO.bat"
exit /b %ERRORLEVEL%
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+353 -31
View File
@@ -1,47 +1,369 @@
# Vendoo
# Vendoo 1.43.0
Vendoo ist ein lokaler Multi-Plattform Listing-Generator mit AI, Lagerverwaltung und Publishing-System.
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.
## Status
- Version: `1.0.0-baseline`
- Hauptbranch: `main`
- Entwicklungsbranch: `develop`
- UI-Sprache: Deutsch
- Repository: privat
## Release 1.43.0: Module-aware Navigation, Listing Studio & Updater UI
Die vollständige System- und Feature-Dokumentation befindet sich in [`FEATURES.md`](FEATURES.md).
- 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
## Tech Stack
## Release 1.42.0: Production Operations & Controlled Update Completion
- Node.js + Express als ESM
- Vanilla HTML, CSS und JavaScript ohne Framework oder Build-Schritt
- SQLite über `better-sqlite3`
- CSS Custom Properties für Theming und Dark Mode
- Session-basierte Authentifizierung mit Rollen `admin` und `editor`
- 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
## Projektregeln
## Hotfix 1.41.2: native Module zuverlässig in Git
- Neue Datenbankspalten werden als Migration im `MIGRATIONS`-Array von `lib/db.mjs` ergänzt.
- Neue API-Routen werden in `server.mjs` angelegt.
- Neue Datenbankfunktionen werden aus `lib/db.mjs` exportiert und in `server.mjs` importiert.
- Frontend-Tabs verwenden `data-tab`, `<section class="tab-content">` und die `TAB_TITLES`-Map.
- Farben werden ausschließlich über CSS-Variablen definiert; Dark Mode liegt unter `[data-theme="dark"]`.
- SKU-Format: `VD-XXXX`.
- Schreibende Requests benötigen Authentifizierung, Rollenprüfung und CSRF-Schutz.
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.
## Betriebsmodelle
## CI-Hotfix 1.41.1
1. Lokale Windows-Installation
2. Privater Docker-Betrieb für einen geschlossenen Benutzerkreis
3. Öffentlicher Docker-Betrieb auf einem getrennten Server/VPS
- `Archiv/`, `archive/` und `archives/` werden nicht mehr als produktiver Quellcode geprüft, nach GitHub übertragen oder in Releases gepackt.
- Das ESM-Gate prüft ausschließlich `server.mjs`, `bootstrap.mjs`, `app/`, `lib/`, `public/`, `scripts/` und `tools/`.
- Die produktiven Listings-/Inventory-Moduldateien werden separat als Pflichtdateien geprüft.
- Ein Regressionstest beweist: Archivstände werden ignoriert, echte fehlende Imports im Source-Root bleiben ein harter Fehler.
Ein direktes Freigeben des NAS oder von Port `8124` ins Internet ist nicht vorgesehen.
## Coolify-Produktionsbetrieb
- Dockerfile-Build direkt aus dem privaten GitHub-Repository
- Domain und HTTPS über den Coolify-Reverse-Proxy
- interner Anwendungsport `8124` ohne öffentliches Host-Port-Mapping
- ein persistentes Volume unter `/app/data`
- Healthcheck über `/readyz`
- FLUX Studio beim ersten Produktionsstart standardmäßig deaktiviert
- keine Weitergabe des Docker-Sockets an Vendoo
- genaue Anleitung: `docs/COOLIFY_PRODUCTION_1_41_0.md`
## Kontrollierte 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`
## GitHub-, Merge- und Releaseprozess
- Entwicklung und Import ausschließlich über Release-Branches
- vollständiges Sicherheits-Staging mit `GitHub-Deploy-Assistent.bat`
- Pull Request und GitHub Actions vor jedem Merge
- empfohlenes `Squash and merge` nach `main`
- GitHub Release `v1.41.1` erzeugt Release-Artefakte und GHCR-Images
- GHCR-Tags: `1.41.1`, `1.41`, `stable` und `latest`
- genaue Klick-für-Klick-Anleitung: `docs/GITHUB_MERGE_RELEASE_1_41_0.md`
## Plattformstand
- 18 registrierte Module
- 16 native Module, davon Produktbild Studio standardmäßig deaktiviert
- 2 transparente Legacy Bridges: System und Operations
- 41 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
## FLUX Studio
Für Coolify wird beim ersten Start gesetzt:
```env
VENDOO_INITIAL_DISABLED_MODULES=vendoo.flux-studio
LOCAL_IMAGE_ENABLED=false
```
FLUX Studio ist damit zunächst deaktiviert. Eine spätere bewusste Aktivierung über **Administration → Module** wird persistent gespeichert und nicht bei jedem Neustart wieder überschrieben.
## Einfachster Start
- **Windows lokal:** `setup.bat` starten und den geführten Assistenten verwenden.
- **Linux/NAS:** `chmod +x setup.sh && ./setup.sh`.
- **Coolify-Produktion:** GitHub-Repository per GitHub App anbinden, Dockerfile-Build wählen, Port `8124`, Domain und Volume `/app/data` setzen.
- **GitHub-Erstimport:** `GitHub-Deploy-Assistent.bat` starten, Sicherheitsbericht prüfen und erst danach `PUSH` eingeben.
## Sicherheit
Keine `.env`-Dateien, API-Schlüssel, OAuth-Tokens, SQLite-Datenbanken, Uploads, Backups oder Logs committen. Weitere Hinweise stehen in [`SECURITY.md`](SECURITY.md).
- `.env`, Datenbanken, Uploads, Backups, Logs, Secret Store, Master-Key, installierte Module und lokale Updatearchive werden nicht committed.
- der Produktionscontainer besitzt keinen Docker-Socket.
- Coolify-Token und Deploy-Webhook werden nie im Klartext an den Browser zurückgegeben.
- Deployments benötigen `updates.manage`, CSRF-Schutz, Rate Limit, Audit und die Bestätigung `DEPLOY`.
- Updates löschen keine persistenten Daten.
## Lizenz
---
## Vorheriger Stand 1.31.0
Vendoo 1.31.0 schafft die Grundlage für einen kontrollierten Mehrbenutzer- und Serverbetrieb. Rollen und Berechtigungen werden nicht nur in der Oberfläche, sondern an jeder geschützten API erzwungen. Artikelbearbeitung wird mit kurzlebigen Sperren koordiniert; Sitzungen, fehlgeschlagene Logins, Audit-Ereignisse und Rate Limits sind nachvollziehbar.
## Rollen
- **Administrator:** vollständiger Zugriff einschließlich Benutzer, Backups, Updates und Sicherheit.
- **Manager:** operative Verwaltung von Artikeln, Medien und Publishing, ohne Benutzer- oder Systemadministration.
- **Editor:** Artikel, Bilder, AI, FLUX, Bildproduktion und Qualitätsprüfung.
- **Publisher:** Artikel lesen, Qualität prüfen und Veröffentlichungen ausführen.
- **Leser:** reiner Lesezugriff.
## Sicherer Netzwerkbetrieb
Standardmäßig bindet Vendoo ausschließlich an `127.0.0.1`. Einen privaten LAN-Zugriff aktiviert der Installationsassistent nur nach bewusster Auswahl und setzt die erforderlichen Werte automatisch. Für öffentlichen Zugriff verwendet der VPS-Assistent den getrennten Caddy-Stack mit HTTPS; Port 8124 wird dabei nicht direkt veröffentlicht.
## Bestehende Funktionen
Alle Funktionen aus 1.30.0 bleiben enthalten: Operations Center, Publishing Hardening, Quality Center, Batch Image Factory, FLUX Studio Pro, Advanced Image Editor, Galerie und Browser-Extensions.
## Betriebszentrale (1.30.0)
Unter **Admin → System** stehen jetzt drei zusammenhängende Bereiche bereit:
- **Backup & Restore** mit SQLite-Snapshot, Datei-Prüfsummen, Integritätsprüfung, Download, Rotation und Wiederherstellungs-Staging
- **Update Center** mit Versionsprüfung, Paketvalidierung, Migrationsstatus, Sicherheitsbackup und bewusstem Offline-Anwenden über `setup.bat`
- **Extension-Diagnose** mit Paketversionen, Heartbeats, zuletzt verbundenen Browsern und Live-/Offline-Status
Update und Wiederherstellung ersetzen niemals während des laufenden Servers Dateien. Nach erfolgreicher Prüfung wird eine Operation vorbereitet und anschließend bei beendetem Vendoo über Setup-Menüpunkt 11 angewendet. Menüpunkt 12 stellt den zuvor gesicherten Programm- und Datenstand wieder her.
## Sicherheitsprinzipien
- Keine direkte Kopie einer geöffneten SQLite-Datei; Backups verwenden die SQLite-Backup-API.
- Jedes Backup enthält ein Manifest mit SHA-256-Prüfsummen und wird sofort geprüft.
- Eine Wiederherstellung erzeugt vorab automatisch ein vollständiges Sicherheitsbackup.
- Update-ZIPs mit `.env`, Datenbank, Uploads oder `node_modules` werden abgelehnt.
- Zugangsdaten aus `.env` werden nur nach ausdrücklicher Auswahl in manuelle Backups aufgenommen.
- Automatische Backups enthalten niemals `.env`.
- Update und Restore besitzen einen lokalen Rollback-Snapshot.
## Publishing Hardening (1.29.0)
- persistente Publishing-Queue für API- und Extension-Aufträge
- Schutz vor Doppelveröffentlichungen
- automatische Wiederholungen mit wachsendem Abstand
- Abbruch, manueller Retry und sichere Protokolllöschung
- Fehlerzentrale mit Audit-Ereignissen
- externe IDs, URLs und eBay Offer-IDs
- eBay Statusabgleich, Aktualisierung und Beenden über offizielle API
- Etsy Statusabgleich über offizielle API
- Vinted/Kleinanzeigen als bestätigungspflichtiger Extension-Workflow
- Wiederaufnahme laufender API-Aufträge nach Vendoo-Neustart
- geplante Veröffentlichungen werden an dieselbe gehärtete Queue übergeben
Vendoo 1.28.0 ergänzt die produktive **Batch Image Factory**. Mehrere Bilder aus der zentralen Galerie können in einem persistenten Auftrag über ein oder mehrere Produktionsprofile für Marktplätze, Webshops und Social Media verarbeitet werden. Ergebnisse bleiben nicht destruktiv, erscheinen wieder in der Galerie und lassen sich gesammelt als ZIP exportieren.
## Batch Image Factory (1.28.0)
- Hauptmenü **Bildproduktion** mit Quellenwahl aus der zentralen Bildergalerie
- Acht integrierte Ausgabeprofile sowie eigene speicherbare Profile
- Mehrfachprofile pro Auftrag und bis zu 200 Quellbilder
- Hintergrundentfernung, Sensitivität und Wasserzeichen als globale Auftragsoptionen
- persistente Job- und Positionshistorie mit Pause, Fortsetzen, Abbruch und Retry
- Wiederaufnahme nach Vendoo-Neustart
- nicht destruktive Ergebnisse unter `uploads/batch/`
- automatische Galerieintegration und ZIP-Export pro Auftrag
## Neu in 1.12.0
Der mobile QR-Upload bietet nun zwei eindeutige Wege: **Kamera öffnen** und **Mediathek auswählen**. Dadurch erzwingt das Smartphone nicht mehr ausschließlich die Kamera. Mehrere vorhandene Bilder können direkt gewählt werden; HEIC/HEIF werden nach Möglichkeit serverseitig zu JPG konvertiert.
OpenRouter wurde zusätzlich gehärtet: Vendoo erkennt Free-Bildmodelle präziser, prüft Bildinput-Fähigkeit, versucht mehrere kompatible Request-Formen und wechselt bei Fehlern automatisch zum nächsten Modell. Sind aktuell keine kostenlosen Bildmodelle vorhanden, zeigt die Diagnose das ausdrücklich an.
## Neu in 1.11.0
Der QR-Handy-Upload erzeugt jetzt automatisch Links mit der erkannten LAN-IP des Vendoo-PCs. Unter **Einstellungen → Vendoo LAN-Adresse für QR-Upload** kann die Adresse erkannt, geprüft und dauerhaft gespeichert werden. Die mobile Navigation wurde für Smartphone und Tablet komplett stabilisiert. Bei OpenRouter-Bildgenerierung versucht Vendoo automatisch mehrere passende Free-Modelle nacheinander und nutzt im Auto-Modus anschließend OpenAI als Fallback.
## Neu in 1.10.0
Vendoo unterstützt jetzt eine **AI-Job-Queue** für Bildgenerierung. Kleidung kann mit **1 bis 3** Varianten als **Model**, **Ghost Mannequin** oder **Flatlay** generiert werden. Im Listing-Editor gibt es zusätzlich eine **Historie früherer AI-Generierungen**, damit erzeugte Bilder schnell wiederverwendet werden können.
## Neu in 1.9.0
Die Einstellungen wurden um ein eigenes Steuerzentrum für **AI-Model-Fotos** erweitert. Dort lassen sich Provider, Free-Filter, Safety-Blocker, Standard-Preset, Variantenanzahl und das OpenAI-Fallback-Modell pflegen. Zusätzlich kann die UI die aktuell verfügbaren OpenRouter-Bildmodelle direkt laden und anzeigen.
## Neu in 1.8.0
Vendoo kann jetzt neben OpenAI auch OpenRouter für AI-Model-Fotos verwenden. Wenn `ai_model_photo_provider=auto` gesetzt ist und `OPENROUTER_API_KEY` vorhanden ist, fragt Vendoo automatisch aktuelle Bildmodelle von OpenRouter ab, filtert auf Bildausgabe und sofern aktiviert nur kostenlose Varianten. Gibt es kein passendes freies Modell mehr, fällt Vendoo sauber auf OpenAI zurück.
### Neue relevante Settings
- `ai_model_photo_provider`: `auto`, `openrouter`, `openai`
- `ai_model_photo_openrouter_free_only`: `1` oder `0`
- `ai_model_photo_openrouter_model`: optional feste OpenRouter-Model-ID
- `ai_model_photo_openai_model`: OpenAI-Fallback-Modell, Default `gpt-image-1`
### Neue ENV-Variablen
- `OPENROUTER_API_KEY`
- optional `OPENROUTER_SITE_URL`
- optional `OPENROUTER_APP_NAME`
## Neu in 1.7.0
Vendoo kann jetzt für geeignete Kleidungsstücke sichere AI-Model-Fotos erzeugen. Der Workflow prüft zuerst, ob ein Kleidungsartikel ohne Person vorliegt, blockiert sensible Kategorien und erstellt danach 13 zusätzliche Lifestyle-/Studio-Bilder mit fiktiven erwachsenen Personen oder alternative Ghost-Mannequin-/Flatlay-Varianten. Die Bilder lassen sich direkt in Generator und Listing-Editor übernehmen.
## Neu in 1.26.0
- Hauptnavigation mit **Neuer Artikel** direkt unter dem Dashboard und **Artikel** statt Listings
- repariertes Publish Center mit zuverlässigem Smart-Copy-Popup
- zoombare Medienvorschau mit Fit, 100 %, Mausrad und Verschieben
- kompakte lokale SVG-Iconbibliothek für Galerieaktionen
- aktualisierte Verkäufergebühren für eBay, Vinted, Kleinanzeigen und Etsy
- eigenes Extension Center für Chrome, Edge, Firefox und Safari
# Vendoo
Lokaler Multi-Plattform Listing-Generator mit AI, Lagerverwaltung und Publishing-System.
## Aktueller Entwicklungsstand
- Version: `1.39.0`
- UI-Slice: `UI 2026 / Slice 07 Cross-Browser Extensions, Responsive UI & Mobile Upload`
- Startseite: `Studio Flow`
- Tech Stack: Node.js, Express (ESM), Vanilla JS, SQLite (`better-sqlite3`)
Die vollständige Funktionsbeschreibung steht in [`FEATURES.md`](FEATURES.md). Die freigegebene Designrichtung liegt unter [`MockupDesign/`](MockupDesign/). Der vollständige Listing-Editor ist in [`docs/FULL_LISTING_EDITOR_2026.md`](docs/FULL_LISTING_EDITOR_2026.md) dokumentiert.
## Lokaler Start
```bash
npm install
npm start
```
Danach ist Vendoo standardmäßig unter `http://localhost:8124` erreichbar.
## Wichtige Sicherheitsregel
Nicht versionieren oder weitergeben:
- `.env`
- `db/vendoo.db*`
- `uploads/`
- Backups und Logs
- API-Schlüssel, OAuth-Tokens und SMTP-Passwörter
Die bereitgestellte Projektversion enthält ausschließlich `.env.example` und keine produktiven Laufzeitdaten.
## Browser-Erweiterungen
Die aktuellen Erweiterungen liegen getrennt unter `extensions/chrome`, `extensions/edge`, `extensions/firefox` und `extensions/safari`. Über den Vendoo-Menüpunkt **Extensions** wird der aktive Browser erkannt, der Installationsordner geöffnet und der Pfad kopiert. Safari benötigt auf macOS zusätzlich Xcode und die mitgelieferte WebExtension-Konvertierung.
## Mobiler Foto-Upload
Im Generator und im Listing-Editor kann ein zeitlich begrenzter QR-Code erzeugt werden. Nach dem Scan können Fotos direkt vom Smartphone aufgenommen und ohne Neuladen an Vendoo übertragen werden. Für andere Geräte muss Vendoo über eine erreichbare LAN-IP oder `PUBLIC_BASE_URL` geöffnet sein; `localhost` funktioniert nur auf demselben Gerät.
## Browser-Erweiterung 1.4.0
Die Erweiterungen für Chrome, Edge und Firefox verwenden jetzt eine kompakte, aktionsorientierte Oberfläche. Die primäre Aktion **Nur ausfüllen** trägt ein Listing in das geöffnete Vinted-Formular ein, ohne einen Entwurf zu speichern und ohne eine Veröffentlichung auszulösen. Abschlussaktionen sind davon deutlich getrennt.
## Aktueller UI-Stand
Slice 10 ergänzt das interaktive Dashboard, den aufgeräumten Studio Flow, robuste Publish-Initialisierung, ZIP-Fotodownloads und erweiterte Responsive-Gates.
## Branding
Logo, App-Icons, mobile Größen und Favicons liegen unter `public/brand/`. Die Browser-Erweiterungen für Chrome, Edge und Firefox verwenden dieselbe Icon-Familie.
## Local FLUX / ComfyUI
Vendoo can now prefer a local image backend for AI model photos. The intended order is: **Local FLUX / ComfyUI -> OpenRouter Free -> OpenAI**.
### What was added
- Settings for local URL, token, workflow path and install path
- Backend-only access to the local image server
- PowerShell bootstrap via `setup.ps1` or Settings UI
- Token regeneration for restricted local access
### Default assumptions
- ComfyUI is reachable at `http://127.0.0.1:8188`
- Workflow file: `local-ai/workflows/vendoo_flux_schnell_api.json`
- Vendoo talks to the local server from the backend, not directly from the browser
> Note: the included workflow template is a bootstrap starter and may need adjustment to the exact ComfyUI / FLUX node setup on the target machine.
## Vollautomatische lokale FLUX-Installation
Vendoo kann ComfyUI Portable und optional FLUX.1-schnell FP8 direkt aus der Einstellungsseite oder über `setup.bat` installieren.
### Über die Benutzeroberfläche
1. **Einstellungen → AI-Model-Fotos** öffnen.
2. Als Provider `Auto` oder `Nur lokales FLUX / ComfyUI` auswählen.
3. Installationsoptionen wählen.
4. **Lokales FLUX installieren** anklicken.
5. Den Fortschritt direkt in Vendoo verfolgen.
### Über setup.bat
- `7` lokales FLUX / ComfyUI installieren oder aktualisieren
- `8` Status anzeigen
- `9` lokalen Server starten
- `10` lokalen Server stoppen
### Sicherheitsmodell
ComfyUI wird ausschließlich an `127.0.0.1:8188` gebunden. Der Browser greift nicht direkt auf ComfyUI zu; nur das Vendoo-Backend sendet Bildjobs. Standardmäßig werden Remote-URLs abgelehnt. Ein zufälliger lokaler Token wird zusätzlich in der `.env` gespeichert. Das schützt den Dienst vor Zugriff aus dem LAN, ersetzt aber keine vollständige Prozessisolation gegenüber anderer Software auf demselben Windows-PC.
### Große Downloads
Der Modell-Download ist optional und wird vor dem Start ausdrücklich bestätigt. Bereits vorhandene ComfyUI- oder Modelldateien werden nicht ohne `Force` überschrieben. Downloads und Installationsfortschritt werden protokolliert.
## FLUX installer status and recovery
The local FLUX installer now records a detailed, resumable state in `local-ai/install-state.json`. The settings page displays individual checks for 7-Zip, the ComfyUI archive, embedded Python, ComfyUI, the FLUX model, workflow and runtime scripts. Failed installations can be started again without blindly discarding complete downloads.
The PowerShell 5.1 path bug that could turn `C:\Program Files\7-Zip\7z.exe` into the command `C` has been fixed.
## FLUX installer status hotfix (1.14.2)
The local FLUX installer now supervises 7-Zip instead of waiting without feedback. During extraction Vendoo shows files, extracted MB, elapsed time, heartbeat and the current 7-Zip log. A stale installer is marked as **stalled** and can be resumed without downloading the complete ComfyUI archive again.
## FLUX Studio ab 1.21.0
Vendoo enthält einen eigenen Hauptmenüpunkt **FLUX Studio**. Dort werden Bilder ausschließlich aus freien Prompts über das lokale ComfyUI/FLUX erzeugt. Einstellbar sind Stil, Ausgabeformat, Seed und Inferenzschritte. Ergebnisse werden in einer lokalen Bibliothek gespeichert, können heruntergeladen, in den Listing-Generator übernommen oder mit dem integrierten Editor nachbearbeitet werden.
Der frühere FASHN-/Virtual-Try-on-Stack wurde vollständig aufgegeben. Es werden kein Python-, Torch-, ONNX- oder separater VTO-Dienst mehr installiert. Beim ersten Start des aktualisierten Setups werden alte VTO-Runtime, Autostarts und Einstellungen entfernt; eigene frühere Modelbilder werden zur Sicherheit nach `uploads/ai-generated/legacy-models` verschoben.
### Update
1. Vendoo schließen.
2. Paketinhalt über die bestehende Installation kopieren und Dateien ersetzen.
3. `setup.bat` starten und **Reparieren** ausführen.
4. Vendoo starten und den Menüpunkt **FLUX Studio** öffnen.
## FLUX Studio Pro ab 1.22.0
Slice 30 erweitert das reine Prompt-Studio zu einem persistenten Job-Center. Aufträge können 1, 2 oder 4 Varianten mit eigenen Seeds erzeugen. Seed Lock, Wiederholen, „Ähnlich erneut erzeugen“, Favoriten, Queue-/History-Anbindung, System-/GPU-Daten und Wiederaufnahme nach Vendoo-Neustart sind integriert.
Die Fortschrittsanzeige verwendet im FLUX-Studio-Pro-Pfad ausschließlich reale Phasen und verstrichene Laufzeit. Es werden keine Prozentwerte aus einer angenommenen Renderdauer errechnet.
### Update auf 1.22.0
1. Vendoo und ComfyUI-Fenster schließen.
2. Den Inhalt des Paket-Unterordners `vendoo` über die bestehende Installation kopieren und Dateien ersetzen.
3. Den bestehenden Zielordner und Laufzeitdaten nicht löschen.
4. `setup.bat` starten und **3 Update** ausführen.
5. Bei Bedarf **9 Lokales FLUX / ComfyUI starten**, danach **8 Status** prüfen.
6. Vendoo starten und FLUX Studio öffnen.
## Artikel Quality Center
Vendoo 1.28.0 prüft Artikel vor dem Publishing auf Blocker, Vollständigkeit, Bildqualität, mögliche Duplikate und Plattformtauglichkeit. AI-Verbesserungen werden nur als Vorschlag angezeigt und erst nach ausdrücklicher Bestätigung gespeichert.
## Installationsziel wählen (1.36.0)
`setup.bat` führt auf Windows durch lokale Node-Installation oder Docker Desktop. Für Linux, NAS, Portainer und VPS steht `setup.sh` bereit. Beide Wege verwenden dieselbe `.env`, dieselben Compose-Dateien und dieselben persistenten Datenbereiche.
Privates, proprietäres Projekt. Alle Rechte vorbehalten.
+142 -25
View File
@@ -1,34 +1,151 @@
# Sicherheitsrichtlinie
## Source-Root- und Archivgrenzen ab 1.41.1
Vendoo ist derzeit ein privates Projekt. Sicherheitsprobleme dürfen nicht als öffentliche GitHub-Issues mit Zugangsdaten, Tokens oder personenbezogenen Daten gemeldet werden.
Historische Übergabe- und Featurepakete unter `Archiv/`, `archive/` oder `archives/` sind keine ausführbare Produktquelle. Sie werden aus Git-Staging, Release-Staging, Secret-Scan und ESM-Vertragsprüfung ausgeschlossen. Der produktive Source-Root bleibt separat vollständig geprüft; fehlende reale Moduldateien blockieren CI und Release.
## Niemals committen
# Sicherheit
- `.env` und produktive Konfigurationsdateien
- API-Schlüssel und OAuth-Tokens
- SMTP-Zugangsdaten
- Session-Secrets
- SQLite-Datenbanken einschließlich WAL- und SHM-Dateien
- Uploads, Backups und Logs
- private Zertifikate und Schlüssel
## Vertrauliche Dateien
## Mindestanforderungen für Online-Betrieb
Folgende Dateien dürfen niemals in Git oder öffentliche Pakete gelangen:
- kein direktes Port-Forwarding auf Vendoo oder die NAS-Verwaltung
- HTTPS über vorgeschalteten Reverse Proxy oder sicheren Tunnel
- sichere Cookies (`HttpOnly`, `Secure`, `SameSite`)
- korrekt begrenztes Express `trust proxy`
- CSRF-Schutz für alle schreibenden Requests
- Rollen- und Organisationsprüfung auf jeder geschützten Ressource
- Rate-Limits für Login, Magic Links und sensible APIs
- nicht privilegierter Container ohne Docker-Socket
- persistente Daten ausschließlich in expliziten Vendoo-Volumes
- automatische, geprüfte Backups
- `.env`
- `db/*.db`, `db/*.db-wal`, `db/*.db-shm`
- `uploads/`
- Backups, Logs und Session-Daten
- private Schlüssel und Zertifikate
## Geheimnisse
## Online-Betrieb
Produktive Geheimnisse werden über Server-Secrets beziehungsweise `_FILE`-Umgebungsvariablen bereitgestellt und nicht in der Datenbank oder im Repository im Klartext abgelegt.
Vendoo darf nicht durch eine direkte Portfreigabe von Port 8124 veröffentlicht werden. Für öffentliche Nutzer ist eine getrennte Server-/VPS-Instanz hinter HTTPS und Reverse Proxy vorgesehen.
## Abhängigkeiten
## Meldung von Sicherheitsproblemen
Vor Releases werden Abhängigkeiten, Container-Basisimages und bekannte Sicherheitslücken geprüft. Sicherheitsupdates erhalten Vorrang vor neuen Features.
Sicherheitsprobleme nicht mit Zugangsdaten oder personenbezogenen Daten in öffentliche Issues schreiben.
## Lokaler FLUX / ComfyUI
- ComfyUI wird standardmäßig nur an `127.0.0.1` gebunden.
- Vendoo blockiert Remote-ComfyUI-URLs, solange `LOCAL_IMAGE_ALLOW_REMOTE` nicht ausdrücklich aktiviert wird.
- Die ComfyUI-Oberfläche wird nicht über das LAN veröffentlicht.
- API-Aufrufe erfolgen serverseitig durch Vendoo.
- Ein zufälliger Token wird für die lokale Integrationskonfiguration erzeugt.
- Andere Prozesse auf demselben Windows-Rechner können theoretisch ebenfalls auf einen localhost-Dienst zugreifen; für echte Prozessisolation wäre ein separates Windows-Konto oder eine VM erforderlich.
## ESM-Modulverträge ab 1.41.0
- lokale benannte ESM-Imports werden vor CI und Release gegen die tatsächlich exportierten Symbole geprüft
- fehlende Exporte blockieren den Build vor der Auslieferung
- Worker-Start- und Stop-Verträge werden separat geprüft
## FLUX Studio
- ComfyUI bleibt standardmäßig ausschließlich an `127.0.0.1` gebunden.
- Das FLUX Studio akzeptiert nur Textprompts und erzeugt Dateien unter `uploads/ai-generated`.
- API-Pfade für Bibliothek und Bearbeitungen werden serverseitig auf dieses Verzeichnis begrenzt.
- Start und Stopp der lokalen Engine erfordern eine Admin-Rolle.
- Bearbeitungen erzeugen neue Dateien; Originalausgaben werden nicht still überschrieben.
## Operations Center, Backups und Updates
- Backups werden mit einem Manifest und SHA-256-Prüfsummen erzeugt und vor der Freigabe verifiziert.
- Sicherungen, die `.env` enthalten, enthalten möglicherweise API-Schlüssel, Tokens und Zugangsdaten. Sie sind wie Passwörter zu behandeln und dürfen nicht unverschlüsselt weitergegeben werden.
- Restore- und Update-Archive werden vor der Anwendung in einem lokalen Staging-Bereich geprüft.
- Archive mit Pfad-Traversal, absoluten Pfaden oder unzulässigen Laufzeitdaten werden abgewiesen.
- Updates dürfen keine `.env`, produktive Datenbank, Uploads, Backups, Logs, Modelle oder `node_modules` mitbringen.
- Das eigentliche Anwenden erfolgt nur bei gestopptem Vendoo über `setup.bat` und erzeugt vorher einen Rollback-Snapshot.
- Eine optionale Update-Manifest-URL sollte ausschließlich über HTTPS und aus einer kontrollierten Quelle verwendet werden.
- Extension-Heartbeats enthalten nur Diagnosemetadaten wie Browser, Version, Plattform und Zeitpunkt; keine Formularinhalte oder Zugangsdaten.
## Mehrbenutzer- und Server-Sicherheit ab 1.31.0
- Vendoo bindet standardmäßig an `127.0.0.1`. LAN-Zugriff muss ausdrücklich über `VENDOO_BIND_HOST=0.0.0.0` aktiviert werden.
- Öffentlicher Betrieb ausschließlich über HTTPS und einen korrekt konfigurierten Reverse Proxy. `VENDOO_TRUST_PROXY=true` nur setzen, wenn der Proxy vertrauenswürdig ist und Client-IP-Header kontrolliert.
- `VENDOO_ALLOWED_HOSTS` und `VENDOO_ALLOWED_ORIGINS` eng begrenzen.
- Rollen ersetzen keine Betriebssystemrechte: Datenbank, `.env`, Backups und Uploads müssen auf Dateisystemebene geschützt bleiben.
- Bearbeitungssperren verhindern parallele Änderungen, ersetzen aber keine Backups.
- Audit-Logs können personenbezogene Betriebsdaten wie Benutzer, IP und Aktionen enthalten; Aufbewahrung und Zugriff müssen organisatorisch geregelt werden.
## Architektur-Sicherheitsregeln ab 1.35.0
- Neue Kernel-Routen sind Deny-by-default und benötigen eine registrierte Policy.
- Modulmanifeste, Abhängigkeiten, Capabilities und Ownership werden vor dem Start validiert.
- Zyklische Abhängigkeiten, fehlende Capabilities und doppelte Ressourcen-Ownership blockieren den Kernelstart.
- Drittanbieter-Erweiterungen dürfen nicht im Vendoo-Hauptprozess ausgeführt werden.
- Themes dürfen nur freigegebene, typisierte Design Tokens verändern; freies CSS, JavaScript und externe URLs sind verboten.
- Der Event Bus ordnet Listener einem Owner zu, damit Module beim Stoppen ihre Listener vollständig entfernen können.
- Das Architektur-Gate `npm run verify:architecture` ist verpflichtender Bestandteil von CI und Releases.
## Theme-Sicherheit ab 1.36.0
- Theme-Profile akzeptieren ausschließlich bekannte, typisierte Design Tokens.
- Freies CSS, JavaScript, HTML, URLs und Dateipfade sind nicht zulässig.
- Alle Werte werden im Browser zur Benutzerführung und erneut auf dem Server validiert.
- Normale Benutzer können nur die eigene Theme-Präferenz verändern.
- Theme-Profile und Systemstandard erfordern `settings.manage`.
- Theme-Änderungen werden auditiert.
- Importierte Profile durchlaufen dieselbe Validierung wie manuell erstellte Profile.
- Kontrastprüfungen verhindern nicht automatisch jede bewusste administrative Gestaltung, zeigen problematische Paare aber vor dem Speichern deutlich an.
## Security Foundation ab 1.37.0
Zugangsdaten werden bevorzugt im verschlüsselten Secret-Speicher unter dem persistenten Konfigurationsbereich abgelegt. Die Verschlüsselung verwendet AES-256-GCM; der Master-Key liegt getrennt und wird nicht in Git oder Release-Pakete aufgenommen. Bestehende Secrets aus der Runtime-Konfiguration werden beim ersten Start gesichert, verschlüsselt migriert und aus der Klartextdatei entfernt.
Die Weboberfläche verwendet eine erzwungene Content Security Policy ohne Inline-Skripte und Inline-Eventhandler. Request- und Correlation-IDs werden in Antworten ausgegeben und in der strukturierten Telemetrie geführt.
## Identity- und Settings-Sicherheit ab 1.38.0
- Auth, Benutzer, Sitzungen und Einstellungen laufen als native Module über explizite Policies und Eingabeschemata.
- Der letzte aktive Administrator kann nicht gelöscht, deaktiviert oder zu einer Nicht-Admin-Rolle herabgestuft werden.
- Benutzer können nur Sitzungen widerrufen, die ihrer eigenen Benutzer-ID gehören; administrative globale Revocation erfordert `sessions.manage`.
- Login-Historie erfordert `users.manage`, SMTP-Verwaltung `settings.manage`.
- Die Settings-Allowlist weist unbekannte oder nicht freigegebene Felder zurück.
- Passwort-Hashes, Session-Tokens und Secret-Klarwerte dürfen nicht in Listen-, Status- oder Audit-Antworten erscheinen.
- `lib/auth.mjs` ist nur noch eine Kompatibilitätsfassade; neue Identitätslogik gehört in die nativen Module.
## Catalog- und Medien-Sicherheit ab 1.39.0
- Artikelrouten verwenden explizite Policies für Lesen, Bearbeiten, Papierkorb und endgültiges Löschen.
- Unbekannte Artikel-, Lager- und Medienfelder werden abgewiesen.
- Rich-HTML wird serverseitig bereinigt; Preise, Texte, Tags und Fotolisten besitzen feste Grenzen.
- Endgültiges Löschen ist nur für bereits im Papierkorb befindliche Artikel möglich.
- Lager-Bulk-Aktionen sind auf 500 eindeutige positive Artikel-IDs begrenzt.
- Medienpfade werden normalisiert und gegen Traversal, Nullbytes sowie Zeilenumbrüche geprüft.
- Von aktiven Artikeln referenzierte Bilder sind beim Löschen geschützt.
- Modulimporte initialisieren keine Datenbank; Repositories werden beim Serverstart injiziert.
## Modulsicherheit ab 1.40.0
- Kernmodule sind geschützt und können weder deaktiviert noch gelöscht werden.
- Aktivierungszustände werden atomar im persistenten Konfigurationsbereich gespeichert.
- `.vmod`-Pakete werden auf Format, Manifest, Pfade, Dateigrößen, Einzeldatei-SHA-256 und Gesamtintegrität geprüft.
- Installierbare Fremdmodule müssen den Typ `extension-host` verwenden.
- Deklarative Pakete dürfen keine JavaScript-, Shell-, Native- oder Binärdateien enthalten.
- Ausführbare Pakete benötigen eine vertrauenswürdige Ed25519-Signatur und dürfen ausschließlich in einem isolierten Extension-Host laufen. Solange dieser Host nicht freigegeben ist, bleiben solche Pakete deaktiviert und quarantänisiert.
- Fremdmodule werden niemals durch bloßes Kopieren automatisch registriert oder aktiviert.
- Abhängigkeiten werden vor Aktivierung geprüft; geschützte Abhängigkeiten verhindern unsichere Deaktivierung.
- Installierte Modulpakete und `modules-state.json` werden gemeinsam gesichert und wiederhergestellt.
- Der Laufzeitordner `modules/` ist von Git-Import und Release-Paketen ausgeschlossen.
## Produktionsdeployments über Coolify (1.41.0)
- der Vendoo-Container erhält keinen Zugriff auf `/var/run/docker.sock`
- Vendoo verändert seinen eigenen Container nicht und führt kein `git pull` aus
- Coolify-API-Token und Deploy-Webhook liegen ausschließlich im verschlüsselten Secret Store
- Deploymentrouten benötigen `updates.manage`, CSRF-Schutz, Rate Limit und Audit
- ein Deployment erfordert die exakte Bestätigung `DEPLOY`
- standardmäßig wird vor jedem Auftrag ein Backup einschließlich Konfiguration erzeugt
- Produktionsendpunkte müssen HTTPS verwenden
- 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.
+13
View File
@@ -0,0 +1,13 @@
# Drittanbieter-Hinweise für lokale AI-Engines
## FLUX / ComfyUI
Vendoo kann eine lokale ComfyUI-Installation mit einem FLUX-kompatiblen Modell ansteuern. ComfyUI, Modelle, Custom Nodes und deren Gewichte werden nicht als Vendoo-eigene Software ausgegeben. Für jede installierte Komponente gelten die jeweiligen Lizenz- und Nutzungsbedingungen der Herausgeber.
Vendoo liefert keine FASHN-VTON-, Python-, Torch-, ONNX- oder Human-Parser-Komponenten mehr aus und installiert diese auch nicht mehr.
## Lucide Icons
Die lokale Vendoo-Iconbibliothek verwendet eine kompakte Auswahl von Lucide-Icon-Geometrien.
Lucide steht unter der ISC-Lizenz. Projekt: https://lucide.dev/
+51
View File
@@ -0,0 +1,51 @@
@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%
+38
View File
@@ -0,0 +1,38 @@
{
"schemaVersion": 2,
"architecture": "modular-monolith",
"rules": {
"modulePrefix": "vendoo.",
"denyImplicitRoutes": true,
"denyCrossModuleDatabaseAccess": true,
"denyCrossModuleInternalImports": true,
"thirdPartyExtensionsInMainProcess": false,
"themeCustomCss": false,
"themeTokensOnly": true,
"denyUnknownSettings": true,
"sessionOwnershipRequired": true,
"lastActiveAdminProtected": true,
"catalogPayloadAllowlist": true,
"mediaReferenceProtection": true,
"inventoryBulkLimit": 500
},
"nativeModules": [
"vendoo.auth",
"vendoo.users",
"vendoo.sessions",
"vendoo.settings",
"vendoo.security",
"vendoo.themes",
"vendoo.media",
"vendoo.listings",
"vendoo.inventory"
],
"migration": {
"mode": "strangler",
"legacyBridgeStatus": "legacy-bridge",
"frameworkMigration": false,
"databaseReplacement": false,
"identityCompatibilityFacade": "lib/auth.mjs",
"catalogCompatibilityLayer": "lib/db.mjs"
}
}
+94
View File
@@ -0,0 +1,94 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://vendoo.local/contracts/module.schema.json",
"title": "Vendoo Module Manifest",
"type": "object",
"additionalProperties": false,
"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"
}
},
"$defs": {
"moduleIds": {
"type": "array",
"uniqueItems": true,
"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-]*)+$"
}
}
}
}
+418
View File
@@ -0,0 +1,418 @@
import { db } from '../../../lib/db.mjs';
import { createHash, randomBytes, scryptSync, timingSafeEqual } from 'crypto';
// --- Schema ---
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL DEFAULT '',
password_hash TEXT,
role TEXT NOT NULL DEFAULT 'editor',
active INTEGER NOT NULL DEFAULT 1,
avatar_color TEXT,
last_active_at TEXT,
created_at TEXT DEFAULT (datetime('now')),
invited_by INTEGER,
FOREIGN KEY (invited_by) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
token_hash TEXT UNIQUE NOT NULL,
ip TEXT,
user_agent TEXT,
expires_at TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS auth_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
token_hash TEXT UNIQUE NOT NULL,
type TEXT NOT NULL DEFAULT 'login',
role TEXT DEFAULT 'editor',
used INTEGER NOT NULL DEFAULT 0,
expires_at TEXT NOT NULL,
created_by INTEGER,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (created_by) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
user_email TEXT,
action TEXT NOT NULL,
target_type TEXT,
target_id TEXT,
details TEXT,
ip TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS login_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
ip TEXT,
user_agent TEXT,
success INTEGER NOT NULL DEFAULT 1,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
`);
// Migration: add password_hash column if missing
try { db.exec("ALTER TABLE users ADD COLUMN password_hash TEXT"); } catch {}
try { db.exec("ALTER TABLE users ADD COLUMN failed_login_count INTEGER NOT NULL DEFAULT 0"); } catch {}
try { db.exec("ALTER TABLE users ADD COLUMN locked_until TEXT"); } catch {}
try { db.exec("ALTER TABLE users ADD COLUMN password_changed_at TEXT"); } catch {}
try { db.exec("ALTER TABLE sessions ADD COLUMN last_seen_at TEXT"); } catch {}
try { db.exec("ALTER TABLE sessions ADD COLUMN revoked_at TEXT"); } catch {}
try { db.exec("UPDATE sessions SET last_seen_at = COALESCE(last_seen_at, created_at)"); } catch {}
const AVATAR_COLORS = [
'#4a7c59', '#2d6a9f', '#8b5a3c', '#6b4c8a', '#c4713b',
'#3a8c7a', '#7c4d6e', '#5a7c3a', '#8c5a5a', '#3a6b8c',
];
const SESSION_DURATION_MS = Math.max(1, Number(process.env.VENDOO_SESSION_DAYS || 3)) * 24 * 60 * 60 * 1000;
const SESSION_IDLE_MS = Math.max(10, Number(process.env.VENDOO_SESSION_IDLE_MINUTES || 120)) * 60 * 1000;
const SESSION_TOUCH_INTERVAL_MS = 60 * 1000;
const LOGIN_TOKEN_EXPIRY_MIN = 15;
const INVITE_TOKEN_EXPIRY_HOURS = 48;
function hashToken(token) {
return createHash('sha256').update(token).digest('hex');
}
function generateToken() {
return randomBytes(32).toString('hex');
}
function randomAvatarColor() {
return AVATAR_COLORS[Math.floor(Math.random() * AVATAR_COLORS.length)];
}
// --- Password Hashing (scrypt) ---
export function hashPassword(password) {
const salt = randomBytes(16).toString('hex');
const hash = scryptSync(password, salt, 64).toString('hex');
return `${salt}:${hash}`;
}
export function verifyPassword(password, storedHash) {
if (!storedHash) return false;
const [salt, hash] = storedHash.split(':');
if (!salt || !hash) return false;
const hashBuf = Buffer.from(hash, 'hex');
const supplied = scryptSync(password, salt, 64);
return timingSafeEqual(hashBuf, supplied);
}
// --- Users ---
export function createUser(email, name, role = 'editor', invitedBy = null, password = null) {
role = ['admin', 'manager', 'editor', 'publisher', 'viewer'].includes(role) ? role : 'editor';
const existing = db.prepare('SELECT id FROM users WHERE email = ?').get(email);
if (existing) throw new Error('User existiert bereits');
const color = randomAvatarColor();
const pwHash = password ? hashPassword(password) : null;
const r = db.prepare('INSERT INTO users (email, name, role, avatar_color, invited_by, password_hash) VALUES (?, ?, ?, ?, ?, ?)').run(email, name, role, color, invitedBy, pwHash);
return getUser(r.lastInsertRowid);
}
export function setPassword(userId, password) {
const pwHash = hashPassword(password);
db.prepare("UPDATE users SET password_hash = ?, password_changed_at = datetime('now'), failed_login_count = 0, locked_until = NULL WHERE id = ?").run(pwHash, userId);
destroyUserSessions(userId);
}
export function validatePasswordStrength(password) {
const value = String(password || '');
const problems = [];
if (value.length < 10) problems.push('mindestens 10 Zeichen');
if (!/[a-z]/.test(value)) problems.push('einen Kleinbuchstaben');
if (!/[A-Z]/.test(value)) problems.push('einen Großbuchstaben');
if (!/\d/.test(value)) problems.push('eine Zahl');
if (!/[^A-Za-z0-9]/.test(value)) problems.push('ein Sonderzeichen');
return { valid: problems.length === 0, problems };
}
export function registerFailedLogin(userId, maxAttempts = 8, lockMinutes = 15) {
if (!userId) return null;
const user = getUser(userId);
if (!user) return null;
const count = Number(user.failed_login_count || 0) + 1;
if (count >= maxAttempts) {
const lockedUntil = new Date(Date.now() + lockMinutes * 60 * 1000).toISOString();
db.prepare('UPDATE users SET failed_login_count = 0, locked_until = ? WHERE id = ?').run(lockedUntil, userId);
destroyUserSessions(userId);
return { locked: true, lockedUntil };
}
db.prepare('UPDATE users SET failed_login_count = ? WHERE id = ?').run(count, userId);
return { locked: false, remaining: Math.max(0, maxAttempts - count) };
}
export function clearFailedLogins(userId) {
if (!userId) return;
db.prepare('UPDATE users SET failed_login_count = 0, locked_until = NULL WHERE id = ?').run(userId);
}
export function isUserLocked(user) {
if (!user?.locked_until) return false;
const until = Date.parse(user.locked_until);
if (!Number.isFinite(until) || until <= Date.now()) {
clearFailedLogins(user.id);
return false;
}
return true;
}
export function getUser(id) {
return db.prepare('SELECT * FROM users WHERE id = ?').get(id);
}
export function getUserByEmail(email) {
return db.prepare('SELECT * FROM users WHERE email = ? COLLATE NOCASE').get(email);
}
export function getUsers() {
return db.prepare("SELECT id, email, name, role, active, avatar_color, last_active_at, created_at, locked_until, failed_login_count, CASE WHEN password_hash IS NOT NULL AND password_hash != '' THEN 1 ELSE 0 END as has_password FROM users ORDER BY created_at").all();
}
export function updateUser(id, data) {
const allowed = ['name', 'role', 'active', 'avatar_color'];
const fields = [], values = [];
for (const [k, rawValue] of Object.entries(data)) {
if (!allowed.includes(k) || rawValue === undefined) continue;
const v = k === 'role' ? (['admin', 'manager', 'editor', 'publisher', 'viewer'].includes(rawValue) ? rawValue : 'viewer') : rawValue;
fields.push(`${k} = ?`); values.push(v);
}
if (!fields.length) return getUsers().find(user => Number(user.id) === Number(id)) || null;
values.push(id);
db.prepare(`UPDATE users SET ${fields.join(', ')} WHERE id = ?`).run(...values);
return getUsers().find(user => Number(user.id) === Number(id)) || null;
}
export function deleteUser(id) {
db.prepare('DELETE FROM sessions WHERE user_id = ?').run(id);
db.prepare('DELETE FROM users WHERE id = ?').run(id);
}
export function touchUserActivity(userId) {
db.prepare("UPDATE users SET last_active_at = datetime('now') WHERE id = ?").run(userId);
}
export function getUserCount() {
return db.prepare('SELECT COUNT(*) as count FROM users').get().count;
}
// --- Sessions ---
export function createSession(userId, ip, userAgent) {
const token = generateToken();
const hash = hashToken(token);
const now = new Date();
const expiresAt = new Date(now.getTime() + SESSION_DURATION_MS).toISOString();
const lastSeenAt = now.toISOString();
db.prepare('INSERT INTO sessions (user_id, token_hash, ip, user_agent, expires_at, last_seen_at, revoked_at) VALUES (?, ?, ?, ?, ?, ?, NULL)').run(userId, hash, ip, userAgent || '', expiresAt, lastSeenAt);
return { token, expiresAt };
}
export function validateSession(token) {
if (!token) return null;
const hash = hashToken(token);
const session = db.prepare("SELECT s.*, u.email, u.name, u.role, u.active, u.avatar_color, u.locked_until FROM sessions s JOIN users u ON s.user_id = u.id WHERE s.token_hash = ? AND s.revoked_at IS NULL AND s.expires_at > datetime('now')").get(hash);
if (!session || !session.active || isUserLocked(session)) return null;
const lastSeen = Date.parse(session.last_seen_at || session.created_at || 0);
if (Number.isFinite(lastSeen) && Date.now() - lastSeen > SESSION_IDLE_MS) {
db.prepare("UPDATE sessions SET revoked_at = datetime('now') WHERE id = ?").run(session.id);
return null;
}
if (!Number.isFinite(lastSeen) || Date.now() - lastSeen >= SESSION_TOUCH_INTERVAL_MS) {
const now = new Date().toISOString();
db.prepare('UPDATE sessions SET last_seen_at = ? WHERE id = ?').run(now, session.id);
session.last_seen_at = now;
touchUserActivity(session.user_id);
}
return session;
}
export function destroySession(token) {
const hash = hashToken(token);
db.prepare("UPDATE sessions SET revoked_at = datetime('now') WHERE token_hash = ?").run(hash);
}
export function destroySessionById(id) {
db.prepare("UPDATE sessions SET revoked_at = datetime('now') WHERE id = ?").run(id);
}
export function getSessionById(id) {
return db.prepare("SELECT s.id, s.user_id, s.ip, s.user_agent, s.created_at, s.last_seen_at, s.expires_at, s.revoked_at, u.email, u.name, u.role FROM sessions s JOIN users u ON s.user_id = u.id WHERE s.id = ?").get(id);
}
export function destroyOwnedSessionById(userId, id) {
const result = db.prepare("UPDATE sessions SET revoked_at = datetime('now') WHERE id = ? AND user_id = ? AND revoked_at IS NULL").run(id, userId);
return Number(result.changes || 0);
}
export function destroyUserSessions(userId) {
db.prepare("UPDATE sessions SET revoked_at = datetime('now') WHERE user_id = ? AND revoked_at IS NULL").run(userId);
}
export function getUserSessions(userId) {
return db.prepare("SELECT id, ip, user_agent, created_at, last_seen_at, expires_at FROM sessions WHERE user_id = ? AND revoked_at IS NULL AND expires_at > datetime('now') ORDER BY created_at DESC").all(userId);
}
export function getAllActiveSessions() {
return db.prepare("SELECT s.id, s.user_id, s.ip, s.user_agent, s.created_at, s.last_seen_at, s.expires_at, u.email, u.name, u.role FROM sessions s JOIN users u ON s.user_id = u.id WHERE s.revoked_at IS NULL AND s.expires_at > datetime('now') ORDER BY s.created_at DESC").all();
}
// clean expired sessions periodically
export function cleanExpiredSessions() {
db.prepare("DELETE FROM sessions WHERE expires_at <= datetime('now') OR (revoked_at IS NOT NULL AND revoked_at <= datetime('now', '-7 days'))").run();
}
// --- Auth Tokens (Magic Links) ---
export function createAuthToken(email, type = 'login', role = 'editor', createdBy = null) {
const token = generateToken();
const hash = hashToken(token);
const minutes = type === 'invite' ? INVITE_TOKEN_EXPIRY_HOURS * 60 : LOGIN_TOKEN_EXPIRY_MIN;
const expiresAt = new Date(Date.now() + minutes * 60 * 1000).toISOString();
// invalidate previous unused tokens of same type for same email
db.prepare("UPDATE auth_tokens SET used = 1 WHERE email = ? COLLATE NOCASE AND type = ? AND used = 0").run(email, type);
db.prepare('INSERT INTO auth_tokens (email, token_hash, type, role, expires_at, created_by) VALUES (?, ?, ?, ?, ?, ?)').run(email, hash, type, role, expiresAt, createdBy);
return { token, expiresAt };
}
export function validateAuthToken(token) {
const hash = hashToken(token);
const row = db.prepare("SELECT * FROM auth_tokens WHERE token_hash = ? AND used = 0 AND expires_at > datetime('now')").get(hash);
if (!row) return null;
db.prepare('UPDATE auth_tokens SET used = 1 WHERE id = ?').run(row.id);
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();
export function checkRateLimit(key, maxAttempts = 5, windowMs = 15 * 60 * 1000) {
const now = Date.now();
const entry = rateLimits.get(key);
if (!entry) {
rateLimits.set(key, { count: 1, firstAttempt: now });
return { allowed: true, remaining: maxAttempts - 1 };
}
if (now - entry.firstAttempt > windowMs) {
rateLimits.set(key, { count: 1, firstAttempt: now });
return { allowed: true, remaining: maxAttempts - 1 };
}
entry.count++;
if (entry.count > maxAttempts) {
const retryAfter = Math.ceil((entry.firstAttempt + windowMs - now) / 1000);
return { allowed: false, remaining: 0, retryAfter };
}
return { allowed: true, remaining: maxAttempts - entry.count };
}
// --- CSRF ---
export function generateCsrfToken() {
return randomBytes(24).toString('hex');
}
// --- Audit Log ---
export function auditLog(userId, userEmail, action, targetType = null, targetId = null, details = null, ip = null) {
db.prepare('INSERT INTO audit_log (user_id, user_email, action, target_type, target_id, details, ip) VALUES (?, ?, ?, ?, ?, ?, ?)').run(userId, userEmail, action, targetType, targetId, details, ip);
}
export function getAuditLog(limit = 100, offset = 0) {
return db.prepare('SELECT * FROM audit_log ORDER BY created_at DESC LIMIT ? OFFSET ?').all(limit, offset);
}
export function searchAuditLog({ limit = 100, offset = 0, userId = null, action = '', from = '', to = '', query = '' } = {}) {
const clauses = [];
const values = [];
if (userId) { clauses.push('user_id = ?'); values.push(Number(userId)); }
if (action) { clauses.push('action LIKE ?'); values.push(`%${String(action).slice(0, 80)}%`); }
if (from) { clauses.push('created_at >= ?'); values.push(String(from).slice(0, 30)); }
if (to) { clauses.push('created_at <= ?'); values.push(String(to).slice(0, 30)); }
if (query) {
const q = `%${String(query).slice(0, 120)}%`;
clauses.push('(user_email LIKE ? OR action LIKE ? OR target_type LIKE ? OR target_id LIKE ? OR details LIKE ? OR ip LIKE ?)');
values.push(q, q, q, q, q, q);
}
const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
const safeLimit = Math.max(1, Math.min(1000, Number(limit) || 100));
const safeOffset = Math.max(0, Number(offset) || 0);
const rows = db.prepare(`SELECT * FROM audit_log ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...values, safeLimit, safeOffset);
const total = db.prepare(`SELECT COUNT(*) AS count FROM audit_log ${where}`).get(...values)?.count || 0;
return { rows, total, limit: safeLimit, offset: safeOffset };
}
export function getAuditLogForUser(userId, limit = 50) {
return db.prepare('SELECT * FROM audit_log WHERE user_id = ? ORDER BY created_at DESC LIMIT ?').all(userId, limit);
}
// --- Login History ---
export function recordLogin(userId, ip, userAgent, success = true) {
db.prepare('INSERT INTO login_history (user_id, ip, user_agent, success) VALUES (?, ?, ?, ?)').run(userId, ip, userAgent || '', success ? 1 : 0);
}
export function getLoginHistory(userId, limit = 20) {
return db.prepare('SELECT * FROM login_history WHERE user_id = ? ORDER BY created_at DESC LIMIT ?').all(userId, limit);
}
export function getRecentFailedLogins(email, windowMinutes = 15) {
const user = getUserByEmail(email);
if (!user) return 0;
const r = db.prepare("SELECT COUNT(*) as count FROM login_history WHERE user_id = ? AND success = 0 AND created_at > datetime('now', ?)").get(user.id, `-${windowMinutes} minutes`);
return r.count;
}
// --- Setup: ensure at least one admin exists ---
export function ensureAdminExists() {
const count = getUserCount();
return count === 0;
}
export function isSetupMode() {
return getUserCount() === 0;
}
// Clean expired sessions every 10 minutes
setInterval(cleanExpiredSessions, 10 * 60 * 1000);
@@ -0,0 +1,173 @@
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
@@ -0,0 +1,112 @@
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
@@ -0,0 +1,87 @@
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));
}
}
+51
View File
@@ -0,0 +1,51 @@
export const SECRET_DEFINITIONS = Object.freeze({
ANTHROPIC_API_KEY: { label: 'Anthropic API-Key', category: 'AI', editable: true },
OPENAI_API_KEY: { label: 'OpenAI API-Key', category: 'AI', editable: true },
OPENROUTER_API_KEY: { label: 'OpenRouter API-Key', category: 'AI', editable: true },
ETSY_API_KEY: { label: 'Etsy API-Key', category: 'Marktplatz', editable: true },
ETSY_ACCESS_TOKEN: { label: 'Etsy Access Token', category: 'Marktplatz', editable: false },
ETSY_REFRESH_TOKEN: { label: 'Etsy Refresh Token', category: 'Marktplatz', editable: false },
EBAY_CLIENT_SECRET: { label: 'eBay Client Secret', category: 'Marktplatz', editable: true },
EBAY_ACCESS_TOKEN: { label: 'eBay Access Token', category: 'Marktplatz', editable: false },
EBAY_REFRESH_TOKEN: { label: 'eBay Refresh Token', category: 'Marktplatz', editable: false },
LOCAL_IMAGE_TOKEN: { label: 'FLUX Dienst-Token', category: 'FLUX', editable: true },
SMTP_PASS: { label: 'SMTP-Passwort', category: 'E-Mail', editable: true },
VENDOO_SETUP_TOKEN: { label: 'Ersteinrichtungs-Token', category: 'System', editable: false },
COOLIFY_API_TOKEN: { label: 'Coolify API-Token', category: 'Deployment', editable: true },
COOLIFY_DEPLOY_WEBHOOK_URL: { label: 'Coolify Deploy-Webhook', category: 'Deployment', editable: true },
});
export const SECRET_NAMES = Object.freeze(Object.keys(SECRET_DEFINITIONS));
const SECRET_NAME = /^[A-Z][A-Z0-9_]{2,80}$/;
export function validateSecretName(name) {
const normalized = String(name || '').trim();
if (!SECRET_NAME.test(normalized) || !SECRET_DEFINITIONS[normalized]) {
const error = new Error(`Unbekanntes Secret: ${normalized || '(leer)'}`);
error.code = 'SECRET_NAME_INVALID';
throw error;
}
return normalized;
}
export function validateSecretValue(value) {
const normalized = String(value ?? '');
if (/\r|\n|\0/.test(normalized)) {
const error = new Error('Secrets dürfen keine Zeilenumbrüche oder Nullbytes enthalten.');
error.code = 'SECRET_VALUE_INVALID';
throw error;
}
if (Buffer.byteLength(normalized, 'utf8') > 16_384) {
const error = new Error('Secret ist zu groß.');
error.code = 'SECRET_VALUE_TOO_LARGE';
throw error;
}
return normalized;
}
export function maskSecret(value) {
const text = String(value || '');
if (!text) return '';
if (text.length <= 4) return '••••';
return `••••••••${text.slice(-4)}`;
}
+169
View File
@@ -0,0 +1,169 @@
import { readFileSync } from 'fs';
import { join } from 'path';
import { APP_ROOT } from '../../../lib/runtime-paths.mjs';
import { PlatformError } from '../../kernel/errors.mjs';
const source = JSON.parse(readFileSync(join(APP_ROOT, 'public', 'design-system', 'tokens', 'source.json'), 'utf8'));
const BUILTIN_THEME_IDS = new Set([...source.themes, 'system', 'inherit']);
const HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
const RGB_COLOR = /^rgba?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)(?:\s*,\s*(0|1|0?\.\d+))?\s*\)$/i;
const DIMENSION = /^-?\d+(?:\.\d+)?px$/i;
const THEME_ID = /^[a-z][a-z0-9-]{2,39}$/;
function expandHex(value) {
const hex = value.replace('#', '');
if (hex.length === 3 || hex.length === 4) return hex.split('').map(char => char + char).join('');
return hex;
}
function parseColor(value) {
const raw = String(value || '').trim();
if (HEX_COLOR.test(raw)) {
const hex = expandHex(raw);
return [0, 2, 4].map(index => Number.parseInt(hex.slice(index, index + 2), 16));
}
const match = raw.match(RGB_COLOR);
if (!match) return null;
const channels = match.slice(1, 4).map(Number);
return channels.every(channel => Number.isFinite(channel) && channel >= 0 && channel <= 255) ? channels : null;
}
function validColor(value) {
if (!parseColor(value)) return false;
const match = String(value).trim().match(RGB_COLOR);
if (!match) return true;
const alpha = match[4] === undefined ? 1 : Number(match[4]);
return Number.isFinite(alpha) && alpha >= 0 && alpha <= 1;
}
function validateValue(token, value) {
const raw = String(value ?? '').trim();
if (token.type === 'color') return validColor(raw);
if (token.type === 'dimension') {
if (!DIMENSION.test(raw)) return false;
const numeric = Number.parseFloat(raw);
if (token.min !== undefined && numeric < Number(token.min)) return false;
if (token.max !== undefined && numeric > Number(token.max)) return false;
return true;
}
return false;
}
function relativeLuminance(value) {
const rgb = parseColor(value);
if (!rgb) return null;
const linear = rgb.map(channel => {
const normalized = channel / 255;
return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4;
});
return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
}
export function contrastRatio(foreground, background) {
const first = relativeLuminance(foreground);
const second = relativeLuminance(background);
if (first === null || second === null) return null;
const lighter = Math.max(first, second);
const darker = Math.min(first, second);
return (lighter + 0.05) / (darker + 0.05);
}
export function getBaseThemeValues(themeId = 'light') {
const base = source.themes.includes(themeId) ? themeId : 'light';
return Object.fromEntries(Object.entries(source.tokens).map(([name, token]) => [name, token[base]]));
}
export function resolveThemeValues({ baseTheme = 'light', values = {} } = {}) {
return Object.freeze({ ...getBaseThemeValues(baseTheme), ...values });
}
export function evaluateThemeAccessibility({ baseTheme = 'light', values = {} } = {}) {
const resolved = resolveThemeValues({ baseTheme, values });
const checks = [
['primary-on-canvas', 'color.ink', 'color.canvas', 4.5],
['primary-on-surface', 'color.ink', 'color.surface', 4.5],
['secondary-on-surface', 'color.inkSecondary', 'color.surface', 4.5],
['muted-on-surface', 'color.inkMuted', 'color.surface', 3],
['brand-on-surface', 'color.brand', 'color.surface', 3],
['danger-on-surface', 'color.danger', 'color.surface', 3],
['line-on-surface', 'color.lineStrong', 'color.surface', 3],
].map(([id, foregroundToken, backgroundToken, minimum]) => {
const ratio = contrastRatio(resolved[foregroundToken], resolved[backgroundToken]);
return {
id,
foregroundToken,
backgroundToken,
ratio: ratio === null ? null : Number(ratio.toFixed(2)),
minimum,
ok: ratio !== null && ratio >= minimum,
};
});
return Object.freeze({
ok: checks.every(check => check.ok),
checks: Object.freeze(checks),
failures: Object.freeze(checks.filter(check => !check.ok)),
});
}
export function getThemeContract() {
return {
schemaVersion: source.schemaVersion,
contractVersion: source.contractVersion,
themes: [...source.themes],
tokens: Object.fromEntries(Object.entries(source.tokens).map(([name, token]) => [name, {
css: token.css,
type: token.type,
customizable: Boolean(token.customizable),
label: token.label || name,
group: token.group || name.split('.')[0],
...(token.min !== undefined ? { min: token.min } : {}),
...(token.max !== undefined ? { max: token.max } : {}),
}])),
};
}
export function getBuiltinThemes() {
return source.themes.map(id => ({
id,
name: ({ light: 'Hell', dark: 'Dunkel', contrast: 'Hoher Kontrast' })[id] || id,
base_theme: id,
builtin: true,
values: {},
resolved_values: getBaseThemeValues(id),
accessibility: evaluateThemeAccessibility({ baseTheme: id }),
}));
}
export function validateThemeDefinition(theme, { allowExistingId = false } = {}) {
if (!theme || typeof theme !== 'object' || Array.isArray(theme)) {
throw new PlatformError('Theme-Definition ist ungültig.', { code: 'THEME_INVALID', status: 400, expose: true });
}
const id = String(theme.id || '').trim().toLowerCase();
const name = String(theme.name || '').trim();
const baseTheme = String(theme.base_theme || theme.baseTheme || 'light').trim().toLowerCase();
if (!THEME_ID.test(id) || BUILTIN_THEME_IDS.has(id)) throw new PlatformError('Theme-ID ist ungültig oder reserviert.', { code: 'THEME_ID_INVALID', status: 400, expose: true });
if (!allowExistingId && !id.startsWith('custom-')) throw new PlatformError('Eigene Theme-IDs müssen mit custom- beginnen.', { code: 'THEME_ID_PREFIX', status: 400, expose: true });
if (!name || name.length > 80) throw new PlatformError('Theme-Name ist ungültig.', { code: 'THEME_NAME_INVALID', status: 400, expose: true });
if (!source.themes.includes(baseTheme)) throw new PlatformError('Basis-Theme ist ungültig.', { code: 'THEME_BASE_INVALID', status: 400, expose: true });
const values = {};
for (const [tokenName, value] of Object.entries(theme.values || {})) {
const token = source.tokens[tokenName];
if (!token || !token.customizable) throw new PlatformError(`Token ist nicht anpassbar: ${tokenName}`, { code: 'THEME_TOKEN_FORBIDDEN', status: 400, expose: true });
if (!validateValue(token, value)) throw new PlatformError(`Ungültiger Wert für ${tokenName}.`, { code: 'THEME_VALUE_INVALID', status: 400, expose: true });
values[tokenName] = String(value).trim();
}
const accessibility = evaluateThemeAccessibility({ baseTheme, values });
return Object.freeze({ schemaVersion: 1, id, name, base_theme: baseTheme, values: Object.freeze(values), accessibility });
}
export function validateThemePreference(input, { customThemeExists = () => false } = {}) {
const themeId = String(input?.theme_id || input?.themeId || 'inherit').trim().toLowerCase();
const density = String(input?.density || 'comfortable').trim().toLowerCase();
const reducedMotion = String(input?.reduced_motion || input?.reducedMotion || 'system').trim().toLowerCase();
if (![...BUILTIN_THEME_IDS].includes(themeId) && !customThemeExists(themeId)) {
throw new PlatformError('Gewähltes Theme existiert nicht.', { code: 'THEME_NOT_FOUND', status: 404, expose: true });
}
if (!['compact', 'comfortable', 'spacious'].includes(density)) throw new PlatformError('UI-Dichte ist ungültig.', { code: 'THEME_DENSITY_INVALID', status: 400, expose: true });
if (!['system', 'reduce', 'allow'].includes(reducedMotion)) throw new PlatformError('Bewegungseinstellung ist ungültig.', { code: 'THEME_MOTION_INVALID', status: 400, expose: true });
return Object.freeze({ theme_id: themeId, density, reduced_motion: reducedMotion });
}
+31
View File
@@ -0,0 +1,31 @@
export class PlatformError extends Error {
constructor(message, { code = 'PLATFORM_ERROR', status = 500, details = null, expose = false, cause = undefined } = {}) {
super(String(message || 'Unbekannter Plattformfehler'), { cause });
this.name = 'PlatformError';
this.code = String(code || 'PLATFORM_ERROR');
this.status = Number.isInteger(status) ? status : 500;
this.details = details;
this.expose = Boolean(expose || this.status < 500);
}
}
export function normalizePlatformError(error) {
if (error instanceof PlatformError) return error;
return new PlatformError(error?.message || 'Interner Serverfehler', {
code: error?.code || 'INTERNAL_ERROR',
status: Number.isInteger(error?.status) ? error.status : 500,
details: error?.details || null,
expose: Boolean(error?.expose),
cause: error,
});
}
export function platformErrorPayload(error, requestId = null) {
const normalized = normalizePlatformError(error);
return {
error: normalized.expose ? normalized.message : 'Interner Serverfehler',
code: normalized.code,
request_id: requestId || null,
...(normalized.expose && normalized.details ? { details: normalized.details } : {}),
};
}
+77
View File
@@ -0,0 +1,77 @@
import { PlatformError } from './errors.mjs';
const EVENT_PATTERN = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/;
export class EventBus {
#listeners = new Map();
#maxListeners;
constructor({ maxListenersPerEvent = 50 } = {}) {
this.#maxListeners = Math.max(1, Number(maxListenersPerEvent) || 50);
}
on(eventName, handler, { owner = 'core', once = false } = {}) {
if (!EVENT_PATTERN.test(String(eventName))) {
throw new PlatformError(`Ungültiger Eventname: ${eventName}`, { code: 'EVENT_NAME_INVALID', status: 500 });
}
if (typeof handler !== 'function') {
throw new PlatformError('Event-Handler muss eine Funktion sein.', { code: 'EVENT_HANDLER_INVALID', status: 500 });
}
const listeners = this.#listeners.get(eventName) || [];
if (listeners.length >= this.#maxListeners) {
throw new PlatformError(`Zu viele Listener für ${eventName}.`, { code: 'EVENT_LISTENER_LIMIT', status: 500 });
}
const entry = Object.freeze({ handler, owner: String(owner || 'core'), once: Boolean(once) });
listeners.push(entry);
this.#listeners.set(eventName, listeners);
return () => this.off(eventName, handler);
}
once(eventName, handler, options = {}) {
return this.on(eventName, handler, { ...options, once: true });
}
off(eventName, handler) {
const listeners = this.#listeners.get(eventName) || [];
const remaining = listeners.filter(entry => entry.handler !== handler);
if (remaining.length) this.#listeners.set(eventName, remaining);
else this.#listeners.delete(eventName);
return remaining.length !== listeners.length;
}
removeOwner(owner) {
let removed = 0;
for (const [eventName, listeners] of this.#listeners) {
const remaining = listeners.filter(entry => {
const match = entry.owner === owner;
if (match) removed += 1;
return !match;
});
if (remaining.length) this.#listeners.set(eventName, remaining);
else this.#listeners.delete(eventName);
}
return removed;
}
async emit(eventName, payload = null, context = {}) {
const listeners = [...(this.#listeners.get(eventName) || [])];
const results = [];
for (const entry of listeners) {
try {
results.push({ owner: entry.owner, ok: true, value: await entry.handler(payload, Object.freeze({ ...context, eventName })) });
} catch (error) {
results.push({ owner: entry.owner, ok: false, error });
} finally {
if (entry.once) this.off(eventName, entry.handler);
}
}
return results;
}
snapshot() {
return [...this.#listeners.entries()].map(([event, listeners]) => ({
event,
listeners: listeners.map(entry => ({ owner: entry.owner, once: entry.once })),
}));
}
}
+59
View File
@@ -0,0 +1,59 @@
import { PlatformError } from './errors.mjs';
const FLAG_PATTERN = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/;
function envKey(flag) {
return `VENDOO_FEATURE_${flag.replace(/[^a-z0-9]/gi, '_').toUpperCase()}`;
}
function parseBoolean(value, fallback) {
if (value === undefined || value === null || value === '') return fallback;
if (/^(1|true|yes|on)$/i.test(String(value))) return true;
if (/^(0|false|no|off)$/i.test(String(value))) return false;
throw new PlatformError(`Ungültiger Feature-Flag-Wert: ${value}`, { code: 'FEATURE_FLAG_VALUE_INVALID', status: 500 });
}
export class FeatureFlagRegistry {
#flags = new Map();
define(name, { defaultValue = false, owner = 'vendoo.system', description = '', mutable = false } = {}) {
const normalized = String(name || '');
if (!FLAG_PATTERN.test(normalized)) {
throw new PlatformError(`Ungültiger Feature-Flag: ${normalized}`, { code: 'FEATURE_FLAG_NAME_INVALID', status: 500 });
}
if (this.#flags.has(normalized)) {
throw new PlatformError(`Feature-Flag bereits definiert: ${normalized}`, { code: 'FEATURE_FLAG_DUPLICATE', status: 500 });
}
const configured = parseBoolean(process.env[envKey(normalized)], Boolean(defaultValue));
this.#flags.set(normalized, {
name: normalized,
value: configured,
defaultValue: Boolean(defaultValue),
owner: String(owner),
description: String(description),
mutable: Boolean(mutable),
source: process.env[envKey(normalized)] === undefined ? 'default' : 'environment',
});
return configured;
}
isEnabled(name) {
if (!this.#flags.has(name)) {
throw new PlatformError(`Unbekannter Feature-Flag: ${name}`, { code: 'FEATURE_FLAG_UNKNOWN', status: 500 });
}
return this.#flags.get(name).value;
}
set(name, value) {
const flag = this.#flags.get(name);
if (!flag) throw new PlatformError(`Unbekannter Feature-Flag: ${name}`, { code: 'FEATURE_FLAG_UNKNOWN', status: 404, expose: true });
if (!flag.mutable) throw new PlatformError(`Feature-Flag ist nicht zur Laufzeit änderbar: ${name}`, { code: 'FEATURE_FLAG_IMMUTABLE', status: 409, expose: true });
flag.value = Boolean(value);
flag.source = 'runtime';
return flag.value;
}
list() {
return [...this.#flags.values()].map(flag => ({ ...flag }));
}
}
+43
View File
@@ -0,0 +1,43 @@
import { PlatformError } from './errors.mjs';
function fail(message, field) {
throw new PlatformError(message, { code: 'INPUT_VALIDATION_FAILED', status: 400, expose: true, details: { field } });
}
export function objectSchema(fields, { allowUnknown = false } = {}) {
const contract = Object.freeze({ ...fields });
return input => {
const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {};
const output = {};
if (!allowUnknown) for (const key of Object.keys(source)) if (!contract[key]) fail(`Unbekanntes Eingabefeld: ${key}`, key);
for (const [name, rule] of Object.entries(contract)) {
let value = source[name];
if (value === undefined || value === null) {
if (rule.required) fail(`${name} ist erforderlich.`, name);
continue;
}
if (rule.type === 'string') {
value = String(value);
if (rule.trim !== false) value = value.trim();
if (rule.minLength && value.length < rule.minLength) fail(`${name} ist zu kurz.`, name);
if (rule.maxLength && value.length > rule.maxLength) fail(`${name} ist zu lang.`, name);
if (rule.pattern && !rule.pattern.test(value)) fail(`${name} besitzt ein ungültiges Format.`, name);
} else if (rule.type === 'boolean') {
if (typeof value !== 'boolean') fail(`${name} muss true oder false sein.`, name);
} else if (rule.type === 'number' || rule.type === 'integer') {
value = Number(value);
if (!Number.isFinite(value)) fail(`${name} muss eine Zahl sein.`, name);
if (rule.type === 'integer' && !Number.isInteger(value)) fail(`${name} muss eine ganze Zahl sein.`, name);
if (rule.min !== undefined && value < rule.min) fail(`${name} ist zu klein.`, name);
if (rule.max !== undefined && value > rule.max) fail(`${name} ist zu groß.`, name);
}
if (rule.enum && !rule.enum.includes(value)) fail(`${name} enthält einen nicht erlaubten Wert.`, name);
if (typeof rule.validate === 'function') {
const result = rule.validate(value);
if (result !== true) fail(typeof result === 'string' ? result : `${name} ist ungültig.`, name);
}
output[name] = value;
}
return Object.freeze(output);
};
}
+49
View File
@@ -0,0 +1,49 @@
import { PlatformError } from './errors.mjs';
export class LifecycleManager {
#state = 'created';
#hooks = [];
register({ id, order = 100, start = async () => {}, stop = async () => {} }) {
if (!id || this.#hooks.some(hook => hook.id === id)) {
throw new PlatformError(`Ungültiger oder doppelter Lifecycle-Hook: ${id}`, { code: 'LIFECYCLE_HOOK_INVALID' });
}
this.#hooks.push({ id: String(id), order: Number(order) || 100, start, stop, state: 'registered' });
}
async start(context) {
if (!['created', 'stopped'].includes(this.#state)) throw new PlatformError(`Kernel kann aus Zustand ${this.#state} nicht starten.`, { code: 'LIFECYCLE_STATE_INVALID' });
this.#state = 'starting';
const started = [];
try {
for (const hook of [...this.#hooks].sort((a, b) => a.order - b.order)) {
hook.state = 'starting';
await hook.start(context);
hook.state = 'running';
started.push(hook);
}
this.#state = 'running';
} catch (error) {
for (const hook of started.reverse()) {
try { await hook.stop({ ...context, rollback: true }); } catch {}
hook.state = 'stopped';
}
this.#state = 'failed';
throw error;
}
}
async stop(context) {
if (!['running', 'starting'].includes(this.#state)) return;
this.#state = 'stopping';
for (const hook of [...this.#hooks].sort((a, b) => b.order - a.order)) {
if (hook.state !== 'running') continue;
try { await hook.stop(context); } finally { hook.state = 'stopped'; }
}
this.#state = 'stopped';
}
snapshot() {
return { state: this.#state, hooks: this.#hooks.map(({ start: _s, stop: _t, ...hook }) => ({ ...hook })) };
}
}
+48
View File
@@ -0,0 +1,48 @@
import { PlatformError } from './errors.mjs';
const MODULE_ID = /^vendoo\.[a-z][a-z0-9-]*$/;
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
const CAPABILITY = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/;
const TYPES = new Set(['core', 'feature', 'adapter', 'extension-host']);
const STATUSES = new Set(['native', 'legacy-bridge', 'disabled']);
function uniqueStrings(values, label, pattern = CAPABILITY) {
if (!Array.isArray(values)) throw new PlatformError(`${label} muss ein Array sein.`, { code: 'MODULE_MANIFEST_INVALID' });
const normalized = values.map(value => String(value || '').trim());
if (normalized.some(value => !pattern.test(value))) {
throw new PlatformError(`${label} enthält einen ungültigen Eintrag.`, { code: 'MODULE_MANIFEST_INVALID' });
}
if (new Set(normalized).size !== normalized.length) {
throw new PlatformError(`${label} enthält Duplikate.`, { code: 'MODULE_MANIFEST_INVALID' });
}
return normalized;
}
export function validateModuleManifest(manifest) {
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
throw new PlatformError('Modulmanifest fehlt oder ist ungültig.', { code: 'MODULE_MANIFEST_INVALID' });
}
const normalized = {
schemaVersion: Number(manifest.schemaVersion || 1),
id: String(manifest.id || '').trim(),
name: String(manifest.name || '').trim(),
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')),
provides: Object.freeze(uniqueStrings(manifest.provides || [], 'provides')),
consumes: Object.freeze(uniqueStrings(manifest.consumes || [], 'consumes')),
owns: Object.freeze(uniqueStrings(manifest.owns || [], 'owns')),
};
if (normalized.schemaVersion !== 1) throw new PlatformError('Nicht unterstützte Manifestversion.', { code: 'MODULE_MANIFEST_VERSION' });
if (!MODULE_ID.test(normalized.id)) throw new PlatformError(`Ungültige Modul-ID: ${normalized.id}`, { code: 'MODULE_ID_INVALID' });
if (!normalized.name) throw new PlatformError(`Modulname fehlt: ${normalized.id}`, { code: 'MODULE_NAME_REQUIRED' });
if (!SEMVER.test(normalized.version)) throw new PlatformError(`Ungültige Modulversion: ${normalized.id}`, { code: 'MODULE_VERSION_INVALID' });
if (!TYPES.has(normalized.type)) throw new PlatformError(`Ungültiger Modultyp: ${normalized.id}`, { code: 'MODULE_TYPE_INVALID' });
if (!STATUSES.has(normalized.status)) throw new PlatformError(`Ungültiger Modulstatus: ${normalized.id}`, { code: 'MODULE_STATUS_INVALID' });
if (normalized.requires.includes(normalized.id)) throw new PlatformError(`Modul darf sich nicht selbst benötigen: ${normalized.id}`, { code: 'MODULE_SELF_DEPENDENCY' });
return Object.freeze(normalized);
}
+276
View File
@@ -0,0 +1,276 @@
import { PlatformError } from './errors.mjs';
import { validateModuleManifest } from './module-contract.mjs';
export class ModuleRegistry {
#modules = new Map();
#startOrder = [];
#ownership = new Map();
#started = false;
register({ manifest, lifecycle = {}, source = 'builtin' }, { enabled = true } = {}) {
const validated = validateModuleManifest(manifest);
if (this.#modules.has(validated.id)) {
throw new PlatformError(`Modul bereits registriert: ${validated.id}`, { code: 'MODULE_DUPLICATE' });
}
for (const resource of validated.owns) {
const owner = this.#ownership.get(resource);
if (owner) throw new PlatformError(`Ressource ${resource} wird bereits von ${owner} besessen.`, { code: 'MODULE_OWNERSHIP_CONFLICT' });
}
const active = validated.status !== 'disabled' && enabled !== false;
const record = {
manifest: validated,
lifecycle: {
start: typeof lifecycle.start === 'function' ? lifecycle.start : async () => {},
stop: typeof lifecycle.stop === 'function' ? lifecycle.stop : async () => {},
health: typeof lifecycle.health === 'function' ? lifecycle.health : async () => ({ ok: true }),
},
source: String(source || 'builtin'),
enabled: active,
state: active ? 'registered' : 'disabled',
startedAt: null,
error: null,
};
this.#modules.set(validated.id, record);
for (const resource of validated.owns) this.#ownership.set(resource, validated.id);
return validated;
}
get(id) {
return this.#modules.get(id) || null;
}
has(id) {
return this.#modules.has(id);
}
isEnabled(id) {
return this.#modules.get(id)?.enabled === true;
}
dependentsOf(id, { enabledOnly = false, recursive = true } = {}) {
const found = new Set();
const visit = dependency => {
for (const [candidateId, record] of this.#modules) {
if (enabledOnly && !record.enabled) continue;
if (!record.manifest.requires.includes(dependency) || found.has(candidateId)) continue;
found.add(candidateId);
if (recursive) visit(candidateId);
}
};
visit(id);
return [...found];
}
#resolveStartOrder() {
const visiting = new Set();
const visited = new Set();
const order = [];
const visit = id => {
if (visited.has(id)) return;
if (visiting.has(id)) throw new PlatformError(`Zyklische Modulabhängigkeit bei ${id}.`, { code: 'MODULE_DEPENDENCY_CYCLE' });
const record = this.#modules.get(id);
if (!record) throw new PlatformError(`Fehlende Modulabhängigkeit: ${id}`, { code: 'MODULE_DEPENDENCY_MISSING' });
if (!record.enabled) return;
visiting.add(id);
for (const dependency of record.manifest.requires) {
const dependencyRecord = this.#modules.get(dependency);
if (!dependencyRecord) throw new PlatformError(`Fehlende Modulabhängigkeit: ${dependency}`, { code: 'MODULE_DEPENDENCY_MISSING' });
if (!dependencyRecord.enabled) {
throw new PlatformError(`Modul ${id} benötigt das deaktivierte Modul ${dependency}.`, {
code: 'MODULE_DEPENDENCY_DISABLED', details: { module: id, dependency },
});
}
visit(dependency);
}
visiting.delete(id);
visited.add(id);
order.push(id);
};
for (const [id, record] of this.#modules) if (record.enabled) visit(id);
const capabilities = new Set([...this.#modules.values()].filter(record => record.enabled).flatMap(record => record.manifest.provides));
for (const record of this.#modules.values()) {
if (!record.enabled) continue;
for (const capability of record.manifest.consumes) {
if (!capabilities.has(capability)) throw new PlatformError(`Fehlende Capability ${capability} für ${record.manifest.id}.`, { code: 'MODULE_CAPABILITY_MISSING' });
}
}
return order;
}
validateGraph() {
const order = this.#resolveStartOrder();
return Object.freeze({
order: Object.freeze([...order]),
enabledModules: Object.freeze(order.map(id => this.#modules.get(id)?.manifest.id).filter(Boolean)),
capabilities: Object.freeze([...new Set([...this.#modules.values()]
.filter(record => record.enabled)
.flatMap(record => record.manifest.provides))].sort()),
});
}
async #startRecord(id, context) {
const record = this.#modules.get(id);
if (!record || !record.enabled || record.state === 'running') return;
for (const dependency of record.manifest.requires) {
const dependencyRecord = this.#modules.get(dependency);
if (!dependencyRecord?.enabled) {
throw new PlatformError(`Modul ${id} benötigt das deaktivierte Modul ${dependency}.`, {
code: 'MODULE_DEPENDENCY_DISABLED', status: 409, expose: true,
details: { module: id, dependency },
});
}
if (dependencyRecord.state !== 'running') await this.#startRecord(dependency, context);
}
try {
record.state = 'starting';
record.error = null;
await record.lifecycle.start(Object.freeze({ ...context, module: record.manifest }));
record.state = 'running';
record.startedAt = new Date().toISOString();
if (!this.#startOrder.includes(id)) this.#startOrder.push(id);
} catch (error) {
record.state = 'failed';
record.error = error?.message || String(error);
throw new PlatformError(`Modulstart fehlgeschlagen: ${id}`, { code: 'MODULE_START_FAILED', cause: error, details: { module: id } });
}
}
async startAll(context) {
this.#startOrder = this.#resolveStartOrder();
const started = [];
for (const id of this.#startOrder) {
const record = this.#modules.get(id);
if (!record.enabled) continue;
try {
await this.#startRecord(id, context);
started.push(id);
} catch (error) {
for (const startedId of started.reverse()) {
const startedRecord = this.#modules.get(startedId);
try { await startedRecord.lifecycle.stop(Object.freeze({ ...context, module: startedRecord.manifest, rollback: true })); } catch {}
startedRecord.state = 'stopped';
}
throw error;
}
}
this.#started = true;
}
async enable(id, context) {
const record = this.#modules.get(id);
if (!record) throw new PlatformError('Modul nicht gefunden.', { code: 'MODULE_NOT_FOUND', status: 404, expose: true });
if (record.enabled && record.state === 'running') return this.describe(id);
record.enabled = true;
record.state = 'registered';
try {
this.#resolveStartOrder();
if (this.#started) await this.#startRecord(id, context);
return this.describe(id);
} catch (error) {
record.enabled = false;
record.state = 'disabled';
throw error;
}
}
async disable(id, context, { cascade = false } = {}) {
const record = this.#modules.get(id);
if (!record) throw new PlatformError('Modul nicht gefunden.', { code: 'MODULE_NOT_FOUND', status: 404, expose: true });
if (!record.enabled) return { module: this.describe(id), disabled: [] };
const dependents = this.dependentsOf(id, { enabledOnly: true, recursive: true });
if (dependents.length && !cascade) {
throw new PlatformError('Das Modul wird von aktiven Modulen benötigt.', {
code: 'MODULE_HAS_ACTIVE_DEPENDENTS', status: 409, expose: true, details: { dependents },
});
}
const orderIndex = new Map(this.#startOrder.map((moduleId, index) => [moduleId, index]));
const toDisable = [...dependents, id].sort((a, b) => (orderIndex.get(b) ?? -1) - (orderIndex.get(a) ?? -1));
for (const moduleId of toDisable) {
const target = this.#modules.get(moduleId);
if (!target?.enabled) continue;
if (target.state === 'running') {
target.state = 'stopping';
try {
await target.lifecycle.stop(Object.freeze({ ...context, module: target.manifest, disabled: true }));
target.state = 'disabled';
} catch (error) {
target.state = 'failed';
target.error = error?.message || String(error);
throw new PlatformError(`Modul konnte nicht deaktiviert werden: ${moduleId}`, { code: 'MODULE_STOP_FAILED', status: 500, cause: error });
}
} else {
target.state = 'disabled';
}
target.enabled = false;
target.startedAt = null;
}
return { module: this.describe(id), disabled: toDisable };
}
async unregister(id, context) {
const record = this.#modules.get(id);
if (!record) return false;
if (record.enabled || record.state === 'running') await this.disable(id, context, { cascade: false });
const dependents = this.dependentsOf(id, { enabledOnly: false, recursive: false });
if (dependents.length) {
throw new PlatformError('Modul kann wegen vorhandener Abhängigkeiten nicht entfernt werden.', {
code: 'MODULE_HAS_DEPENDENTS', status: 409, expose: true, details: { dependents },
});
}
for (const resource of record.manifest.owns) if (this.#ownership.get(resource) === id) this.#ownership.delete(resource);
this.#modules.delete(id);
this.#startOrder = this.#startOrder.filter(moduleId => moduleId !== id);
return true;
}
async stopAll(context) {
for (const id of [...this.#startOrder].reverse()) {
const record = this.#modules.get(id);
if (!record || record.state !== 'running') continue;
try {
record.state = 'stopping';
await record.lifecycle.stop(Object.freeze({ ...context, module: record.manifest }));
record.state = 'stopped';
} catch (error) {
record.state = 'failed';
record.error = error?.message || String(error);
}
}
this.#started = false;
}
async health() {
const checks = [];
for (const [id, record] of this.#modules) {
if (!record.enabled || record.state === 'disabled') {
checks.push({ id, ok: true, state: 'disabled' });
continue;
}
try {
const result = await record.lifecycle.health({ module: record.manifest });
checks.push({ id, state: record.state, ok: record.state === 'running' && result?.ok !== false, ...(result || {}) });
} catch (error) {
checks.push({ id, state: record.state, ok: false, error: error?.message || String(error) });
}
}
return checks;
}
describe(id) {
const record = this.#modules.get(id);
if (!record) return null;
return {
...record.manifest,
source: record.source,
enabled: record.enabled,
state: record.state,
startedAt: record.startedAt,
error: record.error,
dependents: this.dependentsOf(id, { enabledOnly: false, recursive: false }),
};
}
snapshot() {
return [...this.#modules.keys()].map(id => this.describe(id));
}
}
+156
View File
@@ -0,0 +1,156 @@
import { EventBus } from './event-bus.mjs';
import { FeatureFlagRegistry } from './feature-flags.mjs';
import { LifecycleManager } from './lifecycle.mjs';
import { ModuleRegistry } from './module-registry.mjs';
import { PolicyEngine } from './policy-engine.mjs';
import { RouteRegistry } from './route-registry.mjs';
import { loadBuiltInModules } from '../modules/catalog.mjs';
import { ModuleStateStore } from '../core/modules/module-state-store.mjs';
import { ModulePackageStore } from '../core/modules/module-package-store.mjs';
import { createExternalLifecycle, PROTECTED_MODULE_IDS } from '../modules/module-manager/service.mjs';
import { MODULES_DIR, MODULE_STATE_PATH } from '../../lib/runtime-paths.mjs';
export function createPlatformKernel({ version, hasPermission, moduleTrustKeys = {} }) {
const events = new EventBus();
const features = new FeatureFlagRegistry();
const modules = new ModuleRegistry();
const moduleStateStore = new ModuleStateStore(MODULE_STATE_PATH);
const modulePackageStore = new ModulePackageStore(MODULES_DIR, { trustKeys: moduleTrustKeys });
const policies = new PolicyEngine();
const routes = new RouteRegistry({ policyEngine: policies, moduleRegistry: modules });
const lifecycle = new LifecycleManager();
policies.register('platform.authenticated', ({ user }) => Boolean(user), {
description: 'Erfordert eine gültige Vendoo-Sitzung.',
});
policies.register('platform.security-admin', ({ user }) => Boolean(user && hasPermission(user.role, 'security.manage')), {
description: 'Erfordert die Berechtigung security.manage.',
});
policies.register('security.secrets.manage', ({ user }) => Boolean(user && hasPermission(user.role, 'security.manage')), {
owner: 'vendoo.security', description: 'Erfordert security.manage für verschlüsselte Zugangsdaten.',
});
policies.register('auth.public', () => true, { owner: 'vendoo.auth', description: 'Öffentliche Authentifizierungsroute mit eigener Eingabe- und Rate-Limit-Prüfung.' });
policies.register('auth.authenticated', ({ user }) => Boolean(user), { owner: 'vendoo.auth', description: 'Erfordert eine gültige Vendoo-Identität.' });
policies.register('users.self', ({ user }) => Boolean(user), { owner: 'vendoo.users', description: 'Erlaubt Zugriff auf das eigene Benutzerprofil.' });
policies.register('users.manage', ({ user }) => Boolean(user && hasPermission(user.role, 'users.manage')), { owner: 'vendoo.users', description: 'Erfordert users.manage.' });
policies.register('sessions.self', ({ user }) => Boolean(user), { owner: 'vendoo.sessions', description: 'Erlaubt Zugriff auf eigene Sitzungen.' });
policies.register('sessions.manage', ({ user }) => Boolean(user && hasPermission(user.role, 'sessions.manage')), { owner: 'vendoo.sessions', description: 'Erfordert sessions.manage.' });
policies.register('settings.read', ({ user }) => Boolean(user), { owner: 'vendoo.settings', description: 'Erfordert eine gültige Sitzung zum Lesen der Anwendungseinstellungen.' });
policies.register('settings.manage', ({ user }) => Boolean(user && hasPermission(user.role, 'settings.manage')), { owner: 'vendoo.settings', description: 'Erfordert settings.manage.' });
policies.register('listings.view', ({ user }) => Boolean(user && hasPermission(user.role, 'listings.view')), { owner: 'vendoo.listings', description: 'Erfordert listings.view.' });
policies.register('listings.edit', ({ user }) => Boolean(user && hasPermission(user.role, 'listings.edit')), { owner: 'vendoo.listings', description: 'Erfordert listings.edit.' });
policies.register('listings.delete', ({ user }) => Boolean(user && hasPermission(user.role, 'listings.delete')), { owner: 'vendoo.listings', description: 'Erfordert listings.delete.' });
policies.register('listings.permanent-delete', ({ user }) => Boolean(user && hasPermission(user.role, 'listings.permanent_delete')), { owner: 'vendoo.listings', description: 'Erfordert listings.permanent_delete.' });
policies.register('inventory.view', ({ user }) => Boolean(user && hasPermission(user.role, 'listings.view')), { owner: 'vendoo.inventory', description: 'Erfordert listings.view für Lagerübersichten.' });
policies.register('inventory.manage', ({ user }) => Boolean(user && hasPermission(user.role, 'inventory.edit')), { owner: 'vendoo.inventory', description: 'Erfordert inventory.edit.' });
policies.register('media.view', ({ user }) => Boolean(user && hasPermission(user.role, 'media.view')), { owner: 'vendoo.media', description: 'Erfordert media.view.' });
policies.register('media.edit', ({ user }) => Boolean(user && hasPermission(user.role, 'media.edit')), { owner: 'vendoo.media', description: 'Erfordert media.edit.' });
policies.register('media.delete', ({ user }) => Boolean(user && hasPermission(user.role, 'media.delete')), { owner: 'vendoo.media', description: 'Erfordert media.delete.' });
policies.register('themes.use', ({ user }) => Boolean(user), {
owner: 'vendoo.themes', description: 'Erfordert eine gültige Sitzung für persönliche Darstellungseinstellungen.',
});
policies.register('themes.manage', ({ user }) => Boolean(user && hasPermission(user.role, 'settings.manage')), {
owner: 'vendoo.themes', description: 'Erfordert settings.manage für systemweite und eigene Theme-Profile.',
});
policies.register('modules.manage', ({ user }) => Boolean(user && hasPermission(user.role, 'modules.manage')), {
owner: 'vendoo.module-manager', description: 'Erfordert modules.manage für Installation, Aktivierung, Export und Entfernung von Modulen.',
});
policies.register('publishing.view', ({ user }) => Boolean(user && hasPermission(user.role, 'publishing.view')), { owner: 'vendoo.publishing', description: 'Erfordert publishing.view.' });
policies.register('publishing.execute', ({ user }) => Boolean(user && hasPermission(user.role, 'publishing.execute')), { owner: 'vendoo.publishing', description: 'Erfordert publishing.execute.' });
policies.register('publishing.manage', ({ user }) => Boolean(user && hasPermission(user.role, 'publishing.manage')), { owner: 'vendoo.publishing', description: 'Erfordert publishing.manage.' });
policies.register('ai.execute', ({ user }) => Boolean(user && hasPermission(user.role, 'generation.execute')), { owner: 'vendoo.ai', description: 'Erfordert generation.execute.' });
policies.register('flux.execute', ({ user }) => Boolean(user && hasPermission(user.role, 'generation.execute')), { owner: 'vendoo.flux-studio', description: 'Erfordert generation.execute.' });
policies.register('quality.view', ({ user }) => Boolean(user && hasPermission(user.role, 'listings.view')), { owner: 'vendoo.quality', description: 'Erfordert listings.view.' });
policies.register('quality.execute', ({ user }) => Boolean(user && hasPermission(user.role, 'quality.execute')), { owner: 'vendoo.quality', description: 'Erfordert quality.execute.' });
policies.register('updates.manage', ({ user }) => Boolean(user && hasPermission(user.role, 'updates.manage')), { owner: 'vendoo.deployments', description: 'Erfordert updates.manage für Coolify-Deployment und Web-Updates.' });
features.define('platform.module-kernel', { defaultValue: true, description: 'Aktiviert den modularen Plattformkernel.' });
features.define('platform.route-contracts', { defaultValue: true, description: 'Aktiviert registrierte Routenverträge.' });
features.define('security.encrypted-secret-store', { defaultValue: true, owner: 'vendoo.security', description: 'Aktiviert AES-256-GCM-verschlüsselte Zugangsdaten außerhalb der Runtime-Konfiguration.' });
features.define('security.request-telemetry', { defaultValue: true, owner: 'vendoo.security', description: 'Aktiviert Request-/Correlation-IDs und strukturierte Telemetrie.' });
features.define('security.strict-script-csp', { defaultValue: true, owner: 'vendoo.security', description: 'Blockiert Inline-Skripte und Inline-Eventhandler per CSP.' });
features.define('identity.native-auth', { defaultValue: true, owner: 'vendoo.auth', description: 'Aktiviert native Setup-, Login- und Einladungsabläufe.' });
features.define('identity.native-users', { defaultValue: true, owner: 'vendoo.users', description: 'Aktiviert native Benutzer- und Rollenverwaltung.' });
features.define('identity.native-sessions', { defaultValue: true, owner: 'vendoo.sessions', description: 'Aktiviert eigentümergebundene Sitzungsverwaltung und Revocation.' });
features.define('settings.native-module', { defaultValue: true, owner: 'vendoo.settings', description: 'Aktiviert allowlist-basierte Anwendungs- und SMTP-Einstellungen.' });
features.define('catalog.native-listings', { defaultValue: true, owner: 'vendoo.listings', description: 'Aktiviert native Artikel-, Duplikat- und Papierkorbverträge.' });
features.define('catalog.native-inventory', { defaultValue: true, owner: 'vendoo.inventory', description: 'Aktiviert native Lagerort- und Bulk-Bestandsverträge.' });
features.define('catalog.native-media', { defaultValue: true, owner: 'vendoo.media', description: 'Aktiviert native Medienbibliothek und Referenzschutz.' });
features.define('themes.token-runtime', { defaultValue: true, owner: 'vendoo.themes', description: 'Aktiviert den versionierten Design-Token-Vertrag.' });
features.define('themes.manager', { defaultValue: true, owner: 'vendoo.themes', description: 'Aktiviert den sicheren Theme Manager mit Benutzerpräferenzen.' });
features.define('themes.accessibility-gate', { defaultValue: true, owner: 'vendoo.themes', description: 'Prüft Theme-Kontraste und sichere Tokenwerte.' });
features.define('extensions.isolated-host', { defaultValue: false, owner: 'vendoo.security', description: 'Reserviert für einen späteren isolierten Extension-Host.' });
features.define('modules.manager', { defaultValue: true, owner: 'vendoo.module-manager', description: 'Aktiviert den sicheren Modulmanager und den persistenten Modulstatus.' });
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.' });
features.define('updates.web-trigger', { defaultValue: true, owner: 'vendoo.deployments', description: 'Erlaubt Administratoren, einen geprüften Coolify-Deployment-Auftrag aus dem Update Center anzufordern.' });
const initialDisabledModules = new Set(String(process.env.VENDOO_INITIAL_DISABLED_MODULES || '')
.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 enabled = forcedEnabled || moduleStateStore.isEnabled(module.manifest.id, defaultEnabled);
modules.register(module, { enabled });
}
for (const installed of modulePackageStore.list()) {
if (installed.invalid || modules.has(installed.id)) continue;
try {
const pkg = modulePackageStore.get(installed.id);
const enabled = installed.activatable && moduleStateStore.isEnabled(installed.id, false);
modules.register({ manifest: pkg.manifest, lifecycle: createExternalLifecycle(pkg), source: 'external' }, { enabled });
} catch {
// Ungültige oder konfliktbehaftete Fremdmodule bleiben quarantänisiert und erscheinen im Modulmanager.
}
}
lifecycle.register({
id: 'modules',
order: 20,
start: context => modules.startAll(context),
stop: context => modules.stopAll(context),
});
const context = Object.freeze({ version, events, features, policies, modules, moduleStateStore, modulePackageStore });
return Object.freeze({
version,
events,
features,
lifecycle,
modules,
policies,
routes,
async start() {
await lifecycle.start(context);
await events.emit('platform.started', { version }, { owner: 'vendoo.system' });
},
async stop() {
await events.emit('platform.stopping', { version }, { owner: 'vendoo.system' });
await lifecycle.stop(context);
},
async health() {
const moduleChecks = await modules.health();
return {
ok: lifecycle.snapshot().state === 'running' && moduleChecks.every(check => check.ok),
lifecycle: lifecycle.snapshot(),
modules: moduleChecks,
};
},
snapshot() {
return {
version,
architecture: 'modular-monolith',
lifecycle: lifecycle.snapshot(),
modules: modules.snapshot(),
features: features.list(),
policies: policies.list(),
routes: routes.list(),
moduleState: moduleStateStore.snapshot(),
};
},
});
}
+30
View File
@@ -0,0 +1,30 @@
import { PlatformError } from './errors.mjs';
const POLICY_ID = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/;
export class PolicyEngine {
#policies = new Map();
register(id, evaluator, { owner = 'vendoo.security', description = '' } = {}) {
const normalized = String(id || '');
if (!POLICY_ID.test(normalized)) throw new PlatformError(`Ungültige Policy-ID: ${normalized}`, { code: 'POLICY_ID_INVALID' });
if (this.#policies.has(normalized)) throw new PlatformError(`Policy bereits registriert: ${normalized}`, { code: 'POLICY_DUPLICATE' });
if (typeof evaluator !== 'function') throw new PlatformError(`Policy benötigt eine Prüffunktion: ${normalized}`, { code: 'POLICY_EVALUATOR_INVALID' });
this.#policies.set(normalized, { id: normalized, evaluator, owner, description });
}
async evaluate(id, context) {
const policy = this.#policies.get(id);
if (!policy) {
return { allowed: false, reason: 'policy_not_registered', policy: id };
}
const result = await policy.evaluator(Object.freeze({ ...context }));
if (result === true) return { allowed: true, policy: id };
if (result === false || result == null) return { allowed: false, reason: 'denied', policy: id };
return { allowed: Boolean(result.allowed), reason: result.reason || null, policy: id, details: result.details || null };
}
list() {
return [...this.#policies.values()].map(({ evaluator: _evaluator, ...policy }) => ({ ...policy }));
}
}
+72
View File
@@ -0,0 +1,72 @@
import { PlatformError, platformErrorPayload } from './errors.mjs';
const METHODS = new Set(['get', 'post', 'put', 'patch', 'delete']);
const ROUTE_ID = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/;
export class RouteRegistry {
#routes = new Map();
#policyEngine;
#moduleRegistry;
constructor({ policyEngine, moduleRegistry = null }) {
this.#policyEngine = policyEngine;
this.#moduleRegistry = moduleRegistry;
}
register(app, definition) {
const method = String(definition.method || '').toLowerCase();
const id = String(definition.id || '');
const path = String(definition.path || '');
if (!ROUTE_ID.test(id)) throw new PlatformError(`Ungültige Route-ID: ${id}`, { code: 'ROUTE_ID_INVALID' });
if (!METHODS.has(method)) throw new PlatformError(`Ungültige HTTP-Methode für ${id}.`, { code: 'ROUTE_METHOD_INVALID' });
if (!path.startsWith('/')) throw new PlatformError(`Ungültiger Routenpfad für ${id}.`, { code: 'ROUTE_PATH_INVALID' });
if (!definition.policy) throw new PlatformError(`Route ${id} benötigt eine explizite Policy.`, { code: 'ROUTE_POLICY_REQUIRED' });
if (!definition.owner) throw new PlatformError(`Route ${id} benötigt einen Modulbesitzer.`, { code: 'ROUTE_OWNER_REQUIRED' });
if (typeof definition.handler !== 'function') throw new PlatformError(`Route ${id} benötigt einen Handler.`, { code: 'ROUTE_HANDLER_REQUIRED' });
const key = `${method.toUpperCase()} ${path}`;
if (this.#routes.has(key)) throw new PlatformError(`Route bereits registriert: ${key}`, { code: 'ROUTE_DUPLICATE' });
const contract = Object.freeze({
id,
method: method.toUpperCase(),
path,
owner: String(definition.owner),
policy: String(definition.policy),
audit: String(definition.audit || ''),
input: definition.input ? 'validated' : 'none',
rateLimit: String(definition.rateLimit || 'default'),
csrf: definition.csrf !== false,
stability: String(definition.stability || 'internal'),
});
this.#routes.set(key, contract);
app[method](path, ...(definition.middleware || []), async (req, res, next) => {
try {
if (this.#moduleRegistry && !this.#moduleRegistry.isEnabled(contract.owner)) {
throw new PlatformError('Dieses Modul ist deaktiviert.', {
code: 'MODULE_DISABLED', status: 503, expose: true,
details: { module: contract.owner },
});
}
const decision = await this.#policyEngine.evaluate(contract.policy, { req, user: req.user, route: contract });
if (!decision.allowed) {
throw new PlatformError('Keine Berechtigung für diese Aktion.', {
code: 'POLICY_DENIED', status: 403, expose: true,
details: { policy: contract.policy, reason: decision.reason },
});
}
if (typeof definition.input === 'function') req.validatedBody = definition.input(req.body);
await definition.handler(req, res, { route: contract, decision });
} catch (error) {
if (res.headersSent) return next(error);
const status = Number.isInteger(error?.status) ? error.status : 500;
res.status(status).json(platformErrorPayload(error, req.requestId));
}
});
return contract;
}
list() {
return [...this.#routes.values()].map(route => ({ ...route }));
}
}
+34
View File
@@ -0,0 +1,34 @@
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
@@ -0,0 +1,28 @@
{
"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
@@ -0,0 +1,32 @@
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
@@ -0,0 +1,14 @@
{
"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
@@ -0,0 +1,67 @@
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
@@ -0,0 +1,100 @@
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
@@ -0,0 +1,39 @@
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
@@ -0,0 +1,28 @@
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
@@ -0,0 +1,38 @@
{
"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
@@ -0,0 +1,192 @@
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
@@ -0,0 +1,132 @@
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
@@ -0,0 +1,695 @@
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
@@ -0,0 +1,41 @@
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
@@ -0,0 +1,20 @@
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
@@ -0,0 +1,25 @@
{
"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
@@ -0,0 +1,6 @@
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
@@ -0,0 +1,27 @@
{
"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
@@ -0,0 +1,2 @@
import { getStorageLocations, getListingsByLocation, getUnassignedListings, bulkUpdateLocation } from '../../../lib/db.mjs';
export const inventoryRepository = Object.freeze({ locations:getStorageLocations, byLocation:getListingsByLocation, unassigned:getUnassignedListings, bulkUpdateLocation });
+7
View File
@@ -0,0 +1,7 @@
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
@@ -0,0 +1,10 @@
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
@@ -0,0 +1,6 @@
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
@@ -0,0 +1,30 @@
{
"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
@@ -0,0 +1,18 @@
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
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,68 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1,28 @@
{
"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
@@ -0,0 +1,2 @@
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
@@ -0,0 +1,2 @@
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
@@ -0,0 +1,8 @@
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
@@ -0,0 +1,28 @@
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
@@ -0,0 +1,14 @@
{
"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
@@ -0,0 +1,62 @@
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
@@ -0,0 +1,233 @@
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
@@ -0,0 +1,26 @@
{
"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
@@ -0,0 +1,13 @@
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'] }; },
},
};
}
@@ -0,0 +1,28 @@
{
"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
@@ -0,0 +1,23 @@
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
@@ -0,0 +1,30 @@
{
"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
@@ -0,0 +1,13 @@
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
@@ -0,0 +1,26 @@
{
"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
@@ -0,0 +1,26 @@
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
@@ -0,0 +1,31 @@
{
"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"
]
}
+24
View File
@@ -0,0 +1,24 @@
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;
export function getSessionsService() {
if (!service) throw new PlatformError('Sitzungs-Modul ist noch nicht gestartet.', { code: 'SESSIONS_MODULE_NOT_READY', status: 503, expose: true });
return service;
}
export function createSessionsModule() {
return {
manifest,
lifecycle: {
async start() { service = (await import('./service.mjs')).createSessionsService(); },
async stop() { service = null; },
async health() { return getSessionsService().health(); },
},
};
}

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