Preview: Deploy auf fremde Subdomain sperren

Ein kopiertes Repo (git clone + push, Vorlage) bringt die
.webklar-preview.json des Originals mit. Sein erster Push ueberschrieb
die Preview des Originals, danach scheiterte der Webhook mit 500
(Unique-Index auf subdomain). Fast passiert bei Olzinger -> HSK-Vorlage,
04.10.2026.

- foreignSubdomainOwner: Subdomain gehoert einem anderen Repo oder einem
  Projekt ohne Repo; ein in Gitea umgezogenes Repo (Weiterleitung) zaehlt
  nicht als fremd
- Webhook: fremde Subdomain -> kein Deploy, Projekt ohne Preview (neu:
  synced, Repo-Name statt fremdem Anzeigenamen); eine schon vergebene
  eigene Preview bleibt, WOMS zeigt den Grund als Preview-Fehler
- runDeploy prueft fuer alle anderen Wege (WOMS, Reaktivieren), vor dem
  Runner, damit ein gesperrter Aufruf keinen laufenden Bau verwaist;
  Appwrite-Fehler bei der Pruefung blockieren dort nicht (Webhook streng)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-04 19:46:27 +00:00
1 parent 4fc4c70b7d
commit 75794f14d5
4 files changed
+370 -5

No files matched your search

+68
View File
@@ -802,7 +802,75 @@ async function runDeployViaRunner(repoFullName, branch, previewConfig, { project
}
}
/**
* Gehoert `subdomain` schon einem ANDEREN Projekt? Ein kopiertes Repo (git clone + push, Vorlage)
* bringt die .webklar-preview.json des Originals mit - ohne diese Pruefung ueberschrieb sein erster
* Push die Preview des Originals (Olzinger -> HSK-Vorlage, 04.10.2026). Ein in Gitea umbenanntes
* oder umgezogenes Repo ist nicht fremd: Gitea leitet den alten Namen auf `repoFullName` weiter.
* Ein Projekt ohne Repo (z. B. die Relaunch-Vorschau) haelt seine Subdomain ebenfalls.
* @returns {Promise<string>} Repo-Name bzw. Projekt des fremden Besitzers, sonst ''
*/
export async function foreignSubdomainOwner(repoFullName, subdomain) {
const own = String(repoFullName || '').toLowerCase()
// ohne Repo gibt es nichts zu klonen - und ein Projekt ohne Repo sperrt sich nicht selbst
if (!subdomain || !own) return ''
const docs = await listDocuments(config.collections.websiteProjects, [
Query.equal('subdomain', subdomain),
Query.limit(5),
])
for (const doc of docs) {
const other = String(doc.repoFullName || '')
if (!other) return `Projekt „${doc.projectName || doc.$id}“ (ohne Repo)`
if (other.toLowerCase() === own) continue
let resolved = ''
try {
resolved = String((await giteaFetch(`/repos/${other}`))?.full_name || '')
} catch {
// geloescht oder Gitea nicht erreichbar: im Zweifel fremd
}
if (resolved.toLowerCase() !== own) return other
}
return ''
}
/**
* Ergebnis fuer einen gesperrten Deploy oder null. `failOpen`: ist die Pruefung selbst nicht
* moeglich (Appwrite weg), trotzdem deployen - so nutzt es runDeploy; der Webhook prueft vorher
* streng, und die WOMS-Wege lesen das Projekt ohnehin zuerst aus Appwrite.
*/
export async function foreignSubdomainBlock(repoFullName, subdomain, { failOpen = false } = {}) {
let owner
try {
owner = await foreignSubdomainOwner(repoFullName, subdomain)
} catch (err) {
if (!failOpen) throw err
console.error(`[previewDeploy] ${repoFullName}: Besitzer der Subdomain "${subdomain}" nicht pruefbar, deploye trotzdem:`, err.message)
return null
}
if (!owner) return null
console.warn(`[previewDeploy] ${repoFullName}: Subdomain "${subdomain}" gehoert ${owner} - kein Deploy`)
return { deployed: false, blocked: true, reason: 'subdomain_foreign', subdomain, owner }
}
/**
* Muss runDeploy den Besitzer der Subdomain pruefen? Nicht, wenn der Aufrufer es schon getan hat
* (`subdomainChecked`, der Webhook) oder wenn es die schon vergebene Subdomain des uebergebenen
* Projekts selbst ist.
*/
function needsOwnerCheck(repoFullName, subdomain, { project, subdomainChecked } = {}) {
if (subdomainChecked) return false
const same = (a, b) => String(a || '').toLowerCase() === String(b || '').toLowerCase()
return !(project?.subdomain && same(project.subdomain, subdomain) && same(project.repoFullName, repoFullName))
}
export async function runDeploy(repoFullName, branch, previewConfig, options = {}) {
// Vor dem Runner pruefen: ein gesperrter Aufruf belegt dort keinen Platz und laesst so den
// Verfolger eines laufenden Baus desselben Repos in Ruhe.
const target = previewConfig.subdomain || repoFullName.split('/').pop()
if (needsOwnerCheck(repoFullName, target, options)) {
const blocked = await foreignSubdomainBlock(repoFullName, target, { failOpen: true })
if (blocked) return blocked
}
if (config.previewRunner.enabled) {
return runDeployViaRunner(repoFullName, branch, previewConfig, options)
}