Compare commits

..
138 changed files with 166 additions and 4377 deletions
-3
View File
@@ -42,9 +42,6 @@ jobs:
- name: Git-Staging-Filter Regressionstest
run: npm run verify:git-staging
- name: Git-Ignore-Grenzen für Quell- und Runtimeordner prüfen
run: npm run verify:ignore-boundaries
- name: Syntaxprüfung
run: npm run check
+1 -2
View File
@@ -47,7 +47,6 @@ jobs:
npm run verify:git
npm run verify:git-regression
npm run verify:git-staging
npm run verify:ignore-boundaries
npm run check
npm run verify:source-boundaries
npm run verify:esm-exports
@@ -125,7 +124,7 @@ jobs:
- name: Container-Metadaten erzeugen
id: meta
uses: docker/metadata-action@v6
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
+42 -42
View File
@@ -13,22 +13,22 @@
node_modules/
# SQLite runtime data
/db/*.db
/db/*.db-shm
/db/*.db-wal
/db/*.sqlite
/db/*.sqlite3
db/*.db
db/*.db-shm
db/*.db-wal
db/*.sqlite
db/*.sqlite3
# User and runtime data
/uploads/*
!/uploads/.gitkeep
/backups/
/logs/
/tmp/
/temp/
/sessions/
/data/
/docker-data/
uploads/*
!uploads/.gitkeep
backups/
logs/
tmp/
temp/
sessions/
data/
docker-data/
*.log
# OS and editor files
@@ -43,20 +43,20 @@ compose.override.yaml
docker-compose.override.yml
# Local AI runtimes and downloaded model data
/local-ai/comfyui/
/local-ai/downloads/
/local-ai/extract-temp/
/local-ai/fashn-vton/.venv/
/local-ai/fashn-vton/engine/
/local-ai/fashn-vton/weights/
/local-ai/fashn-vton/outputs/
/local-ai/fashn-vton/runtime/
/local-ai/fashn-vton/hf-cache/
/local-ai/fashn-vton/fashn-vton-*.zip
/local-ai/fashn-vton/python311/
/local-ai/fashn-vton/uv/
/local-ai/fashn-vton/uv-cache/
/local-ai/fashn-vton/downloads/
local-ai/comfyui/
local-ai/downloads/
local-ai/extract-temp/
local-ai/fashn-vton/.venv/
local-ai/fashn-vton/engine/
local-ai/fashn-vton/weights/
local-ai/fashn-vton/outputs/
local-ai/fashn-vton/runtime/
local-ai/fashn-vton/hf-cache/
local-ai/fashn-vton/fashn-vton-*.zip
local-ai/fashn-vton/python311/
local-ai/fashn-vton/uv/
local-ai/fashn-vton/uv-cache/
local-ai/fashn-vton/downloads/
# Historical handover and feature-package archives (never source)
/Archiv/
@@ -64,32 +64,32 @@ docker-compose.override.yml
/archives/
# Local update, rollback and slice archives
/updates/
updates/
# Operations Center runtime
/operations/staging/
/operations/updates/
/operations/rollback/
/operations/apply-*/
/operations/pending-operation.json
/operations/last-operation.json
operations/staging/
operations/updates/
operations/rollback/
operations/apply-*/
operations/pending-operation.json
operations/last-operation.json
# Deployment assistant state
/.vendoo-install-mode
/runtime/
.vendoo-install-mode
runtime/
# Generated deployment and release output
/release-output/
release-output/
*.release.zip
*.release.sha256.txt
/.vendoo-github-state.json
.vendoo-github-state.json
# First-run and local installer state
/operations/first-admin-code.txt
/config/local/
operations/first-admin-code.txt
config/local/
# Installed Vendoo module packages (runtime)
/modules/
modules/
# Python cache
__pycache__/
-8
View File
@@ -1,13 +1,5 @@
# Changelog
## 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.
-8
View File
@@ -1063,11 +1063,3 @@ Neue Kernel-Routen benötigen zwingend einen Modulbesitzer und eine explizite Po
- 17 Module, 15 nativ, 2 Legacy Bridges und 38 aufgelöste Capabilities
- GHCR-Releasebilder mit versionierten, `stable`- und `latest`-Tags
## 1.41.2 Git Ignore Boundary & Module Tracking
- Root-verankerte Runtime-Ignores
- `app/modules/` und `app/modules/sessions/` bleiben versionierter Source
- Git-Ignore-Semantiktest mit echtem temporärem Repository
- Pflichtdatei- und Tracking-Gate im GitHub-Deploy-Assistenten
+21 -38
View File
@@ -3,9 +3,9 @@ aa343f8ee7948e6b87603e39cf208f8fd1c42a023f1c2261c708c0cd8a46faba .env.example
58e3f4b76cc9398f32dcd5481b2080e7e5ba322aca04b8b8374446299c5d7dd6 .github/CODEOWNERS
5bd335eb750d084ca12b6a3b3f35d0cd7bc454ba24ed280187c66acf75eaf396 .github/dependabot.yml
f65684e98b01d0e0b6bfe1284b5de3354559e4cd51e634fe9e9e2e5473b890e5 .github/pull_request_template.md
505e50970bda1156b91a2f3f5031877495e95b63db06129910ac7326c14c9c92 .github/workflows/ci.yml
a5b12e2d6bd326034c5c45ff137b649aee580dccc9cf4a41c9d6cc44e81c1b0f .github/workflows/release.yml
851ad84ebaa6d07af5fe975b3461c2402e7315a294ae29f0cd1a5f13cb7fc44f .gitignore
c171b46c41f53363139b7ba2b442a6dc4fd2f2807811a2dcc4adbd14844972c1 .github/workflows/ci.yml
6b1fc844cf0f220c9791a049bbaf3e53b7a93d1036dbbd5abe1aad2f0f7637fb .github/workflows/release.yml
6caf6e9ab52bd7f634994876c359e543cc7bc8bed2f0e7c09b70eee2651e07eb .gitignore
9bfa732952ad571d06527b32d96d79133fc23fa607df00de440b2802f543608c .npmrc
9d7c2a587c12f2b12ba2923e367ade8cdd4601dcac800ca2ac34afb302e9a6e1 app/architecture-boundaries.json
a6bdb9b6fdc97a0b2bbc052bd242a9885a75bcc46ee4f0414d8b7e215b6940de app/contracts/module.schema.json
@@ -84,11 +84,11 @@ c6558b8a0cd9d7ef30d0e259747805e3d91a5359c2dbfbd79e6794bb403a9926 app/modules/us
8640accd1550932459ecd14738139284cbcdd93f31a61ab6e7748791fc82933f app/modules/users/routes.mjs
fc4af51a35fd08289ec4380a78c06b3236a2469041ecfc911b9e0d7d269afa19 app/modules/users/service.mjs
c197ce417fbce6dab1d8ee157b0a105a099dd2021ece4dac3891d0b2022a0d58 bootstrap.mjs
c525080bcf08aa43d694f5075c43c7ad9c06901b0aed453695b343393f7a8b9f CHANGELOG.md
bee3a2c088e7fc26d4b59429e51f127adf6c0acaefb9ee28a9e35d349f533de2 CHANGELOG.md
61823fc9fdc036170cce6edeecbb612b442d1c65fc9f7a7eaba47e5016316ee5 CLAUDE.md
61199cecda2c84954de2b60c07eb9f3a2117fbaaa4c430319c461b2e76a10254 compose.bind-mounts.example.yaml
b8167851bcdbefa4a22e143cc6de8ab4441d3970752629bb2d8fdb03984f2904 compose.vps.yaml
b76a40463ce31202dbdc85db371eb9c6fcafce8cd263c685dac990311dbd50f4 compose.yaml
7cb13c091d0ee663ca6a5c004735bf1fdb1a4507ad967ddd2e6624dd9d47faf1 compose.vps.yaml
a2a091aa0c39059b48aa12403ba22ed017266d310d9b21c192211d6f3f926356 compose.yaml
6323f984f1e04ab135356a14ec151941638041f6b305ed68772d5e1812972075 db/schema.sql
49690c4e4c8e84de8eb40ffe79823493b7be20fe46fa2ad9e6544f7f0afbfd2c deploy/Caddyfile
4ffc43e64b0e1e51a97d3f7f6a344ebcff537395008cf909c0fc4e3d3ab0c0cf deploy/coolify.env.example
@@ -107,7 +107,6 @@ c050c8e11f2bdb0a742851423dcff864bafba9cc839c52b2f8d48b75f7b18633 docs/ABNAHMEBE
513144e961bcc6d3a6c6a5f82b80b24733f20cd2e0d14259b500b426a4f9dee6 docs/ABNAHMEBERICHT_1_40_2.md
1439ecb811af31dc2d54ca63f0521ad445e30940fae91ac7d23adc352c8332ad docs/ABNAHMEBERICHT_1_41_0.md
b23f235ef4ddf3bbc0fde848fefe9bdcc4b90d1efb25dc61d51cf7cb295415ec docs/ABNAHMEBERICHT_1_41_1.md
2f199ee66a97bbec2671d528f3b22c4583530644f7c16a4820fbe9c05e731bbd docs/ABNAHMEBERICHT_1_41_2.md
b39d2d892068e1313fc00c9345d54240b6d50caf8a940261a088e67d8778ac63 docs/architecture/CATALOG_MEDIA_MODULES_1_39_0.md
e5f065ba8d516a5ebd47e7e6859d0aab40ef69797413e358d969ef27c454fcdd docs/architecture/DESIGN_SYSTEM_1_35_0.md
732ef23db1ef50862a423841f58e4628ad3b2a2ef2744ed7c5ef1fe01e3e38d1 docs/architecture/IDENTITY_SETTINGS_MODULES_1_38_0.md
@@ -126,7 +125,6 @@ ba9a9b93622fadc399cd9067d80b4ea339193eb19825106264a3b86b7128d107 docs/GENERATOR
e5c6ac33f5c0dc93099148d95af123a9aeb72298d885cabf0b1dc96a9cad31d7 docs/GITHUB_IMPORT_STATUS_1_39_0.md
509fd2822e4c547459fef9a54054d583dc21adb1b1101eda92603f1cbb783604 docs/GITHUB_IMPORT_STATUS_1_41_0.md
8e4ee3c837df6e929a819491b977123872191c8e4f041f8598c9479df10d28fa docs/GITHUB_IMPORT_STATUS_1_41_1.md
16effe96702a6951b6cf3bb06da052420a492024c252a88b69256a3f4a92eccf docs/GITHUB_IMPORT_STATUS_1_41_2.md
5a25e952d7d79b562bc19c990451830ea2ac3e730c9af399425ea18ffb8fd61d docs/GITHUB_IMPORT_STATUS.md
65b7c1cb3a300fa8209628f32d843d1cfba7baaae1f1078bd3cfc1737443df92 docs/GITHUB_MERGE_RELEASE_1_41_0.md
65d19c1576f4a6011008a7e447a3530222517752fb59ebc5bbb506b791e32605 docs/HOTFIX_1_23_1_EDITOR_CACHE_VINTED_FIELDS.md
@@ -177,7 +175,6 @@ dd77564536e5d85eaa76c7a429a61f73ac78547d72734620bc4102b3452cd7b4 docs/RELEASE_1
69b6df0262868a6e6ef81f406ee4c9acd3f662d1f6ee11d7fdd5fea96a2f0de7 docs/RELEASE_1_41_0_COOLIFY_PRODUCTION.md
97f7bc395f2ce6bef24140e75dc8956a0d56cbaf15cb94876d4594d7aa54b1db docs/RELEASE_1_41_0_VERIFICATION.json
29c795b5599bafdf0f911a4c7ab2844ca3fb311bcdd5f3983c7fa2742110c3ff docs/RELEASE_1_41_1_CI_SOURCE_ROOT_ARCHIVE_EXCLUSION_HOTFIX.md
178927940cc1266307f81bbf418870e627d2e52b4956c2d9065c31e04c9bf7dc docs/RELEASE_1_41_2_GIT_IGNORE_BOUNDARY_HOTFIX.md
1908713d7458ecacc833faced77984ef5c0155cd61b0eac13a4f874e3f3a5676 docs/ROADMAP_1_38_0.md
185b1007c90273126ebe92bffe69f88ae4ec4a3f04451cb9d5d4b772a0482391 docs/ROADMAP_1_39_0.md
b581d7d949139f37798dded557235a679621172f444a6271e7fc7ade3020299a docs/ROADMAP_1_40_0.md
@@ -302,7 +299,7 @@ e3a3863b58a571a0f0180bdd333f9389b918415b4ea96b70f3ab6d4105d95c50 extensions/saf
2bf2d5ccf02622cf1d62fd553d21c0ef1b744616742e7962ac302c10d37426e2 extensions/safari/web-extension/popup.js
273322e8dc29b0c6b639a6cc7e44a9570ceece09306fd46003b05a8fc86c88b7 extensions/safari/web-extension/vinted-categories.js
273322e8dc29b0c6b639a6cc7e44a9570ceece09306fd46003b05a8fc86c88b7 extensions/vinted-categories.js
4be8e17f83ebb805abb1beddf4d7a79e3a44ce58585cb6b8143bb01a47e16c6b FEATURES.md
f4caaaad32095dbb6dbb7ef68f1777fdd0a036e1efdfe611446789790021b51d FEATURES.md
7f78a0d512de717ba76c9a5e3e1bd2e52db9edf146e8eeb7297458fde5d92afa GitHub-Deploy-Assistent.bat
9a5bfcafdce53130d7bc36695090a8f87096b4ead9dd5ccc3d46d1d80c07a0fa lib/ai-claude.mjs
ad6ba4ef521557d5c06bc546cda70db57e710e8173daadae74e8f15f99836c1c lib/ai-flux-prompt-jobs.mjs
@@ -349,15 +346,15 @@ b1c62e118d9be316274e966c558cdcccdd41afb3a9f05a60ec1e4a3433d2ca0c MockupDesign/L
a52983b9ea2bf920d115485d10bc30226da88bc5cba8b00bebf801b21c096346 MockupDesign/Publish.png
8c8bf278d0c6725e1dbb97b3c3d0c36a9bfc0753c3c86433fdc294bec70dbc0e MockupDesign/Startseite.png
62f14221b1d71c9e631efdadd221397ff657adae4b737df52c8bff1610b90999 MockupDesign/Vorlagen.png
a111671a21576140185a0c4d01038e200f87cdcbf903efe57bb84f65c972fd1a package-lock.json
740a9ee1699aa8e24fab4ab66373bf4322bee0e60c6dd80dbc07e8f9d18a3771 package.json
62daf878d676237dff931c292a79309662176b84f28be9dbd1af8af42827e883 package-lock.json
7b4ca7c0bad995c03180c04291f0f71a4a6cd35446b4db96fc33bf0a0dd36930 package.json
3da71dd28cb25c7c7317b9061855d40c6a3ab8bc39391963b49ee1e707f838e5 platforms/ebay-de.mjs
cc1ca2b4384c8c548bf755dc33101d248541bf893c3d5f35369961f904afcc00 platforms/ebay-ka.mjs
fa52b247dd758aeee06120499bd1ae6c53e9f460f8bd7762b8f06141b201e32b platforms/etsy.mjs
5c925e3934a4c9e2f26dc15b3449a249f5ce20ecebbd459b15365a34804824d6 platforms/vinted.mjs
a2ea3f5f56dba8bd4d23fa54a26fd941567b4febdd1ed1b4e9f5bcc1c9786e53 public/android-chrome-192x192.png
46ea6ba4ca6ca2352f37353b8bc4ca258935d29ecb8d3c9b133a48ea75c0c58f public/android-chrome-512x512.png
cdf390c3ae338cc422ecd5338b3f50adb20fdbf4fe6193f2f4553c096efb2f10 public/app.js
d06f9ac5e795a2a1c246e89ec69b64d7c90cdf0d34d8d3507ec1950a523ce17c public/app.js
e0e7b8ef01c36f0625b6157aa46bd69de05c17be0b054208fb635d6507b52154 public/apple-touch-icon.png
a2ea3f5f56dba8bd4d23fa54a26fd941567b4febdd1ed1b4e9f5bcc1c9786e53 public/brand/favicons/android-chrome-192x192.png
46ea6ba4ca6ca2352f37353b8bc4ca258935d29ecb8d3c9b133a48ea75c0c58f public/brand/favicons/android-chrome-512x512.png
@@ -399,8 +396,8 @@ fb3dbcd03f7ed15b773b582c2a2b785c3040d3319aba5b7c682a7e74e1cf3b4e public/core/sa
4d69e2cb6b7f9f63b54d708a5d8b1ebefcdcb9f88a977053db5031c05b098464 public/design-system/tokens/source.json
4a9e65edeaee3421edc3dd4831230062494f1a42f8495f4cb979fde8664d06c4 public/design-system/tokens.css
15bf8dd655b34a8015447e834d387af12685238ede598f5f0e7927eea2933483 public/favicon.ico
6d90d0dc93416395caf6155e5efc13e3e87c9c44b828542983d9033bb7a49e08 public/index.html
85a655300980b6ec32b35faadec4b30b85cf45cd5799e8179050ac07a280917d public/login.html
b9a6d053dc1b664e515b06e532f454f34be9b82bd95d456218dc0e991b6afcce public/index.html
248b411391d654e1d87903cb36d65582acbb08a78dbafe156feaf335c1fbe434 public/login.html
e37558ee9612a36db8a19999fbb320f1c249a8e155f94c83a82926bec920f989 public/login.js
59291f75eca405e3856bafea76cc601d681060eb6b36bf82a4be3c3ab7eac290 public/mobile-upload-expired.html
f046f166d89fc32774b87deb02e6d144fd2f40c0925aac67487e5b7c489a0ec1 public/mobile-upload.css
@@ -413,23 +410,23 @@ bf10d5ccc53f287a8b61d488af50c3151c764f36a29ee5d4fe9ee321fa0eda06 public/modules
0edc61f0c2bb604c3adc40813b18fe5ca8d9b846ec2a3d9f406ae26ae313d3ee public/modules/theme-manager/theme-manager.js
e0a6c981d4231eab26d981be62647d0e6f89e05e8371baad121348202ad54f49 public/style.css
ff525e0af963e6f54636cfdc1bd2d045aaf82e7b0199755eb5cdf4c52110be2e public/vendoo-icons.js
9cf6628dd714347563fdc5925e323c0a274ee5967d78555757be85c56fcdc0b0 README.md
bfcb47b331d55689921a3839950af20e17e96a6c44eddad5c74a7e342f078aa8 README.md
f397e46da92394d34c2fc01715f037cca380703ecfa1d4a451de8ec7342e05b3 scripts/docker-backup.sh
86a411ae1fa5e36a11b0a8cb61e6c0169e0b80d3cedc9b7b82c3cfff23dd7390 scripts/docker-entrypoint.sh
1b58c1e5f762c4928d42605c01872f38f6d6bca92342260f3cd88a132e6af095 scripts/docker-restore.sh
1720c13fed290c08361266257279e86c8b5909c4b39d3729439335cbcc06a387 scripts/github-deploy.ps1
d4d41170106979e1be9266a569215ff484e80452f568f0bf8dd9cdc9978dbb7c scripts/github-deploy.sh
be7cfcdd337279294efca8e3c4541e8c60a0d638ae80fe3cfd9021c0d6c326e1 scripts/github-deploy.ps1
8f594c7cc7f0d3a00a4167a9e1a9f34173d13d73d08388da0e2c932bbd318302 scripts/github-deploy.sh
0a9bafc79c1c5d59fb9b51ebc34c00eec4a4e9a5e873ce1818f50f3ab983da70 scripts/install-local-flux.ps1
2924f4e8abe5ebbd099fe1ceb2bc6ceb4bdda55fa3e57dd299cfc05d310a9246 scripts/runtime-start-local-flux.ps1
7c6176d8311b1ba9d511beb40edd41b8248e61c298528bbec3f9ef1171856c05 scripts/runtime-stop-local-flux.ps1
1a5736cf5fccc991df122163977c2eac80281bd99a54a5680b3d7eae99e4b79d scripts/start-local-flux.bat
3bf3aa74f5cc1f2946bb2da637eb30fe8daec783a32f9721ebb0468e20df90bd scripts/uninstall-local-flux.ps1
327bb6e7b99939508b807d0c3c6d5617a2d8f0d5464ba0b1865d8e9e1ed82dde SECURITY.md
af7234a31ef7dca4545c8c071f3773739f856a173fdbf96d0e060cd95c7ade63 SECURITY.md
abb508872f30a0d6e4b6d33ab2f4cb14dea11838b15dbe502e6fac2ecc16cf13 server.mjs
eae7a799aaeec708ea28aca9fc6b08132909cf0b41d412e889064876ce4ba94b setup-gui.ps1
a8a56c5b7062f4a671317b53e50442b53bc641ff44f32155444b6b605ec263b4 setup-gui.ps1
cf4519ce5adc01f05458e3286e86f3a419cceaff5fcdd869a1d07f1c1d1a0d8c setup.bat
9433d663b6537ab5bb52a80733b18ac97e932ec50650b2d6bae8d67d5f05cdb4 setup.ps1
9cf6480f9eeda7fecc82a6bf1d9bfb3d94b75ca42c92e7697679b9efd8816215 setup.sh
eb20bcc9151a9ed4a039aaebc6f5776223313bd4281972735856aa202fdfc2f2 setup.ps1
94f028ca7249592af0fe583d72537afd2fe9cd8e87e6ecae806719293386bc4f setup.sh
2851c914a1b50a036cb265ddd32e5545cc7e8e34ba9d682445926acc23eb8f28 THIRD_PARTY_NOTICES.md
90485a16e3468cb9a6750646700fbb036cb9ac410e9b8bd5c582f7b923bf3c31 tools/build-release.mjs
94f4ced84834fceeb4f797bf8aab0703c873b57fd5d4a5b3fb02fee12e42bd2e tools/check-syntax.mjs
@@ -438,21 +435,17 @@ ac8440bce2659a035302609722f1dfed5d125223376cd575ffc0636397c94b54 tools/generate
e3d8c6e8dfd566acbe037ad744edbf60816152d4994698c2a9c46d2b890e246a tools/verify-capability-graph-1.40.2.mjs
8916ca58f8f67b7186d4b3644d13a56743965a57a50dc30bb33bd6baa8b2b646 tools/verify-capability-graph-1.41.0.mjs
e7cbaaa0ca1f0a655af500aea14044455a80cfae10246716a17488f96e99dae7 tools/verify-capability-graph-1.41.1.mjs
62b3e3c998759d59b78ae0a6609f51bf87369880ea1384b274c771d70d2a6476 tools/verify-capability-graph-1.41.2.mjs
85114eff887ba65ca7df00f43333414c44b3aadc16fbc2fe10dc4dd4f9aa7aed tools/verify-catalog-media-1.39.0.mjs
753288b83a655dbe4a9da8e93d8c3956ae17544699e2221873d479d72b977e2f tools/verify-catalog-media-1.40.0.mjs
f208ce5267103ab81c30da71e7b7c51c973b36dd617a1bc42c12063fbf1473f0 tools/verify-catalog-media-1.41.0.mjs
1c8da7af9bb5d743a60997fcba3345e568a862c97ff7102621134fa347a0056f tools/verify-catalog-media-1.41.1.mjs
b05890cd996c6dc28a47fb662026ac794bd096c23c3b1bb7a9fdc01224e8032e tools/verify-catalog-media-1.41.2.mjs
93d8fdee3feaf6e82ef15e7ac84bfd7e43ad13192f94655ccfce37f5a87b21ef tools/verify-catalog-services-1.39.0.mjs
60de35103ed4c4abf30445b4465b849f8be8842f1a4c22da6ae5d74943dba386 tools/verify-catalog-services-1.40.0.mjs
f89edc3b9d32c1618522c5810e53f25b47e72d52e8a041a2e7ae7939fbe169f2 tools/verify-catalog-services-1.41.0.mjs
122d1664ce9eff57331fd5f86554943494d46ba38e90f7e1a13c0de33ad6906d tools/verify-catalog-services-1.41.1.mjs
fb8ee4afa1d6abc09106ab032793da8dfef43bcb8614c7fe04086eb52cfb2a0b tools/verify-catalog-services-1.41.2.mjs
8f9ede5f112ee03323db3a6c73aee00b7037f5139dad241b11baf5f0e9cee4e0 tools/verify-config-store.mjs
e5a0614ef4f2ea73bdaaa1924cfafe86550a1a50c0105c569e405c332d8389a5 tools/verify-coolify-deployment-1.41.0.mjs
63afde37353f750f4bfe0b02324f774a7b3c7ece9339d85e5428b9a427c0d11e tools/verify-coolify-deployment-1.41.1.mjs
a66efb128565db5267c03a1503eb29d1f12024e1a357e1fd17d409430c2503f3 tools/verify-coolify-deployment-1.41.2.mjs
5752f6edb956696781da8bddb1c1848d26941fbed5a524691653c84c88c34bff tools/verify-db-migrations.py
3745de55a110d8254983b096c6afab196ddaec7f1153cd2f34c1e480b15aa33e tools/verify-deployment-1.32.0.mjs
44f6b349b722e5a83d4725c4879fa1a9383f59ab5634c49d3d7d9d7aa0cd5e12 tools/verify-deployment-1.33.0.mjs
@@ -469,17 +462,14 @@ b65eec7fd54e1459b363d99d20f9438ae5bc4b518e775de8b97e33e89a382e42 tools/verify-d
77f44ad4e13da47bcf4bc9c6a37d014c8b5f6c3e735ecfdb63830e09e43df296 tools/verify-deployment-1.40.2.mjs
9a33b8594744ed4c7774f12da5b14dce64049c1c0df65e98c9cd26d5404fda0c tools/verify-deployment-1.41.0.mjs
a885727e8847bf32ead7d7dc80e2e27deac62df53763a187717fcd8e6157a1f5 tools/verify-deployment-1.41.1.mjs
ca6e6baebc9489fd101d005f75c34c90b28e914443a11e3ad759513f653f4c82 tools/verify-deployment-1.41.2.mjs
c2d270efae9209d6fdf810f44db4cfdb9642db29060d5b75c2d920a859bc6918 tools/verify-esm-export-contracts.mjs
58c275a972393d7db7cab087523d3963a743e5a0d654fbc9b2cc09618abfea6c tools/verify-flux-loop-hotfix-1.40.1.mjs
58c275a972393d7db7cab087523d3963a743e5a0d654fbc9b2cc09618abfea6c tools/verify-flux-loop-hotfix-1.40.2.mjs
824ea8ed05fb68ef6bedba83e76697f9b03eb9830db981781e002c64f8b6858d tools/verify-flux-loop-hotfix-1.41.0.mjs
a055379e9ca3e7ebded24d4c75ccd54f8f81aa3618d63cdf41d79e3a17f126ac tools/verify-flux-loop-hotfix-1.41.1.mjs
c84f814c5e7dba7144709c6be860ab8ecbcb0541d167dca1e3172717196a327b tools/verify-flux-loop-hotfix-1.41.2.mjs
e06cda8d9850a84cde34a99009c51a18dbc1b7a66e45c82f35aadd631a71540b tools/verify-git-ignore-boundaries-1.41.2.mjs
d716f48436179c7f156b43a687b6149648fd38de12767da2be1bc63e3e1fce31 tools/verify-git-safety-regression.mjs
872cb8d1e0f86e2e571ddcce1adbb002d0a86f2d834f4ee7f601e011c7306e70 tools/verify-git-safety.mjs
ad2d1df5725da388ca427ddf6da0e0d62bf02999c6554e8c1ab918d63f5b8d50 tools/verify-git-staging.mjs
3e2eb9358b3a7060cf7d47d52dadb7744544a7682befb45822303cb96b2788ba tools/verify-git-staging.mjs
1d4351e4a827e718766a1677ddfbc3771a91942d0d5a93753e39f1b99c68a4dd tools/verify-hotfix-1.23.2.mjs
2f43dd1a89a36527cd3b4873b8cc9f7386cdfc06ade96fcc05dd5deb15dbcec7 tools/verify-identity-db-1.38.0.py
2f43dd1a89a36527cd3b4873b8cc9f7386cdfc06ade96fcc05dd5deb15dbcec7 tools/verify-identity-db-1.39.0.py
@@ -489,15 +479,12 @@ a1dddb35f4b6eeafe47ff3f40e5565bdb4477c8f63101d3c0d7f84c7393abe4c tools/verify-i
ff839066b02121a19046f19093904550e1184785e73dd2d079ab518cc74583ff tools/verify-identity-settings-1.40.0.mjs
3725fdbb20e93dd0bcbf3ad385bbb0c459cdd68448371757e549543b007155cc tools/verify-identity-settings-1.41.0.mjs
3dc54d074497913255774603f043e1154e2ae865ca62d7d4d682826cb02906c2 tools/verify-identity-settings-1.41.1.mjs
6d1d890a184a4cb4e1ae9edf784a0df250563e7287f6d3ee5fc43c89913b95c9 tools/verify-identity-settings-1.41.2.mjs
9492911fac3771e3b906b5a583e4bbf136ec8383b5fc23b0478f58b7407f7c65 tools/verify-initial-disabled-modules-1.41.0.mjs
932e2359c3ee00924e1a89cdcdabe44ddf1a60479f75f4d4094f2102d1c12949 tools/verify-initial-disabled-modules-1.41.1.mjs
745cb929519db756ba23e3198b4c92ff9e91173bd03d821bc2e37280af0a32b7 tools/verify-initial-disabled-modules-1.41.2.mjs
44c95844422be3deaad6054ee3f0cced9c25986815d976e0c64b042d0c2c5e1e tools/verify-installer-shell.sh
33ca957bf8acd3cc73354ef4c8d5e4b61c44810ecd8a8994ff89305cf1b9429c tools/verify-module-manager-1.40.0.mjs
2a80e837f754e7490f775956161e4fc8866c127c37b95d688baa239fee4cd883 tools/verify-module-manager-1.41.0.mjs
6b83edfaa57dd3633c7d100a8ed476ef63ae41384d215c787a2cbb6e608b22e5 tools/verify-module-manager-1.41.1.mjs
f4da6e0be42aa7eda0a496f4ab77547af6e3238a6d405aec00a61a9bb3622c1a tools/verify-module-manager-1.41.2.mjs
cab03e98687af87f359f6424d440fc0b644da97b3c614509b8c7d008d4b18a8d tools/verify-platform-architecture-1.35.0.mjs
732bb15431746a829e45ae55c695360cbf1dd21d4fc3be26a98f35708d571e84 tools/verify-platform-architecture-1.36.0.mjs
ee8ae7bed4e93a2ef278662552a4ae5d751306aad7133a93f380bbf2a5d5a28c tools/verify-platform-architecture-1.37.0.mjs
@@ -507,7 +494,6 @@ c9e704c372cf6a973fcdb887b337ead6509fd03e78570e6d1c9e24b1d879fe6b tools/verify-p
8b1b0eda98f0d16a99b47b0cb7a9e550d40d3dd048abbec6695c11ceca96f259 tools/verify-platform-architecture-1.40.2.mjs
2a06e9f6e73b37f35d055d0dd911e9cb741fa1fdb2cdfc5d926a2635f817356c tools/verify-platform-architecture-1.41.0.mjs
957cc887090f8d9c3a477c532e1aca79d24a3725924cb6e5a934f121a155c0e4 tools/verify-platform-architecture-1.41.1.mjs
ef13554f4d1a16dc97e5c17af3a719a0bd25839dae0f0f28608a706f893010f4 tools/verify-platform-architecture-1.41.2.mjs
36e35fedb08923c22fd8551389d33432b66dbd4215d95c594ef29bfc790c98e0 tools/verify-release-1.25.0.mjs
89621694b7bad18b6f3f1ac1956032f64112c40d3edc069ca581dc813d57d628 tools/verify-release-1.25.1-static.mjs
c908f5186cf2d71ef83d5e649327ae19965568933acfb32057ca00d29cf890bc tools/verify-release-1.25.1.mjs
@@ -525,13 +511,11 @@ a415d294679adab8ea76b86235a522e73ad9dd9ea0e2348691b1de3191255b81 tools/verify-s
f5296410e7c56a924978569a8ae904a1de8d4e327cb833e34e4b49e0384f3892 tools/verify-security-foundation-1.40.0.mjs
cd21f37b51ca99312c624a3b353d9eb5bff7e3dbca774600ae429f468b1d6b3f tools/verify-security-foundation-1.41.0.mjs
a619c7ecff5febe8afa746125ba100c603598951dfc177562307614ecf509715 tools/verify-security-foundation-1.41.1.mjs
6610a5d5ea6e4debedcf582c5d9992914110526bc2c371d6b9c5472fb8083768 tools/verify-security-foundation-1.41.2.mjs
31a120622ce8bb8451014f2662366f81abd28d0f3be59adfe35d17e0ada1838f tools/verify-slice30-mock.mjs
20957a4a038b9afdb01a8c98b881644daf793d2a6bb778a4c8e04780687d00e4 tools/verify-slice31-image-editor.mjs
21061ed5dc88feef48e80efffd9a1054d38598dce5a5d3f9f6bde39b539b83f0 tools/verify-slice32-batch.mjs
0f05b1ac3b686cf535bcfb3e6f6df24a29c3429ade1b3c666c1b99abb1a7311b tools/verify-slice32-image-render.mjs
168e58bd08b657abf2c7a84c7d0210e8393720e3f2b665703b03d45646480b04 tools/verify-source-boundaries-1.41.1.mjs
4424e4abf9a1dddd138b0dcb91b6d63f646c3a0c8eaae484d1e9d5b34bb35d77 tools/verify-source-boundaries-1.41.2.mjs
c9aa6693aa93ff1f220a2ce3bc35ef714a0b18a53eae0f907879fc92c66c1e38 tools/verify-theme-manager-1.36.0.mjs
1e61c4b180aa09de3dae9e72aef313b09a6f422229f180984b69a8d226e7cd8d tools/verify-theme-manager-1.37.0.mjs
3257b31d80034c077b25d3f1b13f8378378772bc6512445aa5ac8e882e4be109 tools/verify-theme-manager-1.38.0.mjs
@@ -539,6 +523,5 @@ c9aa6693aa93ff1f220a2ce3bc35ef714a0b18a53eae0f907879fc92c66c1e38 tools/verify-t
072a20e27b80b911ace2c9c18d1bb0298f59345967b6ee8f3d6b9075c522a6ba tools/verify-theme-manager-1.40.0.mjs
251bc56d2d52cbac1e126e35ad5e0077affb2b5cde6b251ad3df6a39ce67d16d tools/verify-theme-manager-1.41.0.mjs
33a6f24d26cda4d2129501c4e826ed0dda4eb9e15a324bf823b2ef19540e31ef tools/verify-theme-manager-1.41.1.mjs
8d825f95253f96b182ac9322b20c8f32c189eedd2a8c366170791c7623acbb1f tools/verify-theme-manager-1.41.2.mjs
0f3f684019763a14f5fbaec42e42c680cf748d930d46cd607fc61eb413ca4553 tools/verify-ui-contracts-1.26.1.mjs
a7ba863085e196ba5629cfbc344fcf8a05f45406f6bc6c6b75b3f45bf48bfa46 update-manifest.json
5f599423e8bec8b88de8aaf1223be49f63fb53b4a090e9129b7592c4a6e5c4d7 update-manifest.json
Binary file not shown.
@@ -0,0 +1,15 @@
Vendoo Logo & Favicon Paket
Inhalt:
- logo/: Hauptlogo und transparente Varianten
- icons/: PNG-App-Icons und allgemeine Größen für Web/Mobile/Desktop
- favicons/: favicon.ico, PNG-Favicons, Apple/Android/Web-Manifest-Dateien
- social/: Markenübersicht / Guideline-Sheet
Wichtige Dateien:
- logo/vendoo-logo-transparent.png
- icons/vendoo-icon-512x512.png
- favicons/favicon.ico
- favicons/apple-touch-icon.png
- favicons/site.webmanifest
- favicons/html-snippet.txt
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

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

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

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

