feat: release per URL registrieren (gitea-asset) - /api/v1/releases/from-url

- Admin-Endpoint laedt die ZIP einmal von einer URL (z.B. Gitea-Release-Asset),
  speichert sie lokal; Kunden-Download bleibt token-/lizenzgeschuetzt.
- Guards: Produkt/Version/URL-Pruefung, GITEA_BASE_URL-Restriktion, DNS-SSRF-Schutz,
  optional GITEA_TOKEN fuer private Repos, ZIP-Signatur + 50MB-Limit.
- env-Beispiele + README + Tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-06-07 15:51:19 +02:00
parent 576ad1f74a
commit e691b675cd
4 changed files with 102 additions and 0 deletions

View File

@@ -202,6 +202,19 @@ try {
const relList = await admin('GET', '/api/v1/releases/gdpr-content-blocker');
ok('release list shows 1.1.0', relList.json.ok && relList.json.releases.some((r) => r.version === '1.1.0'), JSON.stringify(relList.json));
// ── Release from URL: guards (real fetch not exercised offline) ──
const fuProd = await admin('POST', '/api/v1/releases/from-url', { product: 'nope', version: '1.2.0', zip_url: 'https://example.com/a.zip' });
ok('from-url unknown product → 404', fuProd.status === 404, JSON.stringify(fuProd.json));
const fuVer = await admin('POST', '/api/v1/releases/from-url', { product: P, version: 'x', zip_url: 'https://example.com/a.zip' });
ok('from-url bad version → 400', fuVer.status === 400, JSON.stringify(fuVer.json));
const fuUrl = await admin('POST', '/api/v1/releases/from-url', { product: P, version: '1.2.0', zip_url: 'not-a-url' });
ok('from-url invalid url → 400', fuUrl.status === 400, JSON.stringify(fuUrl.json));
const fuLocal = await admin('POST', '/api/v1/releases/from-url', { product: P, version: '1.2.0', zip_url: 'http://127.0.0.1/a.zip' });
ok('from-url private host → 400', fuLocal.status === 400, JSON.stringify(fuLocal.json));
} catch (e) {
console.error(e);
fail++;