feat(tasks): Aufgabe als Git-Issue anlegen, Zustand wandert mit
Die Zuordnung stand im Schema laengst – sourceIssue, sourceUrl und sourceRepositoryId samt Eindeutigkeit je Repository, gefuellt von der Uebernahme aus Issues seit v0.9.0. Es fehlte die Gegenrichtung. Ein Knopf im Aufgabendialog legt das Issue an; Nummer und Adresse landen in denselben Feldern, der Dublettenschutz gilt also fuer beide Wege. In den Rumpf kommt ein Rueckverweis auf das Projekt. Ausdruecklich per Knopf und nie nebenbei: was in ein fremdes System geschrieben wird, soll jemand gewollt haben. Ist die Aufgabe erst verknuepft, wandert der Zustand allerdings mit – erledigt schliesst das Issue, ein Zurueckziehen oeffnet es wieder. Fehlertolerant: ein nicht erreichbares Git-System darf das Abhaken nicht verhindern. Die Zustaende passen nicht aufeinander, und das bleibt so. Nebula kennt vier, ein Issue zwei; nach aussen geht nur die Grenze um ERLEDIGT. Ein Rundlauf verliert damit IN ARBEIT und BLOCKIERT. Labels waeren die Alternative, wuerden aber den Issue-Tracker mit Zustaenden fuellen, die dort niemand pflegt. Kein Widerspruch zu "keine eigenen Git-Operationen": Issues laufen ueber die HTTP-API der Provider. Dafuer gibt es jetzt sendJson neben getJson – durch dieselbe SSRF-Schleuse, aber ohne Umleitungen zu folgen. Ein 307 auf eine interne Adresse naehme den Rumpf und den Token im Header mit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -98,6 +98,83 @@ export async function getJson<T>(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schreibender Aufruf – POST, PATCH oder PUT.
|
||||
*
|
||||
* Bewusst dieselbe Schleuse wie getJson: die Basis-URL stammt aus einem
|
||||
* Formular, und ohne die Pruefung waere ein schreibender Aufruf der weitaus
|
||||
* unangenehmere Weg nach innen. Redirects werden hier **nicht** verfolgt.
|
||||
* Ein 307 auf eine interne Adresse wuerde den Rumpf mitnehmen, und der
|
||||
* enthaelt bei uns immer den Token im Header – lieber ein Fehler als eine
|
||||
* Anfrage, die woanders landet.
|
||||
*/
|
||||
export async function sendJson<T>(
|
||||
url: string,
|
||||
method: "POST" | "PATCH" | "PUT",
|
||||
body: unknown,
|
||||
options: FetchOptions = {},
|
||||
): Promise<T> {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(
|
||||
() => controller.abort(),
|
||||
options.timeoutMs ?? DEFAULT_TIMEOUT,
|
||||
);
|
||||
|
||||
try {
|
||||
await assertAllowedUrl(url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Nebula-Dashboard",
|
||||
...options.headers,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
cache: "no-store",
|
||||
redirect: "manual",
|
||||
});
|
||||
|
||||
// Bei redirect: "manual" kommt der Umleitungsstatus durch, statt dass
|
||||
// fetch ihm folgt. Das ist hier gewollt und ein Fehlerfall.
|
||||
if (response.status >= 300 && response.status < 400) {
|
||||
throw new GitApiError(
|
||||
"Das Git-System hat umgeleitet. Schreibende Aufrufe folgen dem nicht – " +
|
||||
"bitte die Basis-URL der Verbindung auf die endgueltige Adresse setzen.",
|
||||
response.status,
|
||||
url,
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => "");
|
||||
throw new GitApiError(friendlyMessage(response.status, url, text), response.status, url);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
if (!text) return null as T;
|
||||
return JSON.parse(text) as T;
|
||||
} catch (error) {
|
||||
if (error instanceof GitApiError) throw error;
|
||||
if (error instanceof BlockedHostError) {
|
||||
throw new GitApiError(error.message, 403, url);
|
||||
}
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
throw new GitApiError(
|
||||
`Zeitueberschreitung nach ${options.timeoutMs ?? DEFAULT_TIMEOUT} ms`,
|
||||
408,
|
||||
url,
|
||||
);
|
||||
}
|
||||
const reason = error instanceof Error ? error.message : String(error);
|
||||
throw new GitApiError(`Verbindung fehlgeschlagen: ${reason}`, 0, url);
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
/** Wie getJson, gibt aber bei jedem Fehler den Fallback zurueck. */
|
||||
export async function getJsonSafe<T>(
|
||||
url: string,
|
||||
|
||||
Reference in New Issue
Block a user