After

Width:  |  Height:  |  Size: 15 KiB

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

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

+1 -5
View File
@@ -1,11 +1,7 @@
# Vendoo 1.41.2
# Vendoo 1.41.1
Vendoo 1.41.1 ist die korrigierte Produktionsgrundlage für den Betrieb über **Coolify**, GitHub und `https://vendoo.flatlined.de`. Der Container bleibt unveränderlich: Vendoo führt weder `git pull` noch Docker-Befehle aus, sondern delegiert freigegebene Deployments nach einem verifizierten Vorab-Backup an Coolify.
## Hotfix 1.41.2: native Module zuverlässig in Git
Runtimeordner werden in `.gitignore` ausschließlich am Projektroot ausgeschlossen. Dadurch bleiben `app/modules/` und `app/modules/sessions/` normale, versionierte Quellordner. Der Deploy-Assistent prüft vor jedem Push, dass alle produktiven Moduldateien physisch vorhanden und durch Git getrackt sind.
## CI-Hotfix 1.41.1
- `Archiv/`, `archive/` und `archives/` werden nicht mehr als produktiver Quellcode geprüft, nach GitHub übertragen oder in Releases gepackt.
-5
View File
@@ -144,8 +144,3 @@ Die Weboberfläche verwendet eine erzwungene Content Security Policy ohne Inline
- Antworten externer Deploymentdienste werden begrenzt und Redirects nicht automatisch verfolgt
- der Initialwert für deaktivierte Module überschreibt keine spätere persistente Administratorentscheidung
## Git-Ignore-Grenzen
Lokale Runtimeordner werden ausschließlich als Root-Pfade (`/modules/`, `/sessions/`, `/logs/` usw.) ignoriert. Produktive Quellpfade unter `app/modules/` dürfen weder ignoriert noch ungetrackt sein; CI und Deploy-Assistent erzwingen diese Grenze.
@@ -1,169 +0,0 @@
import { createHash, createPublicKey, verify as verifySignature } from 'crypto';
import { posix as pathPosix } from 'path';
import { PlatformError } from '../../kernel/errors.mjs';
import { validateModuleManifest } from '../../kernel/module-contract.mjs';
export const MODULE_PACKAGE_FORMAT = 'vendoo.module.package';
export const MODULE_PACKAGE_SCHEMA = 1;
export const MAX_PACKAGE_BYTES = 10 * 1024 * 1024;
export const MAX_FILE_COUNT = 200;
export const MAX_FILE_BYTES = 2 * 1024 * 1024;
function stable(value) {
if (Array.isArray(value)) return value.map(stable);
if (value && typeof value === 'object') {
return Object.keys(value).sort().reduce((result, key) => {
result[key] = stable(value[key]);
return result;
}, {});
}
return value;
}
export function canonicalJson(value) {
return JSON.stringify(stable(value));
}
function sha256(value) {
return createHash('sha256').update(value).digest('hex');
}
export function packageIntegrityPayload(modulePackage) {
return {
format: MODULE_PACKAGE_FORMAT,
schemaVersion: MODULE_PACKAGE_SCHEMA,
manifest: modulePackage.manifest,
runtime: modulePackage.runtime,
files: modulePackage.files.map(file => ({ path: file.path, sha256: file.sha256, size: file.size })),
};
}
export function calculatePackageDigest(modulePackage) {
return sha256(canonicalJson(packageIntegrityPayload(modulePackage)));
}
function safeRelativePath(value) {
const raw = String(value || '').replace(/\\/g, '/').trim();
const normalized = pathPosix.normalize(raw);
if (!raw || normalized === '.' || normalized.startsWith('../') || normalized.includes('/../') || normalized.startsWith('/') || /^[A-Za-z]:/.test(raw) || raw.includes('\0')) {
throw new PlatformError(`Unsicherer Moduldateipfad: ${raw || '(leer)'}`, {
code: 'MODULE_PACKAGE_PATH_INVALID', status: 400, expose: true,
});
}
return normalized;
}
function validateRuntime(runtime = {}) {
const mode = String(runtime.mode || 'declarative').trim();
if (!['builtin-reference', 'declarative', 'isolated'].includes(mode)) {
throw new PlatformError(`Nicht unterstützter Modul-Runtime-Modus: ${mode}`, { code: 'MODULE_RUNTIME_INVALID', status: 400, expose: true });
}
const entry = runtime.entry === undefined || runtime.entry === null || runtime.entry === '' ? null : safeRelativePath(runtime.entry);
if (mode === 'declarative' && entry) {
throw new PlatformError('Deklarative Module dürfen keinen ausführbaren Einstiegspunkt besitzen.', { code: 'MODULE_RUNTIME_EXECUTABLE_FORBIDDEN', status: 400, expose: true });
}
if (mode === 'isolated' && !entry) {
throw new PlatformError('Isolierte Module benötigen einen Einstiegspunkt.', { code: 'MODULE_RUNTIME_ENTRY_REQUIRED', status: 400, expose: true });
}
return Object.freeze({ mode, entry });
}
function validateFiles(files, runtime) {
if (!Array.isArray(files)) throw new PlatformError('Moduldateien fehlen.', { code: 'MODULE_PACKAGE_FILES_INVALID', status: 400, expose: true });
if (files.length > MAX_FILE_COUNT) throw new PlatformError('Das Modulpaket enthält zu viele Dateien.', { code: 'MODULE_PACKAGE_TOO_MANY_FILES', status: 400, expose: true });
const seen = new Set();
let total = 0;
const normalized = files.map(item => {
if (!item || typeof item !== 'object' || Array.isArray(item)) throw new PlatformError('Ungültiger Moduldateieintrag.', { code: 'MODULE_PACKAGE_FILE_INVALID', status: 400, expose: true });
const path = safeRelativePath(item.path);
if (seen.has(path)) throw new PlatformError(`Doppelte Moduldatei: ${path}`, { code: 'MODULE_PACKAGE_FILE_DUPLICATE', status: 400, expose: true });
seen.add(path);
const content = String(item.content || '');
let buffer;
try { buffer = Buffer.from(content, 'base64'); } catch { buffer = null; }
if (!buffer || buffer.length > MAX_FILE_BYTES) throw new PlatformError(`Moduldatei ist ungültig oder zu groß: ${path}`, { code: 'MODULE_PACKAGE_FILE_TOO_LARGE', status: 400, expose: true });
const digest = sha256(buffer);
if (!/^[a-f0-9]{64}$/.test(String(item.sha256 || '')) || digest !== String(item.sha256)) {
throw new PlatformError(`Prüfsumme stimmt nicht: ${path}`, { code: 'MODULE_PACKAGE_FILE_HASH_MISMATCH', status: 400, expose: true });
}
if (Number(item.size) !== buffer.length) throw new PlatformError(`Dateigröße stimmt nicht: ${path}`, { code: 'MODULE_PACKAGE_FILE_SIZE_MISMATCH', status: 400, expose: true });
total += buffer.length;
if (runtime.mode === 'declarative' && /\.(?:mjs|cjs|js|node|exe|dll|so|dylib|ps1|bat|cmd|sh)$/i.test(path)) {
throw new PlatformError(`Deklaratives Modul enthält ausführbaren Code: ${path}`, { code: 'MODULE_PACKAGE_EXECUTABLE_FORBIDDEN', status: 400, expose: true });
}
return Object.freeze({ path, sha256: digest, size: buffer.length, content });
});
if (total > MAX_PACKAGE_BYTES) throw new PlatformError('Das Modulpaket ist zu groß.', { code: 'MODULE_PACKAGE_TOO_LARGE', status: 400, expose: true });
if (runtime.entry && !seen.has(runtime.entry)) throw new PlatformError('Der Runtime-Einstiegspunkt fehlt im Paket.', { code: 'MODULE_RUNTIME_ENTRY_MISSING', status: 400, expose: true });
return Object.freeze(normalized);
}
function verifyOptionalSignature(modulePackage, trustKeys = {}) {
const signature = modulePackage.signature;
if (!signature) return { signed: false, trusted: false, keyId: null };
const algorithm = String(signature.algorithm || '').toLowerCase();
const keyId = String(signature.keyId || '').trim();
const value = String(signature.value || '').trim();
if (algorithm !== 'ed25519' || !keyId || !value) {
throw new PlatformError('Ungültige Modulsignatur.', { code: 'MODULE_SIGNATURE_INVALID', status: 400, expose: true });
}
const publicKey = trustKeys[keyId];
if (!publicKey) return { signed: true, trusted: false, keyId };
try {
const verified = verifySignature(null, Buffer.from(modulePackage.integrity.digest, 'hex'), createPublicKey(publicKey), Buffer.from(value, 'base64'));
if (!verified) throw new Error('Signaturprüfung fehlgeschlagen');
return { signed: true, trusted: true, keyId };
} catch (error) {
throw new PlatformError('Modulsignatur konnte nicht bestätigt werden.', { code: 'MODULE_SIGNATURE_REJECTED', status: 400, expose: true, cause: error });
}
}
export function validateModulePackage(input, { trustKeys = {}, allowBuiltinReference = false } = {}) {
const source = input && typeof input === 'object' && !Array.isArray(input) ? input : null;
if (!source) throw new PlatformError('Ungültiges Modulpaket.', { code: 'MODULE_PACKAGE_INVALID', status: 400, expose: true });
if (source.format !== MODULE_PACKAGE_FORMAT || Number(source.schemaVersion) !== MODULE_PACKAGE_SCHEMA) {
throw new PlatformError('Nicht unterstütztes Modulpaketformat.', { code: 'MODULE_PACKAGE_VERSION_UNSUPPORTED', status: 400, expose: true });
}
const manifest = validateModuleManifest(source.manifest);
const runtime = validateRuntime(source.runtime);
if (runtime.mode === 'builtin-reference' && !allowBuiltinReference) {
throw new PlatformError('Referenzexporte eingebauter Module können nicht installiert werden.', { code: 'MODULE_BUILTIN_REFERENCE_NOT_INSTALLABLE', status: 400, expose: true });
}
if (manifest.type !== 'extension-host' && runtime.mode !== 'builtin-reference') {
throw new PlatformError('Installierbare Fremdmodule müssen den Typ extension-host verwenden.', { code: 'MODULE_EXTERNAL_TYPE_REQUIRED', status: 400, expose: true });
}
const files = validateFiles(source.files || [], runtime);
const normalized = {
format: MODULE_PACKAGE_FORMAT,
schemaVersion: MODULE_PACKAGE_SCHEMA,
manifest,
runtime,
files,
createdAt: String(source.createdAt || new Date().toISOString()),
integrity: {
algorithm: String(source.integrity?.algorithm || '').toLowerCase(),
digest: String(source.integrity?.digest || '').toLowerCase(),
},
signature: source.signature || null,
};
if (normalized.integrity.algorithm !== 'sha256' || !/^[a-f0-9]{64}$/.test(normalized.integrity.digest)) {
throw new PlatformError('Paketintegrität fehlt oder ist ungültig.', { code: 'MODULE_PACKAGE_INTEGRITY_INVALID', status: 400, expose: true });
}
const expected = calculatePackageDigest(normalized);
if (expected !== normalized.integrity.digest) {
throw new PlatformError('Paketintegrität stimmt nicht.', { code: 'MODULE_PACKAGE_INTEGRITY_MISMATCH', status: 400, expose: true });
}
const trust = verifyOptionalSignature(normalized, trustKeys);
return Object.freeze({ ...normalized, trust: Object.freeze(trust) });
}
export function buildModulePackage({ manifest, runtime = { mode: 'builtin-reference', entry: null }, files = [], createdAt = new Date().toISOString() }) {
const normalizedFiles = files.map(file => {
const path = safeRelativePath(file.path);
const buffer = Buffer.isBuffer(file.content) ? file.content : Buffer.from(String(file.content || ''), 'utf8');
return { path, sha256: sha256(buffer), size: buffer.length, content: buffer.toString('base64') };
});
const draft = { format: MODULE_PACKAGE_FORMAT, schemaVersion: MODULE_PACKAGE_SCHEMA, manifest, runtime, files: normalizedFiles, createdAt, integrity: { algorithm: 'sha256', digest: '' }, signature: null };
draft.integrity.digest = calculatePackageDigest(draft);
return validateModulePackage(draft, { allowBuiltinReference: true });
}
-112
View File
@@ -1,112 +0,0 @@
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'fs';
import { dirname, join, resolve, sep } from 'path';
import { PlatformError } from '../../kernel/errors.mjs';
import { validateModulePackage } from './module-package-contract.mjs';
function ensureInside(root, pathname) {
const base = resolve(root);
const target = resolve(pathname);
if (target !== base && !target.startsWith(`${base}${sep}`)) {
throw new PlatformError('Moduldatei liegt außerhalb des Modulspeichers.', { code: 'MODULE_STORAGE_ESCAPE', status: 400, expose: true });
}
return target;
}
export class ModulePackageStore {
#root;
#trustKeys;
constructor(root, { trustKeys = {} } = {}) {
this.#root = resolve(root);
this.#trustKeys = { ...trustKeys };
mkdirSync(this.#root, { recursive: true });
}
#moduleRoot(id) {
return ensureInside(this.#root, join(this.#root, id));
}
#packagePath(id) {
return join(this.#moduleRoot(id), 'package.vmod');
}
list() {
if (!existsSync(this.#root)) return [];
const result = [];
for (const entry of readdirSync(this.#root, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
try {
const packagePath = this.#packagePath(entry.name);
if (!existsSync(packagePath)) continue;
const parsed = JSON.parse(readFileSync(packagePath, 'utf8'));
const pkg = validateModulePackage(parsed, { trustKeys: this.#trustKeys });
result.push({
id: pkg.manifest.id,
manifest: pkg.manifest,
runtime: pkg.runtime,
trust: pkg.trust,
installedAt: String(parsed.installedAt || parsed.createdAt || ''),
activatable: pkg.runtime.mode === 'declarative' || (pkg.runtime.mode === 'isolated' && pkg.trust.trusted),
packagePath,
});
} catch (error) {
result.push({ id: entry.name, invalid: true, error: error?.message || String(error), activatable: false });
}
}
return result.sort((a, b) => String(a.id).localeCompare(String(b.id)));
}
get(id) {
const packagePath = this.#packagePath(id);
if (!existsSync(packagePath)) return null;
const parsed = JSON.parse(readFileSync(packagePath, 'utf8'));
return validateModulePackage(parsed, { trustKeys: this.#trustKeys });
}
install(input) {
const pkg = validateModulePackage(input, { trustKeys: this.#trustKeys });
const root = this.#moduleRoot(pkg.manifest.id);
if (existsSync(root)) throw new PlatformError('Dieses Fremdmodul ist bereits installiert.', { code: 'MODULE_ALREADY_INSTALLED', status: 409, expose: true });
const temporary = `${root}.${process.pid}.tmp`;
mkdirSync(temporary, { recursive: true });
try {
for (const file of pkg.files) {
const destination = ensureInside(temporary, join(temporary, file.path));
mkdirSync(dirname(destination), { recursive: true });
writeFileSync(destination, Buffer.from(file.content, 'base64'), { mode: 0o600 });
}
const persisted = { ...pkg, trust: undefined, installedAt: new Date().toISOString() };
writeFileSync(join(temporary, 'package.vmod'), `${JSON.stringify(persisted, null, 2)}\n`, { mode: 0o600 });
renameSync(temporary, root);
} catch (error) {
rmSync(temporary, { recursive: true, force: true });
throw error;
}
return this.describe(pkg.manifest.id);
}
remove(id) {
const root = this.#moduleRoot(id);
if (!existsSync(root)) return false;
rmSync(root, { recursive: true, force: true });
return true;
}
export(id) {
const packagePath = this.#packagePath(id);
if (!existsSync(packagePath)) throw new PlatformError('Installiertes Modulpaket nicht gefunden.', { code: 'MODULE_NOT_FOUND', status: 404, expose: true });
return readFileSync(packagePath);
}
describe(id) {
const pkg = this.get(id);
if (!pkg) return null;
return {
id: pkg.manifest.id,
manifest: pkg.manifest,
runtime: pkg.runtime,
trust: pkg.trust,
activatable: pkg.runtime.mode === 'declarative' || (pkg.runtime.mode === 'isolated' && pkg.trust.trusted),
};
}
}
-87
View File
@@ -1,87 +0,0 @@
import { mkdirSync, readFileSync, renameSync, writeFileSync, existsSync } from 'fs';
import { dirname } from 'path';
import { PlatformError } from '../../kernel/errors.mjs';
const SCHEMA_VERSION = 1;
function normalizeState(input = {}) {
const modules = {};
const source = input && typeof input === 'object' && !Array.isArray(input) ? input.modules : null;
if (source && typeof source === 'object' && !Array.isArray(source)) {
for (const [id, record] of Object.entries(source)) {
if (!/^vendoo\.[a-z][a-z0-9-]*$/.test(id)) continue;
if (!record || typeof record !== 'object' || Array.isArray(record)) continue;
modules[id] = {
enabled: record.enabled !== false,
source: String(record.source || 'admin'),
updatedAt: String(record.updatedAt || new Date(0).toISOString()),
};
}
}
return { schemaVersion: SCHEMA_VERSION, modules };
}
export class ModuleStateStore {
#path;
#state;
constructor(pathname) {
this.#path = pathname;
this.#state = this.#read();
}
#read() {
try {
if (!existsSync(this.#path)) return normalizeState();
const parsed = JSON.parse(readFileSync(this.#path, 'utf8'));
if (Number(parsed?.schemaVersion || 0) !== SCHEMA_VERSION) {
throw new Error(`Nicht unterstützte Modulstatus-Version: ${parsed?.schemaVersion}`);
}
return normalizeState(parsed);
} catch (error) {
throw new PlatformError('Der persistente Modulstatus konnte nicht gelesen werden.', {
code: 'MODULE_STATE_READ_FAILED',
cause: error,
details: { path: this.#path },
});
}
}
#persist() {
mkdirSync(dirname(this.#path), { recursive: true });
const temporary = `${this.#path}.${process.pid}.tmp`;
writeFileSync(temporary, `${JSON.stringify(this.#state, null, 2)}\n`, { mode: 0o600 });
renameSync(temporary, this.#path);
}
has(id) { return Boolean(this.#state.modules[id]); }
isEnabled(id, fallback = true) {
const record = this.#state.modules[id];
return record ? record.enabled !== false : Boolean(fallback);
}
setEnabled(id, enabled, { source = 'admin' } = {}) {
if (!/^vendoo\.[a-z][a-z0-9-]*$/.test(String(id || ''))) {
throw new PlatformError('Ungültige Modul-ID für den Modulstatus.', { code: 'MODULE_ID_INVALID', status: 400, expose: true });
}
this.#state.modules[id] = {
enabled: Boolean(enabled),
source: String(source || 'admin').slice(0, 80),
updatedAt: new Date().toISOString(),
};
this.#persist();
return { id, ...this.#state.modules[id] };
}
remove(id) {
if (!this.#state.modules[id]) return false;
delete this.#state.modules[id];
this.#persist();
return true;
}
snapshot() {
return JSON.parse(JSON.stringify(this.#state));
}
}
-34
View File
@@ -1,34 +0,0 @@
import manifest from './module.json' with { type: 'json' };
let runtime = null;
let started = false;
export function configureAiModule(deps = {}) {
runtime = {
startModelJobs: deps.startModelJobs || (() => {}),
stopModelJobs: deps.stopModelJobs || (() => {}),
startBatchJobs: deps.startBatchJobs || (() => {}),
stopBatchJobs: deps.stopBatchJobs || (() => {}),
};
}
export function createAiModule() {
return {
manifest,
source: 'builtin',
lifecycle: {
async start() {
if (!runtime) throw new Error('AI-Modul ist nicht konfiguriert.');
runtime.startModelJobs();
runtime.startBatchJobs();
started = true;
},
async stop() {
runtime?.stopModelJobs();
runtime?.stopBatchJobs();
started = false;
},
async health() { return { ok: Boolean(runtime && started), native: true, workers: ['model-photos', 'image-batch'] }; },
},
};
}
-28
View File
@@ -1,28 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.ai",
"name": "AI Services",
"version": "1.0.0",
"type": "feature",
"status": "native",
"description": "Native AI-Services für Text-, Modellfoto- und Batch-Jobs mit kontrolliertem Worker-Lifecycle.",
"requires": [
"vendoo.security",
"vendoo.media"
],
"permissions": [
"generation.execute"
],
"provides": [
"ai.text",
"ai.images"
],
"consumes": [
"media.assets",
"security.policies"
],
"owns": [
"ai.providers",
"ai.jobs"
]
}
-32
View File
@@ -1,32 +0,0 @@
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { PlatformError } from '../../kernel/errors.mjs';
const here = dirname(fileURLToPath(import.meta.url));
const manifest = JSON.parse(readFileSync(join(here, 'module.json'), 'utf8'));
let service = null;
let setupTokenMatcher = () => false;
export function configureAuthModule({ setupTokenMatches }) {
if (typeof setupTokenMatches === 'function') setupTokenMatcher = setupTokenMatches;
}
export function getAuthService() {
if (!service) throw new PlatformError('Auth-Modul ist noch nicht gestartet.', { code: 'AUTH_MODULE_NOT_READY', status: 503, expose: true });
return service;
}
export function createAuthModule() {
return {
manifest,
lifecycle: {
async start() {
const { createAuthService } = await import('./service.mjs');
service = createAuthService({ setupTokenMatches: setupTokenMatcher });
},
async stop() { service = null; },
async health() { return getAuthService().health(); },
},
};
}
-14
View File
@@ -1,14 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.auth",
"name": "Authentication",
"version": "2.0.0",
"type": "core",
"status": "native",
"description": "Native Authentifizierung mit Setup, Login, Einladungen, Passwortregeln und sicheren Sitzungsübergaben.",
"requires": ["vendoo.security"],
"permissions": ["sessions.manage"],
"provides": ["auth.identity", "auth.tokens"],
"consumes": ["security.policies"],
"owns": ["auth.tokens", "auth.login-history"]
}
-67
View File
@@ -1,67 +0,0 @@
import { objectSchema } from '../../kernel/input-schema.mjs';
import { getAuthService } from './index.mjs';
const emailRule = { type: 'string', required: true, minLength: 3, maxLength: 254, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ };
export function registerPublicAuthRoutes({ app, routes, deps }) {
routes.register(app, {
id: 'auth.setup-status.read', method: 'GET', path: '/auth/setup-check', owner: 'vendoo.auth', policy: 'auth.public',
csrf: false, stability: 'stable',
handler: async (_req, res) => res.json(getAuthService().setupStatus({ mailConfigured: deps.isMailerConfigured(), setupTokenRequired: deps.setupTokenRequired })),
});
routes.register(app, {
id: 'auth.setup.create', method: 'POST', path: '/auth/setup', owner: 'vendoo.auth', policy: 'auth.public', csrf: false, stability: 'stable',
input: objectSchema({ email: emailRule, name: { type: 'string', maxLength: 120 }, password: { type: 'string', required: true, maxLength: 512, trim: false }, setup_token: { type: 'string', maxLength: 512, trim: false } }),
handler: async (req, res) => {
const result = getAuthService().setup({ ...req.validatedBody, setupToken: req.get('x-vendoo-setup-token') || req.validatedBody.setup_token, ip: deps.getClientIp(req), userAgent: req.get('user-agent') || '' });
deps.setSessionCookies(req, res, result.session, result.csrf);
res.json({ ok: true, redirect: '/' });
},
});
routes.register(app, {
id: 'auth.login.create', method: 'POST', path: '/auth/login', owner: 'vendoo.auth', policy: 'auth.public', csrf: false, stability: 'stable',
input: objectSchema({ email: emailRule, password: { type: 'string', required: true, maxLength: 512, trim: false } }),
handler: async (req, res) => {
const result = getAuthService().login({ ...req.validatedBody, ip: deps.getClientIp(req), userAgent: req.get('user-agent') || '' });
deps.setSessionCookies(req, res, result.session, result.csrf);
res.json({ ok: true, redirect: '/' });
},
});
routes.register(app, {
id: 'auth.invite.create', method: 'POST', path: '/auth/invite', owner: 'vendoo.auth', policy: 'auth.authenticated',
middleware: [deps.requireAuth, deps.csrfProtect], csrf: true, stability: 'stable',
input: objectSchema({ email: emailRule, name: { type: 'string', maxLength: 120 }, role: { type: 'string', enum: ['admin','manager','editor','publisher','viewer'] } }),
handler: async (req, res) => {
const { token } = getAuthService().createInvite({ email: req.validatedBody.email, role: req.validatedBody.role || 'editor', createdBy: req.user.id });
const result = await deps.sendMagicLink(req.validatedBody.email, token, `${req.protocol}://${req.get('host')}`, 'invite');
deps.auditLog(req.user.id, req.user.email, 'user.invited', 'user', req.validatedBody.email, `Rolle: ${req.validatedBody.role || 'editor'}`, deps.getClientIp(req));
res.json({ ok: true, message: 'Einladung gesendet!', consoleFallback: result.consoleFallback || false });
},
});
routes.register(app, {
id: 'auth.magic.redirect', method: 'GET', path: '/auth/magic', owner: 'vendoo.auth', policy: 'auth.public', csrf: false, stability: 'stable',
handler: async (req, res) => {
const token = String(req.query?.token || '');
if (!token) return res.redirect('/login.html?error=' + encodeURIComponent('Kein Token angegeben'));
res.redirect(`/login.html?invite=${encodeURIComponent(token)}`);
},
});
routes.register(app, {
id: 'auth.invite.accept', method: 'POST', path: '/auth/accept-invite', owner: 'vendoo.auth', policy: 'auth.public', csrf: false, stability: 'stable',
input: objectSchema({ token: { type: 'string', required: true, minLength: 16, maxLength: 512, trim: false }, password: { type: 'string', required: true, maxLength: 512, trim: false }, name: { type: 'string', maxLength: 120 } }),
handler: async (req, res) => {
const result = getAuthService().acceptInvite({ ...req.validatedBody, ip: deps.getClientIp(req), userAgent: req.get('user-agent') || '' });
deps.setSessionCookies(req, res, result.session, result.csrf);
res.json({ ok: true, redirect: '/' });
},
});
routes.register(app, {
id: 'auth.logout.create', method: 'POST', path: '/auth/logout', owner: 'vendoo.auth', policy: 'auth.authenticated',
middleware: [deps.requireAuth, deps.csrfProtect], csrf: true, stability: 'stable',
handler: async (req, res) => {
getAuthService().logout({ token: req.cookies?.auth_token, ip: deps.getClientIp(req) });
deps.clearSessionCookies(res);
res.json({ ok: true });
},
});
}
-100
View File
@@ -1,100 +0,0 @@
import {
auditLog, checkRateLimit, clearFailedLogins, createAuthToken, createSession, createUser,
generateCsrfToken, getUserByEmail, isSetupMode, isUserLocked, recordLogin,
registerFailedLogin, setPassword, updateUser, validateAuthToken, validatePasswordStrength, verifyPassword,
validateSession, destroySession,
} from '../../core/identity/identity-store.mjs';
import { PlatformError } from '../../kernel/errors.mjs';
function fail(message, status = 400, code = 'AUTH_FAILED', details = null) {
throw new PlatformError(message, { status, code, expose: true, details });
}
export function createAuthService({ setupTokenMatches }) {
return Object.freeze({
setupStatus({ mailConfigured, setupTokenRequired }) {
return { setupMode: isSetupMode(), mailConfigured: Boolean(mailConfigured), setupTokenRequired: Boolean(setupTokenRequired) };
},
setup({ email, name, password, setupToken, ip, userAgent }) {
if (!isSetupMode()) fail('Setup bereits abgeschlossen', 400, 'SETUP_COMPLETED');
if (!setupTokenMatches(setupToken)) fail('Ungültiger Einrichtungs-Code.', 403, 'SETUP_TOKEN_INVALID');
const passwordCheck = validatePasswordStrength(password);
if (!passwordCheck.valid) fail(`Passwort benötigt ${passwordCheck.problems.join(', ')}.`, 400, 'PASSWORD_WEAK');
try {
const user = createUser(email, name || email.split('@')[0], 'admin', null, password);
auditLog(user.id, email, 'user.created', 'user', String(user.id), 'Erster Admin (Setup)', ip);
const session = createSession(user.id, ip, userAgent);
recordLogin(user.id, ip, userAgent, true);
auditLog(user.id, email, 'login.success', null, null, 'via setup', ip);
return { user, session, csrf: generateCsrfToken() };
} catch (error) {
fail(error?.message || 'Setup fehlgeschlagen.', 500, 'SETUP_FAILED');
}
},
login({ email, password, ip, userAgent }) {
const rl = checkRateLimit(`login:${ip}`, 5, 15 * 60 * 1000);
if (!rl.allowed) fail(`Zu viele Versuche. Bitte warte ${rl.retryAfter} Sekunden.`, 429, 'LOGIN_RATE_LIMIT', { retry_after: rl.retryAfter });
const user = getUserByEmail(email);
if (user && isUserLocked(user)) {
auditLog(user.id, email, 'login.blocked_locked', null, null, user.locked_until, ip);
fail('Account nach mehreren Fehlversuchen vorübergehend gesperrt.', 423, 'ACCOUNT_LOCKED', { locked_until: user.locked_until });
}
if (!user || !verifyPassword(password, user.password_hash)) {
if (user) {
recordLogin(user.id, ip, userAgent, false);
const lockResult = registerFailedLogin(user.id);
if (lockResult?.locked) {
auditLog(user.id, email, 'login.account_locked', 'user', String(user.id), lockResult.lockedUntil, ip);
fail('Zu viele Fehlversuche. Account für 15 Minuten gesperrt.', 423, 'ACCOUNT_LOCKED', { locked_until: lockResult.lockedUntil });
}
}
auditLog(user?.id || null, email, 'login.failed', null, null, null, ip);
fail('E-Mail oder Passwort falsch', 401, 'LOGIN_INVALID');
}
if (!user.active) fail('Account deaktiviert', 403, 'ACCOUNT_DISABLED');
clearFailedLogins(user.id);
const session = createSession(user.id, ip, userAgent);
recordLogin(user.id, ip, userAgent, true);
auditLog(user.id, email, 'login.success', null, null, 'via password', ip);
return { user, session, csrf: generateCsrfToken() };
},
createInvite({ email, role, createdBy }) {
return createAuthToken(email, 'invite', role || 'editor', createdBy);
},
acceptInvite({ token, password, name, ip, userAgent }) {
const passwordCheck = validatePasswordStrength(password);
if (!passwordCheck.valid) fail(`Passwort benötigt ${passwordCheck.problems.join(', ')}.`, 400, 'PASSWORD_WEAK');
const authToken = validateAuthToken(token);
if (!authToken) fail('Link ungültig oder abgelaufen', 400, 'INVITE_INVALID');
let user = getUserByEmail(authToken.email);
if (!user) {
user = createUser(authToken.email, name || authToken.email.split('@')[0], authToken.role, authToken.created_by, password);
auditLog(user.id, user.email, 'user.activated', 'user', String(user.id), null, ip);
} else {
setPassword(user.id, password);
if (name) user = updateUser(user.id, { name });
}
if (!user.active) fail('Account deaktiviert', 403, 'ACCOUNT_DISABLED');
const session = createSession(user.id, ip, userAgent);
recordLogin(user.id, ip, userAgent, true);
auditLog(user.id, user.email, 'login.success', null, null, 'via invite', ip);
return { user, session, csrf: generateCsrfToken() };
},
logout({ token, ip }) {
if (!token) return { ok: true };
const session = validateSession(token);
if (session) auditLog(session.user_id, session.email, 'logout', null, null, null, ip);
destroySession(token);
return { ok: true };
},
health() {
return { ok: true, setup_mode: isSetupMode() };
},
});
}
-38
View File
@@ -1,38 +0,0 @@
import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { createAuthModule } from './auth/index.mjs';
import { createSecurityModule } from './security/index.mjs';
import { createSessionsModule } from './sessions/index.mjs';
import { createSettingsModule } from './settings/index.mjs';
import { createListingsModule } from './listings/index.mjs';
import { createInventoryModule } from './inventory/index.mjs';
import { createMediaModule } from './media/index.mjs';
import { createThemesModule } from './themes/index.mjs';
import { createUsersModule } from './users/index.mjs';
import { createModuleManagerModule } from './module-manager/index.mjs';
import { createAiModule } from './ai/index.mjs';
import { createFluxStudioModule } from './flux-studio/index.mjs';
import { 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(), createFluxStudioModule(), createQualityModule(), createPublishingModule(), createModuleManagerModule(), createDeploymentsModule(),
];
}
-28
View File
@@ -1,28 +0,0 @@
import manifest from './module.json' with { type: 'json' };
import { PlatformError } from '../../kernel/errors.mjs';
import { createDeploymentService } from './service.mjs';
let service = null;
let adapters = {};
export function configureDeploymentsModule(nextAdapters = {}) {
adapters = { ...adapters, ...nextAdapters };
}
export function getDeploymentService() {
if (!service) throw new PlatformError('Deployment-Modul ist noch nicht gestartet.', {
code: 'DEPLOYMENT_MODULE_NOT_READY', status: 503, expose: true,
});
return service;
}
export function createDeploymentsModule() {
return {
manifest,
lifecycle: {
async start() { service = createDeploymentService(adapters); },
async stop() { service = null; },
async health() { return getDeploymentService().health(); },
},
};
}
-34
View File
@@ -1,34 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.deployments",
"name": "Deployments & Updates",
"version": "1.0.0",
"type": "core",
"status": "native",
"description": "Kontrollierte Coolify-Deployments, verschlüsselte Deployment-Zugangsdaten und sichere Web-Updates ohne Docker-Socket.",
"requires": [
"vendoo.security",
"vendoo.settings",
"vendoo.operations",
"vendoo.module-manager"
],
"permissions": [
"updates.manage"
],
"provides": [
"deployments.control",
"deployments.coolify",
"updates.web"
],
"consumes": [
"security.secrets",
"settings.configuration",
"operations.backup",
"modules.management"
],
"owns": [
"deployments.configuration",
"deployments.history",
"updates.coolify"
]
}
-62
View File
@@ -1,62 +0,0 @@
import { objectSchema } from '../../kernel/input-schema.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 },
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 },
});
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(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) => res.json(await getDeploymentService().checkUpdates()),
});
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 result = await getDeploymentService().trigger(req.validatedBody || {});
deps.auditLog(req.user.id, req.user.email, 'deployment.triggered', 'deployment', 'coolify', JSON.stringify({ backup_id: result.backup_id, branch: result.github_branch }), deps.getClientIp(req));
res.status(202).json(result);
},
});
}
-244
View File
@@ -1,244 +0,0 @@
import { mkdirSync, existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { readRuntimeConfig, setConfigValue, writeRuntimeConfig } from '../../../lib/config-store.mjs';
import { getSecret, getSecretMetadata, hasSecret, setSecret } from '../../../lib/secret-store.mjs';
import { OPERATIONS_DIR, APP_VERSION } from '../../../lib/runtime-paths.mjs';
import { PlatformError } from '../../kernel/errors.mjs';
const LAST_DEPLOYMENT_PATH = join(OPERATIONS_DIR, 'last-deployment-request.json');
const PROVIDERS = new Set(['none', 'coolify']);
const MODES = new Set(['webhook', 'api']);
const CHANNELS = new Set(['stable', 'beta']);
const METHODS = new Set(['GET', 'POST']);
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 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 readLastDeployment() {
try { return existsSync(LAST_DEPLOYMENT_PATH) ? JSON.parse(readFileSync(LAST_DEPLOYMENT_PATH, 'utf8')) : null; }
catch { return null; }
}
function writeLastDeployment(value) {
mkdirSync(dirname(LAST_DEPLOYMENT_PATH), { recursive: true });
const temporary = `${LAST_DEPLOYMENT_PATH}.${process.pid}.tmp`;
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
renameSync(temporary, LAST_DEPLOYMENT_PATH);
}
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}'),
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: /^(1|true|yes)$/i.test(String(process.env.VENDOO_DEPLOY_BACKUP_UPLOADS || 'false')),
api_token: getSecretMetadata('COOLIFY_API_TOKEN'),
deploy_webhook: getSecretMetadata('COOLIFY_DEPLOY_WEBHOOK_URL'),
};
}
function publicSettings() {
const settings = currentSettings();
const configured = 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)
);
return {
...settings,
configured,
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,
last_deployment: readLastDeployment(),
docker_socket_access: false,
};
}
function buildApiDeployUrl(settings, force) {
const endpoint = settings.deploy_endpoint || '/api/v1/deploy?uuid={uuid}&force={force}';
if (!endpoint.startsWith('/')) fail('Coolify API-Endpunkt muss mit / beginnen.');
if (/\r|\n|\0/.test(endpoint) || endpoint.includes('..')) fail('Coolify API-Endpunkt ist ungültig.');
const path = endpoint
.replaceAll('{uuid}', encodeURIComponent(settings.resource_uuid))
.replaceAll('{force}', force ? 'true' : 'false');
return `${settings.coolify_url}${path}`;
}
async function fetchWithTimeout(url, options = {}, timeoutMs = 15000) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try { return await fetch(url, { ...options, signal: controller.signal, redirect: 'error' }); }
catch (error) {
if (error?.name === 'AbortError') fail('Coolify-Anfrage hat das Zeitlimit überschritten.', 'COOLIFY_TIMEOUT', 504);
fail(`Coolify ist nicht erreichbar: ${error.message}`, 'COOLIFY_UNREACHABLE', 502);
} finally { clearTimeout(timeout); }
}
async function readSafeResponse(response) {
const text = (await response.text()).slice(0, 4000);
try { return text ? JSON.parse(text) : null; } catch { return text || null; }
}
export function createDeploymentService({ createBackup, checkForUpdates } = {}) {
if (typeof createBackup !== 'function') fail('Backup-Adapter fehlt.', 'DEPLOYMENT_ADAPTER_MISSING', 500);
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 response = await fetchWithTimeout(`${settings.coolify_url}/api/health`, { headers: { Accept: 'application/json' } }, 10000);
const body = await readSafeResponse(response);
if (!response.ok) fail(`Coolify Healthcheck antwortet mit HTTP ${response.status}.`, 'COOLIFY_HEALTH_FAILED', 502, { response: body });
return { ok: true, status: response.status, response: body, url: settings.coolify_url };
}
async function trigger({ force = false, confirmation = '', reason = 'manual' } = {}) {
if (confirmation !== 'DEPLOY') fail('Zur Bestätigung muss exakt DEPLOY übermittelt werden.', 'DEPLOYMENT_CONFIRMATION_REQUIRED');
const settings = currentSettings();
if (settings.provider !== 'coolify') fail('Coolify ist nicht als Deployment-Provider aktiviert.');
let backup = null;
if (settings.backup_before_deploy) {
backup = await createBackup({
kind: 'manual', includeUploads: settings.backup_uploads, includeConfig: true,
label: `pre-deploy-${APP_VERSION}`,
});
}
let url;
const headers = { Accept: 'application/json', 'User-Agent': `Vendoo/${APP_VERSION}` };
let 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 requestedAt = new Date().toISOString();
const response = await fetchWithTimeout(url, { method, headers }, 20000);
const body = await readSafeResponse(response);
const record = {
requested_at: requestedAt,
provider: 'coolify', trigger_mode: settings.trigger_mode, method,
resource_uuid: settings.resource_uuid || null,
github_repository: settings.github_repository, github_branch: settings.github_branch,
version: APP_VERSION, force: Boolean(force), reason: clean(reason, 120),
accepted: response.ok, http_status: response.status,
backup_id: backup?.id || null, backup_file: backup?.filename || null,
response: body,
};
writeLastDeployment(record);
if (!response.ok) fail(`Coolify hat den Deployment-Auftrag mit HTTP ${response.status} abgewiesen.`, 'COOLIFY_DEPLOY_REJECTED', 502, { response: body, backup_id: backup?.id || null });
return { ...record, message: 'Coolify-Deployment wurde angefordert. Der laufende Container wird nicht selbst verändert.' };
}
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: clean(input.deploy_endpoint || '/api/v1/deploy?uuid={uuid}&force={force}', 500),
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',
};
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 !== '••••••••') {
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 !== '••••••••') {
setSecret('COOLIFY_DEPLOY_WEBHOOK_URL', normalizeWebhookUrl(input.deploy_webhook_url), { source: 'deployment-ui' });
}
return publicSettings();
}
return Object.freeze({
status: publicSettings,
update,
testConnection,
trigger,
async checkUpdates() {
if (typeof checkForUpdates !== 'function') return { configured: false, current_version: APP_VERSION, message: 'Updateprüfung ist nicht verfügbar.' };
return checkForUpdates();
},
health() {
const status = publicSettings();
return { ok: true, provider: status.provider, configured: status.configured, docker_socket_access: false };
},
});
}
-20
View File
@@ -1,20 +0,0 @@
import manifest from './module.json' with { type: 'json' };
let runtime = null;
let started = false;
export function configureFluxStudioModule(deps = {}) {
runtime = { start: deps.start || (() => {}), stop: deps.stop || (() => {}) };
}
export function createFluxStudioModule() {
return {
manifest,
source: 'builtin',
lifecycle: {
async start() { if (!runtime) throw new Error('FLUX-Modul ist nicht konfiguriert.'); runtime.start(); started = true; },
async stop() { runtime?.stop(); started = false; },
async health() { return { ok: Boolean(runtime && started), native: true, worker: 'flux-prompt' }; },
},
};
}
-25
View File
@@ -1,25 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.flux-studio",
"name": "FLUX Studio",
"version": "1.0.0",
"type": "feature",
"status": "native",
"description": "Native FLUX-Studio-Verträge und kontrollierter Prompt-Job-Worker.",
"requires": [
"vendoo.ai"
],
"permissions": [
"generation.execute"
],
"provides": [
"flux.prompt-jobs"
],
"consumes": [
"ai.images"
],
"owns": [
"flux.generations",
"flux.history"
]
}
-6
View File
@@ -1,6 +0,0 @@
import manifest from './module.json' with { type:'json' };
import { InventoryService } from './service.mjs';
let service = null;
export function configureInventoryModule({ repository }) { service = new InventoryService(repository); }
export function getInventoryService(){ if (!service) throw new Error('Inventory-Modul ist nicht konfiguriert.'); return service; }
export function createInventoryModule(){ return { manifest, lifecycle:{ async start(){}, async stop(){}, async health(){ return {ok:Boolean(service),native:true,contracts:['locations','bulk-update']}; } } }; }
-27
View File
@@ -1,27 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.inventory",
"name": "Inventory",
"version": "1.0.0",
"type": "feature",
"status": "native",
"description": "Native Lagerort- und Bestandsverwaltung mit begrenzten Bulk-Aktionen.",
"requires": [
"vendoo.auth",
"vendoo.listings"
],
"permissions": [
"listings.view",
"inventory.edit"
],
"provides": [
"inventory.stock"
],
"consumes": [
"auth.identity",
"listings.catalog"
],
"owns": [
"inventory.locations"
]
}
-2
View File
@@ -1,2 +0,0 @@
import { getStorageLocations, getListingsByLocation, getUnassignedListings, bulkUpdateLocation } from '../../../lib/db.mjs';
export const inventoryRepository = Object.freeze({ locations:getStorageLocations, byLocation:getListingsByLocation, unassigned:getUnassignedListings, bulkUpdateLocation });
-7
View File
@@ -1,7 +0,0 @@
import { getInventoryService } from './index.mjs';
export function registerInventoryRoutes({app,routes,deps}){ const service=getInventoryService();
routes.register(app,{id:'inventory.locations.list',method:'GET',path:'/api/inventory/locations',owner:'vendoo.inventory',policy:'inventory.view',csrf:false,stability:'stable',handler:async(_q,r)=>r.json(service.locations())});
routes.register(app,{id:'inventory.unassigned.list',method:'GET',path:'/api/inventory/unassigned',owner:'vendoo.inventory',policy:'inventory.view',csrf:false,stability:'stable',handler:async(_q,r)=>r.json(service.unassigned())});
routes.register(app,{id:'inventory.location.read',method:'GET',path:'/api/inventory/location/:loc',owner:'vendoo.inventory',policy:'inventory.view',csrf:false,stability:'stable',handler:async(q,r)=>r.json(service.byLocation(q.params.loc))});
routes.register(app,{id:'inventory.location.bulk-update',method:'PUT',path:'/api/inventory/bulk-location',owner:'vendoo.inventory',policy:'inventory.manage',stability:'stable',input:body=>service.validateBulk(body),handler:async(q,r)=>{const result=service.bulk(q.validatedBody);deps.auditLog(q.user.id,q.user.email,'inventory.location_bulk_updated','listing',null,JSON.stringify({count:result.updated,location:q.validatedBody.location}),deps.getClientIp(q));r.json(result);}});
}
-10
View File
@@ -1,10 +0,0 @@
import { PlatformError } from '../../kernel/errors.mjs';
function cleanLocation(value){ const text=String(value ?? '').trim(); if(text.length>160 || /[\u0000\r\n]/.test(text)) throw new PlatformError('Lagerort ist ungültig.',{code:'INVENTORY_LOCATION_INVALID',status:400,expose:true}); return text; }
export class InventoryService {
constructor(repository) { this.repository = repository; }
locations(){ return this.repository.locations(); }
unassigned(){ return this.repository.unassigned(); }
byLocation(location){ return this.repository.byLocation(cleanLocation(location)); }
validateBulk(input={}){ for(const key of Object.keys(input||{})) if(!['ids','location'].includes(key)) throw new PlatformError(`Unbekanntes Lagerfeld: ${key}`,{code:'INVENTORY_INPUT_INVALID',status:400,expose:true}); const ids=Array.isArray(input.ids)?[...new Set(input.ids.map(Number).filter(Number.isInteger).filter(id=>id>0))]:[]; if(!ids.length) throw new PlatformError('Mindestens eine Artikel-ID ist erforderlich.',{code:'INVENTORY_IDS_REQUIRED',status:400,expose:true}); if(ids.length>500) throw new PlatformError('Maximal 500 Artikel pro Vorgang.',{code:'INVENTORY_BATCH_TOO_LARGE',status:400,expose:true}); return Object.freeze({ ids, location:cleanLocation(input.location) }); }
bulk(input){ const valid=this.validateBulk(input); return { ok:true, updated:this.repository.bulkUpdateLocation(valid.ids, valid.location) }; }
}
-6
View File
@@ -1,6 +0,0 @@
import manifest from './module.json' with { type: 'json' };
import { ListingsService } from './service.mjs';
let service = null;
export function configureListingsModule({ repository }) { service = new ListingsService(repository); }
export function getListingsService() { if (!service) throw new Error('Listings-Modul ist nicht konfiguriert.'); return service; }
export function createListingsModule() { return { manifest, lifecycle: { async start(){}, async stop(){}, async health(){ return { ok:Boolean(service), native:true, contracts:['catalog','trash'] }; } } }; }
-30
View File
@@ -1,30 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.listings",
"name": "Listings",
"version": "2.0.0",
"type": "feature",
"status": "native",
"description": "Native Artikelverwaltung mit validierten Payloads, Papierkorb, Duplikaten und Audit-Verträgen.",
"requires": [
"vendoo.auth",
"vendoo.media"
],
"permissions": [
"listings.view",
"listings.edit",
"listings.delete",
"listings.permanent-delete"
],
"provides": [
"listings.catalog"
],
"consumes": [
"media.assets",
"auth.identity"
],
"owns": [
"listings.records",
"listings.trash"
]
}
-18
View File
@@ -1,18 +0,0 @@
import {
createListing, getListing, getListings, updateListing, softDeleteListing,
restoreListing, getDeletedListings, permanentlyDeleteListing, getTrashCount,
duplicateListing,
} from '../../../lib/db.mjs';
export const listingsRepository = Object.freeze({
create: createListing,
get: getListing,
list: getListings,
update: updateListing,
softDelete: softDeleteListing,
restore: restoreListing,
listDeleted: getDeletedListings,
permanentDelete: permanentlyDeleteListing,
trashCount: getTrashCount,
duplicate: duplicateListing,
});
-19
View File
@@ -1,19 +0,0 @@
import { getListingsService } from './index.mjs';
export function registerListingsRoutes({ app, routes, deps }) {
const service = getListingsService();
routes.register(app, { id:'listings.catalog.create', method:'POST', path:'/api/listings', owner:'vendoo.listings', policy:'listings.edit', stability:'stable', input: body => service.validateCreate(body),
handler: async (req,res) => { const item = service.create(req.validatedBody, req.user.id); deps.auditLog(req.user.id, req.user.email, 'listing.created', 'listing', String(item.id), item.title, deps.getClientIp(req)); res.json(item); } });
routes.register(app, { id:'listings.catalog.list', method:'GET', path:'/api/listings', owner:'vendoo.listings', policy:'listings.view', csrf:false, stability:'stable',
handler: async (req,res) => res.json(service.list(req.query || {})) });
routes.register(app, { id:'listings.catalog.read', method:'GET', path:'/api/listings/:id', owner:'vendoo.listings', policy:'listings.view', csrf:false, stability:'stable', handler: async (req,res)=>res.json(service.get(req.params.id)) });
routes.register(app, { id:'listings.catalog.update', method:'PUT', path:'/api/listings/:id', owner:'vendoo.listings', policy:'listings.edit', stability:'stable', input: body => service.validateUpdate(Object.fromEntries(Object.entries(body || {}).filter(([k]) => k !== '_edit_lock'))),
handler: async (req,res) => { const lock = deps.validateResourceLock({ resourceType:'listing', resourceId:req.params.id, userId:req.user.id, token:req.get('x-edit-lock') || req.body?._edit_lock }); if (!lock.valid) return res.status(409).json({ error:`${lock.lock?.user_name || lock.lock?.user_email || 'Ein anderer Benutzer'} bearbeitet diesen Artikel gerade.`, code:'EDIT_LOCKED', lock:lock.lock }); const item=service.update(req.params.id, req.validatedBody, req.user.id); deps.auditLog(req.user.id, req.user.email, 'listing.updated', 'listing', String(item.id), item.title, deps.getClientIp(req)); res.json(item); } });
routes.register(app, { id:'listings.catalog.delete', method:'DELETE', path:'/api/listings/:id', owner:'vendoo.listings', policy:'listings.delete', stability:'stable', handler: async (req,res)=>{ const result=service.softDelete(req.params.id); deps.auditLog(req.user.id, req.user.email, 'listing.soft_deleted', 'listing', req.params.id, null, deps.getClientIp(req)); res.json(result); } });
routes.register(app, { id:'listings.catalog.duplicate', method:'POST', path:'/api/listings/:id/duplicate', owner:'vendoo.listings', policy:'listings.edit', stability:'stable', handler: async (req,res)=>{ const item=service.duplicate(req.params.id); deps.auditLog(req.user.id, req.user.email, 'listing.duplicated', 'listing', String(item.id), item.title, deps.getClientIp(req)); res.json(item); } });
routes.register(app, { id:'listings.trash.list', method:'GET', path:'/api/trash', owner:'vendoo.listings', policy:'listings.view', csrf:false, stability:'stable', handler:async(_req,res)=>res.json(service.trash()) });
routes.register(app, { id:'listings.trash.count', method:'GET', path:'/api/trash/count', owner:'vendoo.listings', policy:'listings.view', csrf:false, stability:'stable', handler:async(_req,res)=>res.json(service.trashCount()) });
routes.register(app, { id:'listings.trash.restore', method:'POST', path:'/api/trash/:id/restore', owner:'vendoo.listings', policy:'listings.edit', stability:'stable', handler:async(req,res)=>{ const result=service.restore(req.params.id); deps.auditLog(req.user.id, req.user.email, 'listing.restored', 'listing', req.params.id, null, deps.getClientIp(req)); res.json(result); } });
routes.register(app, { id:'listings.trash.permanent-delete', method:'DELETE', path:'/api/trash/:id', owner:'vendoo.listings', policy:'listings.permanent-delete', stability:'stable', handler:async(req,res)=>{ const result=service.permanentDelete(req.params.id); deps.auditLog(req.user.id, req.user.email, 'listing.permanently_deleted', 'listing', req.params.id, null, deps.getClientIp(req)); res.json(result); } });
routes.register(app, { id:'listings.trash.empty', method:'DELETE', path:'/api/trash', owner:'vendoo.listings', policy:'listings.permanent-delete', stability:'stable', handler:async(req,res)=>{ const result=service.emptyTrash(); deps.auditLog(req.user.id, req.user.email, 'trash.emptied', null, null, `${result.deleted} Einträge`, deps.getClientIp(req)); res.json(result); } });
}
-68
View File
@@ -1,68 +0,0 @@
import { PlatformError } from '../../kernel/errors.mjs';
import { sanitizeRichHtml } from '../../../lib/html-templates.mjs';
const ALLOWED_FIELDS = new Set([
'platform','ai_provider','title','description','description_html','tags','photos','seller_notes',
'brand','category','size','color','condition','price','status','language','sku','storage_location',
]);
const ARRAY_LIMITS = { tags: 50, photos: 100 };
function fail(message, field, status = 400, code = 'LISTING_INPUT_INVALID') {
throw new PlatformError(message, { code, status, expose: true, details: { field } });
}
function htmlToPlainText(html) {
return String(html || '').replace(/<\s*br\s*\/?>/gi, '\n').replace(/<\/(p|div|h[1-6]|li|blockquote|pre)>/gi, '\n')
.replace(/<[^>]+>/g, '').replace(/&nbsp;/gi, ' ').replace(/&amp;/gi, '&').replace(/&lt;/gi, '<')
.replace(/&gt;/gi, '>').replace(/&quot;/gi, '"').replace(/&#39;/gi, "'").replace(/\n{3,}/g, '\n\n').trim();
}
function cleanText(value, max, field, { required = false } = {}) {
const text = String(value ?? '').trim();
if (required && !text) fail(`${field} ist erforderlich.`, field);
if (text.length > max) fail(`${field} ist zu lang.`, field);
if (/\u0000/.test(text)) fail(`${field} enthält ungültige Steuerzeichen.`, field);
return text;
}
function normalizePayload(input = {}, { partial = false } = {}) {
const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {};
for (const key of Object.keys(source)) if (!ALLOWED_FIELDS.has(key)) fail(`Unbekanntes Artikelfeld: ${key}`, key);
const out = {};
const textRules = {
platform:64, ai_provider:64, title:240, description:12000, seller_notes:6000, brand:160,
category:240, size:120, color:120, condition:120, status:64, language:16, sku:80, storage_location:160,
};
for (const [field,max] of Object.entries(textRules)) if (source[field] !== undefined) out[field] = cleanText(source[field], max, field, { required: field === 'title' && !partial });
if (!partial && source.title === undefined) fail('title ist erforderlich.', 'title');
if (source.description_html !== undefined) {
const html = String(source.description_html || '');
if (html.length > 50000) fail('description_html ist zu lang.', 'description_html');
out.description_html = sanitizeRichHtml(html);
if (source.description === undefined) out.description = htmlToPlainText(out.description_html).slice(0, 12000);
}
for (const field of ['tags','photos']) if (source[field] !== undefined) {
if (!Array.isArray(source[field])) fail(`${field} muss eine Liste sein.`, field);
out[field] = source[field].map(v => cleanText(v, field === 'photos' ? 500 : 120, field)).filter(Boolean).slice(0, ARRAY_LIMITS[field]);
}
if (source.price !== undefined && source.price !== null && source.price !== '') {
const price = Number(source.price);
if (!Number.isFinite(price) || price < 0 || price > 100000000) fail('price ist ungültig.', 'price');
out.price = price;
} else if (source.price !== undefined) out.price = null;
return Object.freeze(out);
}
export class ListingsService {
constructor(repository) { this.repository = repository; }
validateCreate(input) { return normalizePayload(input, { partial: false }); }
validateUpdate(input) { return normalizePayload(input, { partial: true }); }
create(input, actorId) { return this.repository.create({ ...this.validateCreate(input), created_by: actorId || null, updated_by: actorId || null }); }
list(filters = {}) { return this.repository.list(cleanText(filters.q || '', 240, 'q'), cleanText(filters.platform || '', 64, 'platform'), cleanText(filters.status || '', 64, 'status')); }
get(id) { const item = this.repository.get(Number(id)); if (!item) throw new PlatformError('Artikel nicht gefunden.', { code:'LISTING_NOT_FOUND', status:404, expose:true }); return item; }
update(id, input, actorId) { this.get(id); return this.repository.update(Number(id), { ...this.validateUpdate(input), updated_by: actorId || null }); }
softDelete(id) { this.get(id); this.repository.softDelete(Number(id)); return { ok:true }; }
duplicate(id) { const copy = this.repository.duplicate(Number(id)); if (!copy) throw new PlatformError('Artikel nicht gefunden.', { code:'LISTING_NOT_FOUND', status:404, expose:true }); return copy; }
trash() { return this.repository.listDeleted(); }
trashCount() { return { count: this.repository.trashCount() }; }
restore(id) { const item = this.repository.listDeleted().find(entry => Number(entry.id) === Number(id)); if (!item) throw new PlatformError('Artikel befindet sich nicht im Papierkorb.', { code:'LISTING_TRASH_NOT_FOUND', status:404, expose:true }); this.repository.restore(Number(id)); return { ok:true }; }
permanentDelete(id) { const item = this.repository.listDeleted().find(entry => Number(entry.id) === Number(id)); if (!item) throw new PlatformError('Artikel befindet sich nicht im Papierkorb.', { code:'LISTING_TRASH_NOT_FOUND', status:404, expose:true }); this.repository.permanentDelete(Number(id)); return { ok:true }; }
emptyTrash() { const items = this.repository.listDeleted(); for (const item of items) this.repository.permanentDelete(item.id); return { ok:true, deleted:items.length }; }
}
-1
View File
@@ -1 +0,0 @@
import manifest from './module.json' with {type:'json'}; import { MediaService } from './service.mjs'; let service=null; export function configureMediaModule(deps){service=new MediaService(deps);} export function getMediaService(){if(!service)throw new Error('Media-Modul ist nicht konfiguriert.');return service;} export function createMediaModule(){return {manifest,lifecycle:{async start(){},async stop(){},async health(){return {ok:Boolean(service),native:true,contracts:['library','reference-protection']};}}};}
-28
View File
@@ -1,28 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.media",
"name": "Media Library",
"version": "2.0.0",
"type": "feature",
"status": "native",
"description": "Native Medienbibliothek mit sicherer Pfadbehandlung, Referenzschutz, Favoriten und Metadaten.",
"requires": [
"vendoo.auth"
],
"permissions": [
"media.view",
"media.edit",
"media.delete"
],
"provides": [
"media.assets",
"media.derivatives"
],
"consumes": [
"auth.identity"
],
"owns": [
"media.library",
"media.uploads"
]
}
-2
View File
@@ -1,2 +0,0 @@
import { listMediaLibraryItems, setMediaLibraryFavorite, getListingReferencedMediaPaths, removeMediaLibraryRecords } from '../../../lib/db.mjs';
export const mediaRepository=Object.freeze({list:listMediaLibraryItems,setFavorite:setMediaLibraryFavorite,referenced:getListingReferencedMediaPaths,removeRecords:removeMediaLibraryRecords});
-2
View File
@@ -1,2 +0,0 @@
import { getMediaService } from './index.mjs';
export function registerMediaRoutes({app,routes,deps}){const service=getMediaService();routes.register(app,{id:'media.library.list',method:'GET',path:'/api/media-library',owner:'vendoo.media',policy:'media.view',csrf:false,stability:'stable',handler:async(q,r)=>r.json(await service.list(q.query||{}))});routes.register(app,{id:'media.library.favorite',method:'PATCH',path:'/api/media-library/favorite',owner:'vendoo.media',policy:'media.edit',stability:'stable',input:body=>service.validateFavorite(body),handler:async(q,r)=>{const result=service.favorite(q.validatedBody);if(!result.ok)return r.status(400).json({error:'Nur FLUX-Bilder können als Favorit markiert werden.'});deps.auditLog(q.user.id,q.user.email,'media.favorite_updated','media',q.validatedBody.image_path,String(q.validatedBody.favorite),deps.getClientIp(q));r.json(result);}});routes.register(app,{id:'media.library.delete',method:'DELETE',path:'/api/media-library/items',owner:'vendoo.media',policy:'media.delete',stability:'stable',input:body=>service.validateDelete(body),handler:async(q,r)=>{const result=service.delete(q.validatedBody);deps.auditLog(q.user.id,q.user.email,'media.deleted','media',null,JSON.stringify({deleted:result.deleted,protected:result.protected.length}),deps.getClientIp(q));r.json(result);}});}
-8
View File
@@ -1,8 +0,0 @@
import { existsSync, statSync, unlinkSync } from 'node:fs'; import { resolve, sep, extname } from 'node:path'; import { PlatformError } from '../../kernel/errors.mjs'; function normalizePath(value){const p=String(value||'').replace(/\\/g,'/').replace(/^\/+/, '').trim();if(!p||p.includes('..')||/[\u0000\r\n]/.test(p)) throw new PlatformError('Ungültiger Medienpfad.',{code:'MEDIA_PATH_INVALID',status:400,expose:true});return p;}
export class MediaService { constructor({uploadRoot,sharp,repository}){this.uploadRoot=resolve(uploadRoot);this.sharp=sharp;this.repository=repository;}
async list(query={}){const source=String(query.source||'all').slice(0,32),search=String(query.q||'').slice(0,240),favoritesOnly=String(query.favorites||'0')==='1',sort=['newest','oldest','name'].includes(String(query.sort))?String(query.sort):'newest';const all=this.repository.list({source,search,favoritesOnly,sort});const pageSize=Math.max(12,Math.min(96,Number(query.page_size)||24)),total=all.length,totalPages=Math.max(1,Math.ceil(total/pageSize)),page=Math.min(Math.max(1,Number(query.page)||1),totalPages);const pageItems=all.slice((page-1)*pageSize,page*pageSize);const items=await Promise.all(pageItems.map(async item=>{const imagePath=normalizePath(item.image_path),absolute=resolve(this.uploadRoot,imagePath);let fileSize=null,width=Number(item.width)||null,height=Number(item.height)||null,format=item.format||extname(imagePath).replace('.','').toLowerCase()||null;if(absolute!==this.uploadRoot&&absolute.startsWith(this.uploadRoot+sep)&&existsSync(absolute)){try{fileSize=statSync(absolute).size;}catch{}if((!width||!height)&&this.sharp){try{const m=await this.sharp(absolute,{failOn:'none'}).metadata();width=m.width||width;height=m.height||height;format=m.format||format;}catch{}}}return {...item,image_path:imagePath,public_url:`/uploads/${imagePath}`,file_size:fileSize,width,height,format};}));const summary=this.repository.list({source:'all',search,favoritesOnly,sort});const counts=summary.reduce((acc,item)=>{for(const s of item.sources||[])acc[s]=(acc[s]||0)+1;return acc;},{});return {items,pagination:{page,page_size:pageSize,total,total_pages:totalPages},summary:{total:summary.length,sources:counts}};}
validateFavorite(input={}){for(const key of Object.keys(input||{}))if(!['image_path','favorite'].includes(key))throw new PlatformError(`Unbekanntes Medienfeld: ${key}`,{code:'MEDIA_INPUT_INVALID',status:400,expose:true});return Object.freeze({image_path:normalizePath(input.image_path),favorite:input.favorite!==false});}
favorite(input={}){const valid=this.validateFavorite(input);return this.repository.setFavorite(valid.image_path,valid.favorite);}
validateDelete(input={}){for(const key of Object.keys(input||{}))if(!['image_paths'].includes(key))throw new PlatformError(`Unbekanntes Medienfeld: ${key}`,{code:'MEDIA_INPUT_INVALID',status:400,expose:true});if(!Array.isArray(input.image_paths))throw new PlatformError('image_paths muss eine Liste sein.',{code:'MEDIA_PATHS_REQUIRED',status:400,expose:true});const paths=[...new Set(input.image_paths.map(normalizePath))].slice(0,500);if(!paths.length)throw new PlatformError('Keine Medien ausgewählt.',{code:'MEDIA_PATHS_REQUIRED',status:400,expose:true});return Object.freeze({image_paths:paths});}
delete(input={}){const requested=this.validateDelete(input).image_paths,referenced=this.repository.referenced(),protectedPaths=requested.filter(p=>referenced.has(p)),deletable=requested.filter(p=>!referenced.has(p)),deletedFiles=[];for(const p of deletable){const absolute=resolve(this.uploadRoot,p);if(absolute===this.uploadRoot||!absolute.startsWith(this.uploadRoot+sep))continue;try{if(existsSync(absolute)&&statSync(absolute).isFile()){unlinkSync(absolute);deletedFiles.push(p);}}catch{}}this.repository.removeRecords(deletable);return {ok:true,deleted:deletable.length,deleted_files:deletedFiles.length,protected:protectedPaths};}
}
-28
View File
@@ -1,28 +0,0 @@
import manifest from './module.json' with { type: 'json' };
import { ModuleManagerService } from './service.mjs';
let service = null;
export function createModuleManagerModule() {
return {
manifest,
source: 'builtin',
lifecycle: {
async start(context) {
service = new ModuleManagerService({
modules: context.modules,
stateStore: context.moduleStateStore,
packageStore: context.modulePackageStore,
context,
});
},
async stop() { service = null; },
async health() { return { ok: Boolean(service), protected: true }; },
},
};
}
export function getModuleManagerService() {
if (!service) throw new Error('Module Manager ist noch nicht gestartet.');
return service;
}
-14
View File
@@ -1,14 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.module-manager",
"name": "Module Manager",
"version": "1.0.0",
"type": "core",
"status": "native",
"description": "Sichere Verwaltung eingebauter und installierter Vendoo-Module mit Aktivierung, Export, Quarantäne und kontrollierter Entfernung.",
"requires": ["vendoo.security", "vendoo.settings"],
"permissions": ["modules.manage"],
"provides": ["modules.management", "modules.packages"],
"consumes": ["security.policies", "settings.configuration"],
"owns": ["modules.state", "modules.packages"]
}
-62
View File
@@ -1,62 +0,0 @@
import { objectSchema } from '../../kernel/input-schema.mjs';
import { getModuleManagerService } from './index.mjs';
const toggleSchema = objectSchema({
cascade: { type: 'boolean' },
});
export function registerModuleManagerRoutes({ app, routes, upload, deps }) {
routes.register(app, {
id: 'modules.catalog.read', method: 'GET', path: '/api/admin/modules', owner: 'vendoo.module-manager',
policy: 'modules.manage', audit: 'modules.catalog.read', rateLimit: 'administration', csrf: false, stability: 'stable',
handler: async (_req, res) => res.json(getModuleManagerService().catalog()),
});
routes.register(app, {
id: 'modules.enable', method: 'POST', path: '/api/admin/modules/:id/enable', owner: 'vendoo.module-manager',
policy: 'modules.manage', audit: 'modules.enabled', rateLimit: 'administration', csrf: true, stability: 'stable',
handler: async (req, res) => {
const module = await getModuleManagerService().enable(req.params.id);
deps.auditLog(req.user.id, req.user.email, 'module.enabled', 'module', req.params.id, null, deps.getClientIp(req));
res.json({ module });
},
});
routes.register(app, {
id: 'modules.disable', method: 'POST', path: '/api/admin/modules/:id/disable', owner: 'vendoo.module-manager',
policy: 'modules.manage', audit: 'modules.disabled', rateLimit: 'administration', csrf: true, stability: 'stable', input: toggleSchema,
handler: async (req, res) => {
const result = await getModuleManagerService().disable(req.params.id, req.validatedBody || {});
deps.auditLog(req.user.id, req.user.email, 'module.disabled', 'module', req.params.id, JSON.stringify(result.disabled), deps.getClientIp(req));
res.json(result);
},
});
routes.register(app, {
id: 'modules.install', method: 'POST', path: '/api/admin/modules/install', owner: 'vendoo.module-manager',
policy: 'modules.manage', audit: 'modules.installed', rateLimit: 'administration', csrf: true, stability: 'stable',
middleware: [upload.single('module')],
handler: async (req, res) => {
const module = getModuleManagerService().install(req.file?.buffer);
deps.auditLog(req.user.id, req.user.email, 'module.installed', 'module', module.id, module.version, deps.getClientIp(req));
res.status(201).json({ module });
},
});
routes.register(app, {
id: 'modules.remove', method: 'DELETE', path: '/api/admin/modules/:id', owner: 'vendoo.module-manager',
policy: 'modules.manage', audit: 'modules.removed', rateLimit: 'administration', csrf: true, stability: 'stable',
handler: async (req, res) => {
const result = await getModuleManagerService().remove(req.params.id);
deps.auditLog(req.user.id, req.user.email, 'module.removed', 'module', req.params.id, null, deps.getClientIp(req));
res.json(result);
},
});
routes.register(app, {
id: 'modules.export', method: 'GET', path: '/api/admin/modules/:id/export', owner: 'vendoo.module-manager',
policy: 'modules.manage', audit: 'modules.exported', rateLimit: 'administration', csrf: false, stability: 'stable',
handler: async (req, res) => {
const payload = getModuleManagerService().export(req.params.id);
deps.auditLog(req.user.id, req.user.email, 'module.exported', 'module', req.params.id, null, deps.getClientIp(req));
res.setHeader('Content-Type', 'application/vnd.vendoo.module+json; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="${String(req.params.id).replace(/[^a-z0-9.-]/gi, '_')}.vmod"`);
res.send(payload);
},
});
}
-233
View File
@@ -1,233 +0,0 @@
import { readFileSync } from 'fs';
import { PlatformError } from '../../kernel/errors.mjs';
import { buildModulePackage } from '../../core/modules/module-package-contract.mjs';
export const PROTECTED_MODULE_IDS = Object.freeze([
'vendoo.system',
'vendoo.security',
'vendoo.auth',
'vendoo.users',
'vendoo.sessions',
'vendoo.settings',
'vendoo.themes',
'vendoo.listings',
'vendoo.inventory',
'vendoo.media',
'vendoo.operations',
'vendoo.module-manager',
'vendoo.deployments',
]);
const PROTECTED_MODULES = new Set(PROTECTED_MODULE_IDS);
function asPublicModule(record, packagesById) {
const external = record.source === 'external';
const pkg = packagesById.get(record.id);
const protectedModule = PROTECTED_MODULES.has(record.id);
const canDisable = !protectedModule && record.enabled;
const canEnable = !record.enabled && (!external || pkg?.activatable === true);
return {
...record,
protected: protectedModule,
removable: external && !record.enabled,
exportable: true,
installSource: external ? 'installed' : 'builtin',
package: pkg ? {
runtime: pkg.runtime,
trust: pkg.trust,
activatable: pkg.activatable,
invalid: Boolean(pkg.invalid),
error: pkg.error || null,
} : null,
actions: {
enable: canEnable,
disable: canDisable,
remove: external && !record.enabled,
export: true,
},
};
}
export function createExternalLifecycle(pkg) {
return {
async start() {
if (pkg.runtime.mode === 'isolated') {
throw new PlatformError('Der isolierte Extension-Host ist für ausführbare Fremdmodule noch nicht aktiviert.', {
code: 'MODULE_EXTENSION_HOST_UNAVAILABLE', status: 409, expose: true,
});
}
},
async stop() {},
async health() {
return { ok: true, runtime: pkg.runtime.mode, external: true };
},
};
}
export class ModuleManagerService {
#modules;
#stateStore;
#packageStore;
#context;
constructor({ modules, stateStore, packageStore, context }) {
this.#modules = modules;
this.#stateStore = stateStore;
this.#packageStore = packageStore;
this.#context = context;
}
registerInstalledPackages() {
const installed = this.#packageStore.list();
for (const entry of installed) {
if (entry.invalid || this.#modules.has(entry.id)) continue;
const pkg = this.#packageStore.get(entry.id);
const enabled = entry.activatable && this.#stateStore.isEnabled(entry.id, false);
this.#modules.register({ manifest: pkg.manifest, lifecycle: createExternalLifecycle(pkg), source: 'external' }, { enabled });
}
return installed.length;
}
catalog() {
const packages = this.#packageStore.list();
const packagesById = new Map(packages.filter(item => item.id).map(item => [item.id, item]));
const modules = this.#modules.snapshot().map(record => asPublicModule(record, packagesById));
for (const pkg of packages) {
if (!pkg.invalid || modules.some(item => item.id === pkg.id)) continue;
modules.push({
id: pkg.id,
name: pkg.id,
version: null,
type: 'extension-host',
status: 'invalid',
source: 'external',
enabled: false,
state: 'invalid',
protected: false,
removable: true,
exportable: false,
installSource: 'installed',
package: { invalid: true, error: pkg.error, activatable: false },
actions: { enable: false, disable: false, remove: true, export: false },
});
}
return {
modules: modules.sort((a, b) => String(a.name || a.id).localeCompare(String(b.name || b.id), 'de')),
summary: {
total: modules.length,
enabled: modules.filter(item => item.enabled).length,
disabled: modules.filter(item => !item.enabled && item.state !== 'invalid').length,
native: modules.filter(item => item.status === 'native').length,
legacy: modules.filter(item => item.status === 'legacy-bridge').length,
external: modules.filter(item => item.source === 'external').length,
protected: modules.filter(item => item.protected).length,
},
safety: {
unsignedDeclarativeAllowed: true,
executableRequiresTrustedSignature: true,
isolatedHostAvailable: false,
directMainProcessExecution: false,
},
};
}
async enable(id) {
const record = this.#modules.get(id);
if (!record) throw new PlatformError('Modul nicht gefunden.', { code: 'MODULE_NOT_FOUND', status: 404, expose: true });
if (record.source === 'external') {
const pkg = this.#packageStore.describe(id);
if (!pkg?.activatable) {
throw new PlatformError('Dieses Fremdmodul ist nicht aktivierbar. Ausführbarer Code benötigt eine vertrauenswürdige Signatur und den isolierten Extension-Host.', {
code: 'MODULE_NOT_ACTIVATABLE', status: 409, expose: true,
});
}
if (pkg.runtime.mode === 'isolated') {
throw new PlatformError('Der isolierte Extension-Host ist noch nicht verfügbar.', { code: 'MODULE_EXTENSION_HOST_UNAVAILABLE', status: 409, expose: true });
}
}
const enabling = new Set();
const enableWithDependencies = async moduleId => {
if (enabling.has(moduleId)) throw new PlatformError('Zyklische Modulabhängigkeit erkannt.', { code: 'MODULE_DEPENDENCY_CYCLE', status: 409, expose: true });
const target = this.#modules.get(moduleId);
if (!target) throw new PlatformError(`Fehlende Modulabhängigkeit: ${moduleId}`, { code: 'MODULE_DEPENDENCY_MISSING', status: 409, expose: true });
if (target.enabled && target.state === 'running') return this.#modules.describe(moduleId);
enabling.add(moduleId);
for (const dependency of target.manifest.requires) await enableWithDependencies(dependency);
const enabled = await this.#modules.enable(moduleId, this.#context);
this.#stateStore.setEnabled(moduleId, true, { source: moduleId === id ? 'module-manager' : 'module-manager-dependency' });
enabling.delete(moduleId);
return enabled;
};
return enableWithDependencies(id);
}
async disable(id, { cascade = false } = {}) {
if (PROTECTED_MODULES.has(id)) {
throw new PlatformError('Dieses Kernmodul ist geschützt und kann nicht deaktiviert werden.', {
code: 'MODULE_PROTECTED', status: 409, expose: true,
});
}
const dependents = this.#modules.dependentsOf(id, { enabledOnly: true, recursive: true });
const protectedDependents = dependents.filter(dependent => PROTECTED_MODULES.has(dependent));
if (protectedDependents.length) {
throw new PlatformError('Das Modul wird von geschützten Kernmodulen benötigt.', {
code: 'MODULE_REQUIRED_BY_PROTECTED', status: 409, expose: true, details: { dependents: protectedDependents },
});
}
const result = await this.#modules.disable(id, this.#context, { cascade: Boolean(cascade) });
for (const disabledId of result.disabled) this.#stateStore.setEnabled(disabledId, false, { source: 'module-manager' });
return result;
}
install(buffer) {
if (!Buffer.isBuffer(buffer) || !buffer.length) throw new PlatformError('Modulpaket fehlt.', { code: 'MODULE_PACKAGE_REQUIRED', status: 400, expose: true });
let parsed;
try { parsed = JSON.parse(buffer.toString('utf8')); }
catch (error) { throw new PlatformError('Das .vmod-Paket enthält kein gültiges JSON.', { code: 'MODULE_PACKAGE_JSON_INVALID', status: 400, expose: true, cause: error }); }
const id = String(parsed?.manifest?.id || '');
if (this.#modules.has(id)) throw new PlatformError('Eine Modul-ID mit diesem Namen ist bereits registriert.', { code: 'MODULE_DUPLICATE', status: 409, expose: true });
const installed = this.#packageStore.install(parsed);
const pkg = this.#packageStore.get(installed.id);
try {
this.#modules.register({ manifest: pkg.manifest, lifecycle: createExternalLifecycle(pkg), source: 'external' }, { enabled: false });
this.#stateStore.setEnabled(installed.id, false, { source: 'module-install' });
} catch (error) {
this.#packageStore.remove(installed.id);
this.#stateStore.remove(installed.id);
throw error;
}
return this.catalog().modules.find(item => item.id === installed.id);
}
async remove(id) {
const record = this.#modules.get(id);
if (!record) {
const removedInvalid = this.#packageStore.remove(id);
if (removedInvalid) this.#stateStore.remove(id);
return { ok: removedInvalid, id };
}
if (record.source !== 'external') {
throw new PlatformError('Eingebaute Vendoo-Module können nicht gelöscht werden.', { code: 'MODULE_BUILTIN_NOT_REMOVABLE', status: 409, expose: true });
}
if (record.enabled) throw new PlatformError('Das Modul muss vor dem Löschen deaktiviert werden.', { code: 'MODULE_DISABLE_BEFORE_REMOVE', status: 409, expose: true });
await this.#modules.unregister(id, this.#context);
this.#packageStore.remove(id);
this.#stateStore.remove(id);
return { ok: true, id };
}
export(id) {
const record = this.#modules.get(id);
if (!record) throw new PlatformError('Modul nicht gefunden.', { code: 'MODULE_NOT_FOUND', status: 404, expose: true });
if (record.source === 'external') return this.#packageStore.export(id);
const pkg = buildModulePackage({
manifest: record.manifest,
runtime: { mode: 'builtin-reference', entry: null },
files: [{ path: 'module.json', content: `${JSON.stringify(record.manifest, null, 2)}\n` }],
});
return Buffer.from(`${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
}
stateSnapshot() {
return this.#stateStore.snapshot();
}
}
-26
View File
@@ -1,26 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.operations",
"name": "Operations Center",
"version": "1.0.0",
"type": "feature",
"status": "legacy-bridge",
"description": "Vertrag für den bestehenden Vendoo-Bereich Operations Center; schrittweise Migration ohne Funktionsbruch.",
"requires": [
"vendoo.security"
],
"permissions": [
"operations.manage"
],
"provides": [
"operations.backup",
"operations.update"
],
"consumes": [
"security.audit"
],
"owns": [
"operations.backups",
"operations.updates"
]
}
-23
View File
@@ -1,23 +0,0 @@
import manifest from './module.json' with { type: 'json' };
let runtime = null;
let started = false;
export function configurePublishingModule(deps = {}) {
runtime = {
start: deps.start || (() => {}),
stop: deps.stop || (() => {}),
};
}
export function createPublishingModule() {
return {
manifest,
source: 'builtin',
lifecycle: {
async start() { if (!runtime) throw new Error('Publishing-Modul ist nicht konfiguriert.'); await runtime.start(); started = true; },
async stop() { await runtime?.stop?.(); started = false; },
async health() { return { ok: Boolean(runtime && started), native: true, queue: 'publishing-jobs' }; },
},
};
}
-30
View File
@@ -1,30 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.publishing",
"name": "Publishing",
"version": "1.0.0",
"type": "feature",
"status": "native",
"description": "Native Publishing-Jobs, Plattformadapter und kontrollierter Queue-Lifecycle.",
"requires": [
"vendoo.listings",
"vendoo.security"
],
"permissions": [
"publishing.view",
"publishing.execute",
"publishing.manage"
],
"provides": [
"publishing.jobs",
"publishing.adapters"
],
"consumes": [
"listings.catalog",
"security.policies"
],
"owns": [
"publishing.queue",
"publishing.events"
]
}
-13
View File
@@ -1,13 +0,0 @@
import manifest from './module.json' with { type: 'json' };
export function createQualityModule() {
return {
manifest,
source: 'builtin',
lifecycle: {
async start() {},
async stop() {},
async health() { return { ok: true, native: true, contracts: ['reports', 'ai-suggestions'] }; },
},
};
}
-26
View File
@@ -1,26 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.quality",
"name": "Quality Center",
"version": "1.0.0",
"type": "feature",
"status": "native",
"description": "Native Qualitätsanalyse, Verlauf und bestätigte AI-Vorschläge.",
"requires": [
"vendoo.listings",
"vendoo.ai"
],
"permissions": [
"quality.execute"
],
"provides": [
"quality.reports"
],
"consumes": [
"listings.catalog",
"ai.text"
],
"owns": [
"quality.history"
]
}
-26
View File
@@ -1,26 +0,0 @@
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { secretStoreStatus } from '../../../lib/secret-store.mjs';
import { getRequestTelemetry } from '../../../lib/structured-logger.mjs';
const here = dirname(fileURLToPath(import.meta.url));
const manifest = JSON.parse(readFileSync(join(here, 'module.json'), 'utf8'));
export function createSecurityModule() {
return {
manifest,
lifecycle: {
async start() {},
async stop() {},
async health() {
const secrets = secretStoreStatus();
return { ok: true, provider: secrets.provider, encrypted_secrets: secrets.encrypted };
},
},
};
}
export function getSecurityFoundationStatus() {
return { secrets: secretStoreStatus(), requests: getRequestTelemetry() };
}
-31
View File
@@ -1,31 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.security",
"name": "Security Core",
"version": "2.0.0",
"type": "core",
"status": "native",
"description": "Nativer Sicherheitskern für verschlüsselte Secrets, Request-Telemetrie, Sicherheitsheader und Security-Dashboard.",
"requires": [
"vendoo.system"
],
"permissions": [
"security.manage"
],
"provides": [
"security.policies",
"security.audit",
"security.secrets",
"security.telemetry",
"security.headers"
],
"consumes": [
"platform.events"
],
"owns": [
"security.permissions",
"security.rate-limits",
"security.secret-store",
"security.request-telemetry"
]
}
-24
View File
@@ -1,24 +0,0 @@
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { PlatformError } from '../../kernel/errors.mjs';
const here = dirname(fileURLToPath(import.meta.url));
const manifest = JSON.parse(readFileSync(join(here, 'module.json'), 'utf8'));
let service = null;
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(); },
},
};
}
-14
View File
@@ -1,14 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.sessions",
"name": "Sessions",
"version": "1.0.0",
"type": "core",
"status": "native",
"description": "Native Sitzungsverwaltung mit Idle-Timeout, Eigentümerprüfung, Revocation und administrativer Bereinigung.",
"requires": ["vendoo.security"],
"permissions": ["sessions.manage"],
"provides": ["auth.sessions", "sessions.lifecycle"],
"consumes": ["security.policies"],
"owns": ["auth.sessions"]
}
-44
View File
@@ -1,44 +0,0 @@
import { getSessionsService } from './index.mjs';
export function registerSessionRoutes({ app, routes, deps }) {
routes.register(app, {
id: 'sessions.admin.list', method: 'GET', path: '/api/admin/sessions', owner: 'vendoo.sessions', policy: 'sessions.manage', csrf: false, stability: 'stable',
handler: async (_req, res) => res.json(getSessionsService().listAll()),
});
routes.register(app, {
id: 'sessions.admin.revoke', method: 'DELETE', path: '/api/admin/sessions/:id', owner: 'vendoo.sessions', policy: 'sessions.manage', csrf: true, stability: 'stable',
handler: async (req, res) => {
const session = getSessionsService().revokeAsAdmin(Number(req.params.id));
deps.auditLog(req.user.id, req.user.email, 'session.destroyed', 'session', String(session.id), null, deps.getClientIp(req));
res.json({ ok: true });
},
});
routes.register(app, {
id: 'sessions.admin.revoke-user', method: 'DELETE', path: '/api/admin/users/:id/sessions', owner: 'vendoo.sessions', policy: 'sessions.manage', csrf: true, stability: 'stable',
handler: async (req, res) => {
getSessionsService().revokeAllForUser(Number(req.params.id));
deps.auditLog(req.user.id, req.user.email, 'sessions.destroyed_all', 'user', String(req.params.id), null, deps.getClientIp(req));
res.json({ ok: true });
},
});
routes.register(app, {
id: 'sessions.admin.cleanup', method: 'POST', path: '/api/admin/sessions/cleanup', owner: 'vendoo.sessions', policy: 'sessions.manage', csrf: true, stability: 'stable',
handler: async (req, res) => {
getSessionsService().cleanup();
deps.auditLog(req.user.id, req.user.email, 'sessions.cleanup', null, null, null, deps.getClientIp(req));
res.json({ ok: true, message: 'Abgelaufene Sessions bereinigt' });
},
});
routes.register(app, {
id: 'sessions.self.list', method: 'GET', path: '/api/my/sessions', owner: 'vendoo.sessions', policy: 'sessions.self', csrf: false, stability: 'stable',
handler: async (req, res) => res.json(getSessionsService().listForUser(req.user.id)),
});
routes.register(app, {
id: 'sessions.self.revoke', method: 'DELETE', path: '/api/my/sessions/:id', owner: 'vendoo.sessions', policy: 'sessions.self', csrf: true, stability: 'stable',
handler: async (req, res) => {
const session = getSessionsService().revokeOwn(req.user.id, Number(req.params.id));
deps.auditLog(req.user.id, req.user.email, 'session.destroyed_own', 'session', String(session.id), null, deps.getClientIp(req));
res.json({ ok: true });
},
});
}
-33
View File
@@ -1,33 +0,0 @@
import {
cleanExpiredSessions, destroyOwnedSessionById, destroySessionById, destroyUserSessions,
getAllActiveSessions, getSessionById, getUserSessions,
} from '../../core/identity/identity-store.mjs';
import { PlatformError } from '../../kernel/errors.mjs';
function fail(message, status = 400, code = 'SESSION_OPERATION_FAILED') {
throw new PlatformError(message, { status, code, expose: true });
}
export function createSessionsService() {
return Object.freeze({
listAll() { return getAllActiveSessions(); },
listForUser(userId) { return getUserSessions(userId); },
revokeAsAdmin(sessionId) {
const session = getSessionById(sessionId);
if (!session) fail('Sitzung nicht gefunden.', 404, 'SESSION_NOT_FOUND');
destroySessionById(sessionId);
return session;
},
revokeAllForUser(userId) {
destroyUserSessions(userId);
return { user_id: Number(userId) };
},
revokeOwn(userId, sessionId) {
const changed = destroyOwnedSessionById(userId, sessionId);
if (!changed) fail('Sitzung nicht gefunden oder gehört nicht zu diesem Benutzer.', 404, 'SESSION_NOT_OWNED');
return { id: Number(sessionId), user_id: Number(userId) };
},
cleanup() { cleanExpiredSessions(); return { ok: true }; },
health() { return { ok: true, active_sessions: getAllActiveSessions().length }; },
});
}
-25
View File
@@ -1,25 +0,0 @@
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { PlatformError } from '../../kernel/errors.mjs';
const here = dirname(fileURLToPath(import.meta.url));
const manifest = JSON.parse(readFileSync(join(here, 'module.json'), 'utf8'));
let service = null;
let adapters = { etsyApi: null, ebayApi: null };
export function configureSettingsModule(nextAdapters = {}) { adapters = { ...adapters, ...nextAdapters }; }
export function getSettingsService() {
if (!service) throw new PlatformError('Einstellungs-Modul ist noch nicht gestartet.', { code: 'SETTINGS_MODULE_NOT_READY', status: 503, expose: true });
return service;
}
export function createSettingsModule() {
return {
manifest,
lifecycle: {
async start() { service = (await import('./service.mjs')).createSettingsService(adapters); },
async stop() { service = null; },
async health() { return getSettingsService().health(); },
},
};
}
-29
View File
@@ -1,29 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.settings",
"name": "Settings",
"version": "1.0.0",
"type": "core",
"status": "native",
"description": "Native Anwendungs- und SMTP-Einstellungen mit Allowlist, verschlüsselten Secrets und Runtime-Konfigurationsadapter.",
"requires": [
"vendoo.security",
"vendoo.users"
],
"permissions": [
"settings.manage"
],
"provides": [
"settings.configuration",
"settings.application",
"settings.smtp"
],
"consumes": [
"security.secrets",
"users.directory"
],
"owns": [
"settings.application",
"settings.smtp"
]
}
-15
View File
@@ -1,15 +0,0 @@
import { db } from '../../../lib/db.mjs';
export function readApplicationSettings() {
const rows = db.prepare('SELECT key, value FROM settings').all();
return Object.fromEntries(rows.map(row => [row.key, row.value]));
}
export function writeApplicationSettings(settings) {
const stmt = db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)');
const transaction = db.transaction(entries => {
for (const [key, value] of entries) stmt.run(key, value == null ? '' : String(value));
});
transaction(Object.entries(settings));
return readApplicationSettings();
}
-45
View File
@@ -1,45 +0,0 @@
import { objectSchema } from '../../kernel/input-schema.mjs';
import { getSettingsService } from './index.mjs';
const smtpSchema = objectSchema({
smtp_host: { type: 'string', maxLength: 255 }, smtp_port: { type: 'string', maxLength: 8 },
smtp_user: { type: 'string', maxLength: 255 }, smtp_pass: { type: 'string', maxLength: 4096, trim: false },
smtp_from: { type: 'string', maxLength: 255 },
});
export function registerSettingsRoutes({ app, routes, deps }) {
routes.register(app, {
id: 'settings.application.read', method: 'GET', path: '/api/settings', owner: 'vendoo.settings', policy: 'settings.read', csrf: false, stability: 'stable',
handler: async (_req, res) => res.json(getSettingsService().get()),
});
routes.register(app, {
id: 'settings.application.update', method: 'PUT', path: '/api/settings', owner: 'vendoo.settings', policy: 'settings.manage', csrf: true, stability: 'stable',
handler: async (req, res) => {
const result = getSettingsService().update(req.body || {});
deps.auditLog(req.user.id, req.user.email, 'settings.updated', 'settings', 'application', null, deps.getClientIp(req));
res.json(result);
},
});
routes.register(app, {
id: 'settings.smtp.read', method: 'GET', path: '/api/admin/smtp', owner: 'vendoo.settings', policy: 'settings.manage', csrf: false, stability: 'stable',
handler: async (_req, res) => res.json(getSettingsService().smtpStatus()),
});
routes.register(app, {
id: 'settings.smtp.update', method: 'PUT', path: '/api/admin/smtp', owner: 'vendoo.settings', policy: 'settings.manage', csrf: true, stability: 'stable', input: smtpSchema,
handler: async (req, res) => {
const result = getSettingsService().updateSmtp(req.validatedBody || {});
deps.auditLog(req.user.id, req.user.email, 'smtp.updated', 'settings', 'smtp', null, deps.getClientIp(req));
res.json(result);
},
});
routes.register(app, {
id: 'settings.smtp.test', method: 'POST', path: '/api/admin/smtp/test', owner: 'vendoo.settings', policy: 'settings.manage', csrf: true, stability: 'stable',
handler: async (req, res) => {
if (!deps.isMailerConfigured()) return res.status(400).json({ error: 'SMTP nicht konfiguriert. Bitte zuerst speichern.' });
if (!req.user?.email) return res.status(400).json({ error: 'Admin-E-Mail nicht gefunden' });
await deps.testMailer(req.user.email);
deps.auditLog(req.user.id, req.user.email, 'smtp.test_sent', 'settings', 'smtp', `an ${req.user.email}`, deps.getClientIp(req));
res.json({ ok: true, message: `Testmail an ${req.user.email} gesendet` });
},
});
}
-136
View File
@@ -1,136 +0,0 @@
import { readRuntimeConfig, setConfigValue, writeRuntimeConfig } from '../../../lib/config-store.mjs';
import { getSecret, hasSecret, setSecret } from '../../../lib/secret-store.mjs';
import { isMailerConfigured, reinitMailer } from '../../../lib/mailer.mjs';
import { PlatformError } from '../../kernel/errors.mjs';
import { readApplicationSettings, writeApplicationSettings } from './repository.mjs';
const SECRET_FIELDS = Object.freeze({
anthropic_key: 'ANTHROPIC_API_KEY', openai_key: 'OPENAI_API_KEY', openrouter_key: 'OPENROUTER_API_KEY',
etsy_key: 'ETSY_API_KEY', ebay_client_secret: 'EBAY_CLIENT_SECRET', ai_model_photo_local_token: 'LOCAL_IMAGE_TOKEN',
});
const RUNTIME_FIELDS = Object.freeze({
ebay_client_id: 'EBAY_CLIENT_ID', ebay_ru_name: 'EBAY_RU_NAME', ebay_sandbox: 'EBAY_SANDBOX',
ollama_url: 'OLLAMA_URL', public_base_url: 'PUBLIC_BASE_URL', ai_model_photo_local_url: 'LOCAL_IMAGE_URL',
ai_model_photo_local_workflow_path: 'LOCAL_IMAGE_WORKFLOW_PATH', ai_model_photo_local_install_path: 'LOCAL_IMAGE_INSTALL_PATH',
});
const DB_FIELDS = new Set([
'default_platform','default_ai','default_language','seller_notes','ai_model_photos_enabled','ai_model_photo_provider',
'ai_model_photo_default_mode','ai_model_photo_default_variants','ai_model_photo_default_preset','ai_model_photo_openrouter_free_only',
'ai_model_photo_block_underwear','ai_model_photo_block_kids','ai_model_photo_preserve_logos','ai_model_photo_openrouter_model',
'ai_model_photo_openai_model','ai_model_photo_local_enabled','ai_model_photo_local_require_token','ai_model_photo_use_source_reference',
'ai_model_photo_reference_strength','ai_model_photo_local_resolution','flux_runtime_profile','flux_default_profile',
'flux_prompt_assistant','flux_default_style','flux_default_format','flux_default_variants','flux_seed_lock','flux_max_parallel_jobs',
'smtp_host','smtp_port','smtp_user','smtp_from',
]);
const ALL_FIELDS = new Set([...Object.keys(SECRET_FIELDS), ...Object.keys(RUNTIME_FIELDS), ...DB_FIELDS]);
function fail(message, status = 400, code = 'SETTINGS_INVALID') {
throw new PlatformError(message, { status, code, expose: true });
}
function cleanString(value, { max = 4096, multiline = false } = {}) {
const result = String(value ?? '').trim();
if (result.length > max) fail('Einstellungswert ist zu lang.');
if (!multiline && /[\r\n\0]/.test(result)) fail('Einstellungswert enthält unzulässige Steuerzeichen.');
return result;
}
export function createSettingsService({ etsyApi, ebayApi }) {
function decorate(settings) {
const result = { ...settings };
result.has_anthropic_key = hasSecret('ANTHROPIC_API_KEY');
result.has_openai_key = hasSecret('OPENAI_API_KEY');
result.has_openrouter_key = hasSecret('OPENROUTER_API_KEY');
result.has_etsy_key = hasSecret('ETSY_API_KEY');
result.etsy_connected = Boolean(etsyApi?.isConnected?.());
result.has_ebay_credentials = Boolean(ebayApi?.hasCredentials?.());
result.ebay_connected = Boolean(ebayApi?.isConnected?.());
result.ebay_sandbox = process.env.EBAY_SANDBOX === 'true';
result.ollama_url = process.env.OLLAMA_URL || 'http://localhost:11434';
result.public_base_url = process.env.PUBLIC_BASE_URL || '';
result.ai_model_photo_local_url = process.env.LOCAL_IMAGE_URL || result.ai_model_photo_local_url || 'http://127.0.0.1:8188';
result.ai_model_photo_local_workflow_path = process.env.LOCAL_IMAGE_WORKFLOW_PATH || result.ai_model_photo_local_workflow_path || 'local-ai/workflows/vendoo_flux_schnell_api.json';
result.ai_model_photo_local_install_path = process.env.LOCAL_IMAGE_INSTALL_PATH || result.ai_model_photo_local_install_path || 'local-ai/comfyui';
result.ai_model_photo_local_enabled = result.ai_model_photo_local_enabled || '1';
result.ai_model_photo_local_require_token = result.ai_model_photo_local_require_token || '1';
result.ai_model_photo_use_source_reference = result.ai_model_photo_use_source_reference || '1';
result.ai_model_photo_reference_strength = result.ai_model_photo_reference_strength || '70';
result.ai_model_photo_local_resolution = result.ai_model_photo_local_resolution || '768';
result.ai_model_photo_preserve_logos = result.ai_model_photo_preserve_logos || '1';
result.flux_runtime_profile = result.flux_runtime_profile || 'auto';
result.flux_default_profile = result.flux_default_profile || 'balanced';
result.flux_prompt_assistant = result.flux_prompt_assistant || '1';
result.flux_default_style = result.flux_default_style || 'photorealistic';
result.flux_default_format = result.flux_default_format || '768x768';
result.flux_default_variants = result.flux_default_variants || '1';
result.flux_seed_lock = result.flux_seed_lock || '0';
result.flux_max_parallel_jobs = result.flux_max_parallel_jobs || '1';
result.local_image_token_set = hasSecret('LOCAL_IMAGE_TOKEN');
return result;
}
function validatePatch(input) {
const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {};
for (const key of Object.keys(source)) if (!ALL_FIELDS.has(key)) fail(`Unbekanntes Einstellungsfeld: ${key}`);
return source;
}
function smtpStatus() {
const settings = readApplicationSettings();
return {
smtp_host: settings.smtp_host || process.env.SMTP_HOST || '', smtp_port: settings.smtp_port || process.env.SMTP_PORT || '587',
smtp_user: settings.smtp_user || process.env.SMTP_USER || '', smtp_pass: hasSecret('SMTP_PASS') ? '••••••••' : '',
smtp_from: settings.smtp_from || process.env.SMTP_FROM || '', configured: isMailerConfigured(),
source: hasSecret('SMTP_PASS') ? 'encrypted-secret-store' : (process.env.SMTP_HOST ? 'environment' : 'none'),
};
}
return Object.freeze({
get() { return decorate(readApplicationSettings()); },
update(input) {
const source = validatePatch(input);
let envContent = readRuntimeConfig();
const dbPatch = {};
for (const [field, secretName] of Object.entries(SECRET_FIELDS)) {
if (source[field] !== undefined && source[field] !== '') setSecret(secretName, cleanString(source[field], { max: 16384 }), { source: 'settings-ui' });
}
for (const [field, envName] of Object.entries(RUNTIME_FIELDS)) {
if (source[field] === undefined) continue;
let value = cleanString(source[field], { max: 4096 });
if (['public_base_url','ai_model_photo_local_url'].includes(field)) value = value.replace(/\/$/, '');
envContent = setConfigValue(envContent, envName, value);
process.env[envName] = value;
}
for (const field of DB_FIELDS) {
if (source[field] === undefined) continue;
dbPatch[field] = cleanString(source[field], { max: field === 'seller_notes' ? 16000 : 4096, multiline: field === 'seller_notes' });
}
if (dbPatch.ai_model_photo_local_enabled !== undefined) {
const value = String(dbPatch.ai_model_photo_local_enabled) === '0' ? 'false' : 'true';
envContent = setConfigValue(envContent, 'LOCAL_IMAGE_ENABLED', value); process.env.LOCAL_IMAGE_ENABLED = value;
}
if (dbPatch.ai_model_photo_local_require_token !== undefined) {
const value = String(dbPatch.ai_model_photo_local_require_token) === '0' ? 'false' : 'true';
envContent = setConfigValue(envContent, 'LOCAL_IMAGE_REQUIRE_TOKEN', value); process.env.LOCAL_IMAGE_REQUIRE_TOKEN = value;
}
writeRuntimeConfig(envContent);
return decorate(writeApplicationSettings(dbPatch));
},
smtpStatus,
updateSmtp(input) {
const patch = {};
for (const field of ['smtp_host','smtp_port','smtp_user','smtp_from']) if (input[field] !== undefined) patch[field] = cleanString(input[field], { max: 512 });
if (input.smtp_pass !== undefined && input.smtp_pass !== '••••••••' && input.smtp_pass !== '') setSecret('SMTP_PASS', cleanString(input.smtp_pass, { max: 4096 }), { source: 'smtp-ui' });
patch.smtp_pass = '';
const settings = writeApplicationSettings(patch);
const smtpSecret = getSecret('SMTP_PASS');
if (settings.smtp_host && settings.smtp_user && smtpSecret) {
reinitMailer({ host: settings.smtp_host, port: settings.smtp_port || '587', user: settings.smtp_user, pass: smtpSecret });
if (settings.smtp_from) process.env.SMTP_FROM = settings.smtp_from;
}
return smtpStatus();
},
health() { return { ok: true, settings: Object.keys(readApplicationSettings()).length, smtp_configured: isMailerConfigured() }; },
});
}
-23
View File
@@ -1,23 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.system",
"name": "System Kernel",
"version": "1.0.0",
"type": "core",
"status": "legacy-bridge",
"description": "Vertrag für den bestehenden Vendoo-Bereich System Kernel; schrittweise Migration ohne Funktionsbruch.",
"requires": [],
"permissions": [
"dashboard.view"
],
"provides": [
"platform.lifecycle",
"platform.events",
"platform.features"
],
"consumes": [],
"owns": [
"system.runtime",
"system.health"
]
}
-27
View File
@@ -1,27 +0,0 @@
import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from '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 getThemeService() {
if (!service) throw new PlatformError('Theme-Modul ist noch nicht gestartet.', { code: 'THEME_MODULE_NOT_READY', status: 503, expose: true });
return service;
}
export function createThemesModule() {
return {
manifest,
lifecycle: {
async start() {
const { createThemeService } = await import('./service.mjs');
service = createThemeService();
},
async stop() { service = null; },
async health() { return getThemeService().health(); },
},
};
}
@@ -1,21 +0,0 @@
CREATE TABLE IF NOT EXISTS theme_profiles (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
base_theme TEXT NOT NULL DEFAULT 'light',
values_json TEXT NOT NULL DEFAULT '{}',
created_by INTEGER,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS user_theme_preferences (
user_id INTEGER PRIMARY KEY,
theme_id TEXT NOT NULL DEFAULT 'inherit',
density TEXT NOT NULL DEFAULT 'comfortable',
reduced_motion TEXT NOT NULL DEFAULT 'system',
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_theme_profiles_updated ON theme_profiles(updated_at DESC);
-30
View File
@@ -1,30 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.themes",
"name": "Themes & Design System",
"version": "1.1.0",
"type": "feature",
"status": "native",
"description": "Natives Theme- und Design-System-Modul mit sicherem Token-Editor, Benutzerpräferenzen und Accessibility-Prüfungen.",
"requires": [
"vendoo.system"
],
"permissions": [
"settings.manage",
"themes.use"
],
"provides": [
"themes.tokens",
"themes.runtime",
"themes.profiles",
"themes.accessibility"
],
"consumes": [
"platform.events"
],
"owns": [
"themes.definitions",
"themes.user-preferences",
"themes.system-default"
]
}
-95
View File
@@ -1,95 +0,0 @@
import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { db } from '../../../lib/db.mjs';
const here = dirname(fileURLToPath(import.meta.url));
const migrationSql = readFileSync(join(here, 'migrations', '001_theme_profiles.sql'), 'utf8');
export function ensureThemeSchema() {
db.exec(migrationSql);
}
function parseProfile(row) {
if (!row) return null;
let values = {};
try { values = JSON.parse(row.values_json || '{}'); } catch {}
return { ...row, values };
}
export function listThemeProfiles() {
ensureThemeSchema();
return db.prepare('SELECT * FROM theme_profiles ORDER BY name COLLATE NOCASE, id').all().map(parseProfile);
}
export function getThemeProfile(id) {
ensureThemeSchema();
return parseProfile(db.prepare('SELECT * FROM theme_profiles WHERE id = ?').get(id));
}
export function saveThemeProfile(profile, userId) {
ensureThemeSchema();
db.prepare(`INSERT INTO theme_profiles (id, name, base_theme, values_json, created_by)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET name = excluded.name, base_theme = excluded.base_theme,
values_json = excluded.values_json, updated_at = datetime('now')`).run(
profile.id, profile.name, profile.base_theme, JSON.stringify(profile.values || {}), userId || null,
);
return getThemeProfile(profile.id);
}
export function deleteThemeProfile(id) {
ensureThemeSchema();
const transaction = db.transaction(() => {
db.prepare("UPDATE user_theme_preferences SET theme_id = 'inherit', updated_at = datetime('now') WHERE theme_id = ?").run(id);
const system = db.prepare("SELECT value FROM settings WHERE key = 'theme_system_default'").get();
if (system?.value === id) db.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES ('theme_system_default', 'system')").run();
return db.prepare('DELETE FROM theme_profiles WHERE id = ?').run(id).changes;
});
return transaction();
}
export function getUserThemePreference(userId) {
ensureThemeSchema();
return db.prepare('SELECT theme_id, density, reduced_motion, updated_at FROM user_theme_preferences WHERE user_id = ?').get(userId) || {
theme_id: 'inherit', density: 'comfortable', reduced_motion: 'system', updated_at: null,
};
}
export function saveUserThemePreference(userId, preference) {
ensureThemeSchema();
db.prepare(`INSERT INTO user_theme_preferences (user_id, theme_id, density, reduced_motion)
VALUES (?, ?, ?, ?)
ON CONFLICT(user_id) DO UPDATE SET theme_id = excluded.theme_id, density = excluded.density,
reduced_motion = excluded.reduced_motion, updated_at = datetime('now')`).run(
userId, preference.theme_id, preference.density, preference.reduced_motion,
);
return getUserThemePreference(userId);
}
function getSetting(key, fallback) {
const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key);
return row?.value ?? fallback;
}
function setSetting(key, value) {
db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)').run(key, String(value));
}
export function getThemeSystemDefault() {
return {
theme_id: getSetting('theme_system_default', 'system'),
density: getSetting('theme_system_density', 'comfortable'),
reduced_motion: getSetting('theme_system_reduced_motion', 'system'),
};
}
export function saveThemeSystemDefault(preference) {
const transaction = db.transaction(() => {
setSetting('theme_system_default', preference.theme_id);
setSetting('theme_system_density', preference.density);
setSetting('theme_system_reduced_motion', preference.reduced_motion);
});
transaction();
return getThemeSystemDefault();
}
-85
View File
@@ -1,85 +0,0 @@
import { PlatformError } from '../../kernel/errors.mjs';
import {
evaluateThemeAccessibility, getBaseThemeValues, getBuiltinThemes, getThemeContract,
resolveThemeValues, validateThemeDefinition, validateThemePreference,
} from '../../core/themes/theme-contract.mjs';
import {
deleteThemeProfile, ensureThemeSchema, getThemeProfile, getThemeSystemDefault,
getUserThemePreference, listThemeProfiles, saveThemeProfile, saveThemeSystemDefault,
saveUserThemePreference,
} from './repository.mjs';
function decorate(profile) {
return {
...profile,
builtin: false,
resolved_values: resolveThemeValues({ baseTheme: profile.base_theme, values: profile.values }),
accessibility: evaluateThemeAccessibility({ baseTheme: profile.base_theme, values: profile.values }),
};
}
function customExists(id) {
return Boolean(getThemeProfile(id));
}
function effectivePreference(userPreference, systemDefault) {
return userPreference.theme_id === 'inherit' ? { ...systemDefault, inherited: true } : { ...userPreference, inherited: false };
}
export function createThemeService() {
ensureThemeSchema();
return Object.freeze({
health() {
ensureThemeSchema();
return { ok: true, profiles: listThemeProfiles().length, contract: getThemeContract().contractVersion };
},
catalog(userId) {
const custom = listThemeProfiles().map(decorate);
const preference = getUserThemePreference(userId);
const systemDefault = getThemeSystemDefault();
return {
contract: getThemeContract(),
builtin: getBuiltinThemes(),
custom,
preference,
system_default: systemDefault,
effective_preference: effectivePreference(preference, systemDefault),
};
},
validate(input) {
const theme = validateThemeDefinition(input, { allowExistingId: true });
return { theme, resolved_values: resolveThemeValues({ baseTheme: theme.base_theme, values: theme.values }), accessibility: theme.accessibility };
},
create(input, userId) {
const theme = validateThemeDefinition(input);
if (getThemeProfile(theme.id)) throw new PlatformError('Theme-ID ist bereits vorhanden.', { code: 'THEME_DUPLICATE', status: 409, expose: true });
return decorate(saveThemeProfile(theme, userId));
},
update(id, input, userId) {
if (!getThemeProfile(id)) throw new PlatformError('Theme wurde nicht gefunden.', { code: 'THEME_NOT_FOUND', status: 404, expose: true });
const theme = validateThemeDefinition({ ...input, id }, { allowExistingId: true });
return decorate(saveThemeProfile(theme, userId));
},
remove(id) {
if (!getThemeProfile(id)) throw new PlatformError('Theme wurde nicht gefunden.', { code: 'THEME_NOT_FOUND', status: 404, expose: true });
deleteThemeProfile(id);
return { ok: true, id };
},
saveUserPreference(userId, input) {
const preference = validateThemePreference(input, { customThemeExists: customExists });
return saveUserThemePreference(userId, preference);
},
saveSystemDefault(input) {
const preference = validateThemePreference(input, { customThemeExists: customExists });
if (preference.theme_id === 'inherit') throw new PlatformError('Der Systemstandard kann nicht auf „Vererben“ gesetzt werden.', { code: 'THEME_SYSTEM_INHERIT_INVALID', status: 400, expose: true });
return saveThemeSystemDefault(preference);
},
exportTheme(id) {
const profile = getThemeProfile(id);
if (!profile) throw new PlatformError('Theme wurde nicht gefunden.', { code: 'THEME_NOT_FOUND', status: 404, expose: true });
return { schemaVersion: 1, type: 'vendoo-theme', theme: { id: profile.id, name: profile.name, base_theme: profile.base_theme, values: profile.values } };
},
});
}
export { getBaseThemeValues };
-24
View File
@@ -1,24 +0,0 @@
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { PlatformError } from '../../kernel/errors.mjs';
const here = dirname(fileURLToPath(import.meta.url));
const manifest = JSON.parse(readFileSync(join(here, 'module.json'), 'utf8'));
let service = null;
export function getUsersService() {
if (!service) throw new PlatformError('Benutzer-Modul ist noch nicht gestartet.', { code: 'USERS_MODULE_NOT_READY', status: 503, expose: true });
return service;
}
export function createUsersModule() {
return {
manifest,
lifecycle: {
async start() { service = (await import('./service.mjs')).createUsersService(); },
async stop() { service = null; },
async health() { return getUsersService().health(); },
},
};
}
-14
View File
@@ -1,14 +0,0 @@
{
"schemaVersion": 1,
"id": "vendoo.users",
"name": "Users & Roles",
"version": "2.0.0",
"type": "core",
"status": "native",
"description": "Native Benutzer-, Profil-, Rollen- und Passwortverwaltung mit Schutz des letzten Administrators.",
"requires": ["vendoo.auth", "vendoo.sessions"],
"permissions": ["users.manage"],
"provides": ["users.directory", "users.roles"],
"consumes": ["auth.identity", "sessions.lifecycle"],
"owns": ["users.accounts", "users.profiles"]
}

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