import { spawn } from "node:child_process"; import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto"; import { mkdtempSync, readFileSync, rmSync } from "node:fs"; import { mkdir, open, readFile, readdir, rename, stat, unlink } from "node:fs/promises"; import { createServer } from "node:http"; import { tmpdir } from "node:os"; import { join } from "node:path"; export function createGithubSyncHandler(config) { const controller = config.controller ?? createDurableSyncController(config); if (!Number.isInteger(config.responseBudgetMs) || config.responseBudgetMs < 1) throw new Error("responseBudgetMs must be a positive integer"); return async (req, res) => { const responseDeadlineMs = Date.now() + config.responseBudgetMs; const requestContext = { deliveryId: null, repository: null, ref: null, requestedCommit: null }; try { const url = new URL(req.url || "/", "http://gitea-github-sync.local"); if (req.method === "GET" && url.pathname === "/healthz") { const readiness = controller.readiness(); writeJson(res, readiness.ready ? 200 : 503, { ok: readiness.ready, repos: config.repos.length, durableInbox: readiness, valuesPrinted: false }); return; } if (req.method === "GET" && url.pathname === "/status") { const status = await controller.status(); writeJson(res, status.ready ? 200 : 503, { ok: status.ready, ...status, valuesPrinted: false }); return; } if (req.method !== "POST" || url.pathname !== config.path) { writeJson(res, 404, { ok: false, error: "not-found", valuesPrinted: false }); return; } const deliveryId = singleHeader(req.headers["x-github-delivery"]); requestContext.deliveryId = deliveryId || null; const body = await waitWithinResponseBudget( readBody(req, config.maxBodyBytes), responseDeadlineMs, "request-body-response-deadline-exhausted", ); verifySignature(req.headers["x-hub-signature-256"], body, config.webhookSecret); const event = String(req.headers["x-github-event"] || ""); if (!deliveryId) throw new HttpError(400, "missing-github-delivery-id"); if (event === "ping") { writeJson(res, 200, { ok: true, event, deliveryId, disposition: "pong", valuesPrinted: false }); return; } if (event !== "push") { writeJson(res, 202, { ok: true, event, deliveryId, disposition: "ignored-event", valuesPrinted: false }); return; } const payload = parsePayload(body); const repository = String(payload?.repository?.full_name || ""); const ref = String(payload?.ref || ""); const requestedCommit = String(payload?.after || ""); Object.assign(requestContext, { repository, ref, requestedCommit }); const repo = config.repos.find((item) => item?.upstream?.repository === repository && ref === `refs/heads/${item?.upstream?.branch}`); if (!repo || /^0+$/u.test(requestedCommit)) { writeJson(res, 202, { ok: true, event, deliveryId, repository, ref, disposition: "ignored-repo-ref-or-delete", valuesPrinted: false }); return; } if (!/^[0-9a-f]{40,64}$/u.test(requestedCommit)) throw new HttpError(400, "invalid-push-after"); const delivery = { deliveryId, repository, ref, requestedCommit, payloadSha: createHash("sha256").update(body).digest("hex"), receivedAt: new Date().toISOString(), }; const acceptPromise = Promise.resolve().then(() => controller.accept(repo, delivery)); let result; try { result = await waitWithinResponseBudget( acceptPromise, responseDeadlineMs, "durable-acceptance-response-deadline-exhausted", ); } catch (error) { if (error instanceof HttpError && error.message === "durable-acceptance-response-deadline-exhausted") { void acceptPromise.then( (lateResult) => safeLog(config.log, deliveryLog("github-to-gitea-delivery-accepted-after-response-deadline", repo, delivery, { ok: true, state: lateResult.state, duplicate: lateResult.duplicate, })), (lateError) => safeLog(config.log, deliveryLog("github-to-gitea-delivery-persist-failed-after-response-deadline", repo, delivery, { ok: false, errorType: "durable-acceptance-late-failure", error: redact(String(lateError?.message || lateError)), })), ); } throw error; } const refsCommitted = result.state === "committed"; writeJson(res, 202, { ok: true, event, deliveryId, repository, ref, repo: repo.key, requestedCommit, state: result.state, disposition: result.duplicate ? "duplicate-durable-inbox-record" : "durable-inbox-accepted", refsCommitted, ...(refsCommitted && result.result ? { result: result.result } : {}), valuesPrinted: false, }); } catch (error) { const status = error instanceof HttpError ? error.status : 503; safeLog(config.log, { event: "github-to-gitea-request-rejected", ...requestContext, ok: false, status, error: redact(String(error?.message || error)), valuesPrinted: false, }); writeJson(res, status, { ok: false, error: error instanceof HttpError ? error.message : "internal-sync-error", valuesPrinted: false }); } }; } export function createDurableInboxStore(options) { const inboxDirectory = options.path; const maxBytes = options.maxBytes; const now = options.now ?? Date.now; let totalBytes = 0; let nextAcceptedSequence = 1; let initialized = false; let metadata = null; let mutationTail = Promise.resolve(); function withMutationLock(task) { const result = mutationTail.then(task); mutationTail = result.catch(() => undefined); return result; } async function initialize() { await mkdir(inboxDirectory, { recursive: true, mode: 0o700 }); const probePath = join(inboxDirectory, `.readiness-${randomUUID()}`); await durableWrite(probePath, '{"ok":true}\n'); await unlink(probePath); await syncDirectory(inboxDirectory); const metadataPath = join(inboxDirectory, ".journal-metadata"); try { metadata = validateJournalMetadata(JSON.parse(await readFile(metadataPath, "utf8"))); } catch (error) { if (error?.code !== "ENOENT") throw new DurableInboxError(503, "durable-inbox-metadata-invalid"); metadata = validateJournalMetadata({ version: 1, kind: "GiteaGithubSyncDurableInbox", createdAt: new Date(now()).toISOString(), }); await durableWrite(metadataPath, serializeRecord(metadata)); } totalBytes = 0; nextAcceptedSequence = 1; for (const entry of await readdir(inboxDirectory, { withFileTypes: true })) { if (!entry.isFile() || !entry.name.endsWith(".json")) continue; const path = join(inboxDirectory, entry.name); const record = await readRecordPath(path); nextAcceptedSequence = Math.max(nextAcceptedSequence, record.acceptedSequence + 1); totalBytes += (await stat(path)).size; } initialized = true; return { totalBytes, maxBytes }; } async function accept(repo, delivery) { return await withMutationLock(async () => { assertInitialized(); const path = recordPath(inboxDirectory, delivery.deliveryId); const existing = await readRecordPath(path, true); const fingerprint = deliveryFingerprint(repo, delivery); if (existing) { if (existing.fingerprint !== fingerprint) throw new DurableInboxError(409, "github-delivery-id-payload-conflict"); return { record: existing, duplicate: true }; } const now = delivery.receivedAt || new Date().toISOString(); const record = { version: 1, deliveryId: delivery.deliveryId, fingerprint, repoKey: repo.key, repository: delivery.repository, ref: delivery.ref, requestedCommit: delivery.requestedCommit, payloadSha: delivery.payloadSha, receivedAt: now, acceptedAt: now, acceptedSequence: nextAcceptedSequence, updatedAt: now, state: "accepted", totalAttempts: 0, cycleAttempt: 0, nextAttemptAt: now, result: null, lastError: null, }; const serialized = serializeRecord(validateInboxRecord(record)); if (totalBytes + Buffer.byteLength(serialized) > maxBytes) { throw new DurableInboxError(503, "durable-inbox-capacity-exhausted"); } await durableWrite(path, serialized); totalBytes += Buffer.byteLength(serialized); nextAcceptedSequence += 1; return { record, duplicate: false }; }); } async function get(deliveryId) { assertInitialized(); return await readRecordPath(recordPath(inboxDirectory, deliveryId), true); } async function list() { assertInitialized(); const records = []; for (const entry of await readdir(inboxDirectory, { withFileTypes: true })) { if (!entry.isFile() || !entry.name.endsWith(".json")) continue; records.push(await readRecordPath(join(inboxDirectory, entry.name))); } return sortInboxRecordsByAcceptedSequence(records); } async function update(deliveryId, updater) { return await withMutationLock(async () => { assertInitialized(); const path = recordPath(inboxDirectory, deliveryId); const previous = await readRecordPath(path); const previousSize = (await stat(path)).size; const next = updater(structuredClone(previous)); const identityFields = ["version", "deliveryId", "fingerprint", "repoKey", "repository", "ref", "requestedCommit", "payloadSha", "receivedAt", "acceptedAt", "acceptedSequence"]; if (!next || identityFields.some((field) => next[field] !== previous[field])) { throw new DurableInboxError(503, "durable-inbox-invalid-record-update"); } const serialized = serializeRecord(validateInboxRecord(next)); await durableWrite(path, serialized); totalBytes += Buffer.byteLength(serialized) - previousSize; return next; }); } async function recoverProcessing(now) { const records = await list(); for (const record of records) { if (record.state !== "processing") continue; await update(record.deliveryId, (current) => ({ ...current, state: "accepted", updatedAt: now, nextAttemptAt: now, recoveredAfterRestart: true, lastError: { errorType: "worker-restarted-during-processing", retryable: true }, })); } } async function cleanupCommitted(cutoffMs) { return await withMutationLock(async () => { assertInitialized(); let removed = 0; const entries = (await readdir(inboxDirectory, { withFileTypes: true })) .filter((entry) => entry.isFile() && entry.name.endsWith(".json")); const records = []; for (const entry of entries) { const path = join(inboxDirectory, entry.name); records.push({ entry, path, record: await readRecordPath(path) }); } const latestCommittedByRepo = new Map(); for (const item of records) { if (item.record.state !== "committed") continue; const current = latestCommittedByRepo.get(item.record.repoKey); if (!current || item.record.acceptedSequence > current.record.acceptedSequence) { latestCommittedByRepo.set(item.record.repoKey, item); } } for (const item of records) { const { path, record } = item; if (record.state !== "committed" || latestCommittedByRepo.get(record.repoKey) === item || Date.parse(record.committedAt) > cutoffMs) continue; const size = (await stat(path)).size; await unlink(path); totalBytes = Math.max(0, totalBytes - size); removed += 1; } if (removed > 0) await syncDirectory(inboxDirectory); return removed; }); } function capacityAvailable() { return initialized && totalBytes < maxBytes; } function usage() { return { totalBytes, maxBytes, capacityAvailable: capacityAvailable() }; } function journalMetadata() { assertInitialized(); return structuredClone(metadata); } function assertInitialized() { if (!initialized) throw new DurableInboxError(503, "durable-inbox-unavailable"); } return { initialize, accept, get, list, update, recoverProcessing, cleanupCommitted, capacityAvailable, usage, journalMetadata }; } export function createDurableSyncController(options) { const now = options.now ?? Date.now; const store = options.store ?? createDurableInboxStore(options.inbox); const retry = options.workerRetry; const workerAbort = new AbortController(); const executor = options.executor ?? createSyncCoordinator({ ...options, signal: workerAbort.signal, responseBudgetMs: retry.attemptTimeoutMs, retryMaxAttempts: 1, retryInitialDelayMs: retry.initialDelayMs, retryMaxDelayMs: retry.maxDelayMs, }); const repoByKey = new Map(options.repos.map((repo) => [repo.key, repo])); const repoTails = new Map(); const activeDeliveries = new Set(); const inFlight = new Set(); let storageReady = false; let storageError = "durable-inbox-starting"; let capacityExhausted = false; let stopped = true; let scanTimer = null; let scanRunning = false; let lastCleanupAt = 0; async function start() { if (!stopped) return; stopped = false; try { await store.initialize(); await store.recoverProcessing(new Date(now()).toISOString()); storageReady = true; storageError = null; capacityExhausted = false; safeLog(options.log, { event: "github-to-gitea-inbox-ready", ...store.usage(), valuesPrinted: false }); wake(0); } catch (error) { storageReady = false; storageError = typedStorageError(error); safeLog(options.log, { event: "github-to-gitea-inbox-unavailable", ok: false, errorType: storageError, error: redact(String(error?.message || error)), valuesPrinted: false }); wake(retry.scanIntervalMs); } } async function accept(repo, delivery) { if (!storageReady) throw new HttpError(503, storageError || "durable-inbox-unavailable"); try { const accepted = await store.accept(repo, delivery); capacityExhausted = false; safeLog(options.log, deliveryLog("github-to-gitea-delivery-accepted", repo, delivery, { ok: true, state: accepted.record.state, duplicate: accepted.duplicate, })); wake(0); return { state: accepted.record.state, duplicate: accepted.duplicate, result: accepted.record.state === "committed" && accepted.record.result ? publicSyncResult(accepted.record.result) : null, }; } catch (error) { if (error instanceof DurableInboxError && error.status === 409) throw new HttpError(409, error.message); if (!(error instanceof DurableInboxError && error.message === "durable-inbox-capacity-exhausted")) { storageReady = false; storageError = typedStorageError(error); } else { capacityExhausted = true; } safeLog(options.log, deliveryLog("github-to-gitea-delivery-persist-failed", repo, delivery, { ok: false, state: "rejected", errorType: error instanceof DurableInboxError ? error.message : "durable-inbox-write-failed", error: redact(String(error?.message || error)), })); throw new HttpError(503, error instanceof DurableInboxError ? error.message : "durable-inbox-write-failed"); } } function readiness() { const usage = store.usage(); return { ready: storageReady && usage.capacityAvailable && !capacityExhausted, storageReady, capacityAvailable: usage.capacityAvailable && !capacityExhausted, totalBytes: usage.totalBytes, maxBytes: usage.maxBytes, errorType: storageReady ? (usage.capacityAvailable && !capacityExhausted ? null : "durable-inbox-capacity-exhausted") : storageError, }; } async function status() { let records = []; if (storageReady) { try { records = await store.list(); } catch (error) { storageReady = false; storageError = typedStorageError(error); } } const counts = { accepted: 0, processing: 0, committed: 0, failed: 0 }; for (const record of records) { if (Object.hasOwn(counts, record.state)) counts[record.state] += 1; } return { ...readiness(), journal: storageReady ? store.journalMetadata() : null, counts, deliveries: statusRecords(records).map(publicInboxRecord), failedDeliveryRecovery: await readFailedDeliveryRecoveryStatus(options.failedDeliveryRecoveryStatePath), }; } function wake(delayMs) { if (stopped) return; if (scanTimer !== null) clearTimeout(scanTimer); scanTimer = setTimeout(() => void scan(), Math.max(0, delayMs)); } async function scan() { if (stopped || scanRunning) return; scanRunning = true; try { if (!storageReady) { await store.initialize(); await store.recoverProcessing(new Date(now()).toISOString()); storageReady = true; storageError = null; } if (now() - lastCleanupAt >= options.inbox.cleanupIntervalMs) { const removed = await store.cleanupCommitted(now() - options.inbox.committedRetentionMs); lastCleanupAt = now(); if (removed > 0) { capacityExhausted = false; safeLog(options.log, { event: "github-to-gitea-inbox-retention", removed, ...store.usage(), valuesPrinted: false }); } } const currentTime = now(); for (const record of sortInboxRecordsByAcceptedSequence(await store.list())) { if (!["accepted", "failed"].includes(record.state)) continue; if (Date.parse(record.nextAttemptAt || record.receivedAt) > currentTime) continue; schedule(record); } } catch (error) { storageReady = false; storageError = typedStorageError(error); safeLog(options.log, { event: "github-to-gitea-inbox-scan-failed", ok: false, errorType: storageError, error: redact(String(error?.message || error)), valuesPrinted: false }); } finally { scanRunning = false; wake(retry.scanIntervalMs); } } function schedule(record) { if (activeDeliveries.has(record.deliveryId) || stopped) return; activeDeliveries.add(record.deliveryId); const previous = repoTails.get(record.repoKey) ?? Promise.resolve(); const task = previous.catch(() => undefined).then(() => processRecord(record.deliveryId)) .catch((error) => { if (error instanceof DurableInboxError) { storageReady = false; storageError = typedStorageError(error); } safeLog(options.log, { event: "github-to-gitea-worker-exception", deliveryId: record.deliveryId, repo: record.repoKey, ok: false, errorType: "durable-worker-exception", error: redact(String(error?.message || error)), valuesPrinted: false, }); }) .finally(() => { activeDeliveries.delete(record.deliveryId); inFlight.delete(task); wake(0); }); repoTails.set(record.repoKey, task.catch(() => undefined)); inFlight.add(task); } async function processRecord(deliveryId) { let record = await store.get(deliveryId); if (!record || record.state === "committed") return; const repo = repoByKey.get(record.repoKey); if (!repo) { await markFailed(record, { ok: false, retryable: false, errorType: "repository-config-missing" }); return; } const attemptStartedAt = new Date(now()).toISOString(); record = await store.update(deliveryId, (current) => ({ ...current, state: "processing", updatedAt: attemptStartedAt, processingAt: attemptStartedAt, totalAttempts: current.totalAttempts + 1, cycleAttempt: current.cycleAttempt + 1, })); const delivery = recordDelivery(record); safeLog(options.log, deliveryLog("github-to-gitea-delivery-processing", repo, delivery, { ok: true, state: "processing", totalAttempts: record.totalAttempts, cycleAttempt: record.cycleAttempt, })); let result; try { result = validateExecutorTerminalResult(await executor.execute(repo, delivery)); } catch (error) { await markFailed(record, { ok: false, retryable: true, errorType: workerAbort.signal.aborted || stopped ? "source-sync-executor-aborted" : error instanceof ExecutorTerminalResultError ? "source-sync-executor-invalid-result" : "source-sync-executor-threw", error: redact(String(error?.message || error)), }); return; } if (result.ok) { const committedAt = new Date(now()).toISOString(); await store.update(deliveryId, (current) => ({ ...current, state: "committed", updatedAt: committedAt, committedAt, nextAttemptAt: null, result: publicSyncResult(result), lastError: null, })); safeLog(options.log, deliveryLog("github-to-gitea-delivery-committed", repo, delivery, { ok: true, state: "committed", sourceCommit: result.sourceCommit, authorityCommit: result.authorityCommit, snapshotRef: result.snapshotRef, disposition: result.disposition, totalAttempts: record.totalAttempts, })); return; } await markFailed(record, result); } async function markFailed(record, result) { const repo = repoByKey.get(record.repoKey) ?? { key: record.repoKey }; const terminal = result.retryable === false || record.cycleAttempt >= retry.maxAttempts; const delayMs = terminal ? retry.terminalRetryDelayMs : retryDelayMs(record.cycleAttempt, retry.initialDelayMs, retry.maxDelayMs); const failedAt = new Date(now()).toISOString(); const nextAttemptAt = new Date(now() + delayMs).toISOString(); await store.update(record.deliveryId, (current) => ({ ...current, state: "failed", updatedAt: failedAt, failedAt, nextAttemptAt, cycleAttempt: terminal ? 0 : current.cycleAttempt, result: null, lastError: { errorType: result.errorType || "source-sync-failed", retryable: result.retryable !== false, terminal, }, })); safeLog(options.log, deliveryLog("github-to-gitea-delivery-failed", repo, recordDelivery(record), { ok: false, state: "failed", errorType: result.errorType || "source-sync-failed", error: result.error ? redact(result.error) : null, terminal, automaticRetryScheduled: true, nextAttemptAt, totalAttempts: record.totalAttempts, cycleAttempt: record.cycleAttempt, })); } async function stop() { if (stopped && storageError === "durable-inbox-stopping") return; stopped = true; storageReady = false; storageError = "durable-inbox-stopping"; if (scanTimer !== null) clearTimeout(scanTimer); workerAbort.abort(); const settling = Promise.allSettled([...inFlight]); await Promise.race([settling, sleep(options.shutdownGraceMs)]); } return { start, stop, accept, readiness, status, scan }; } export function createSyncCoordinator(options) { const states = new Map(); const deliveryOwners = new Map(); const sync = options.sync ?? ((repo, delivery, deadline) => syncRepository(repo, delivery, { ...options, ...deadline })); const wait = options.sleep ?? sleep; const now = options.now ?? Date.now; function stateFor(key) { let state = states.get(key); if (!state) { state = { tail: Promise.resolve(), active: null, lastSuccess: null }; states.set(key, state); } return state; } function execute(repo, delivery) { const requestStartedAt = now(); const deadlineAt = requestStartedAt + options.responseBudgetMs; const fingerprint = deliveryFingerprint(repo, delivery); const owner = deliveryOwners.get(delivery.deliveryId); if (owner && owner.fingerprint !== fingerprint) { throw new HttpError(409, "github-delivery-id-payload-conflict"); } if (owner?.promise) { safeLog(options.log, deliveryLog("github-to-gitea-delivery-duplicate", repo, delivery, { disposition: "join-in-flight" })); return owner.promise.then((result) => ({ ...result, duplicate: true })); } if (owner && owner.fingerprint === fingerprint && owner.result?.ok === true) { safeLog(options.log, deliveryLog("github-to-gitea-delivery-duplicate", repo, delivery, { disposition: "durable-refs-already-committed" })); return Promise.resolve({ ...owner.result, duplicate: true }); } const state = stateFor(repo.key); if (state.lastSuccess?.fingerprint === fingerprint) { safeLog(options.log, deliveryLog("github-to-gitea-delivery-duplicate", repo, delivery, { disposition: "durable-refs-already-committed" })); return Promise.resolve({ ...state.lastSuccess.result, duplicate: true }); } const promise = state.tail.then(async () => { state.active = { delivery, fingerprint }; const startedAt = requestStartedAt; let terminal = null; for (let syncAttempt = 1; syncAttempt <= options.retryMaxAttempts; syncAttempt += 1) { if (now() >= deadlineAt) { terminal = { ok: false, retryable: true, errorType: "response-deadline-exhausted" }; safeLog(options.log, deliveryLog("github-to-gitea-sync", repo, delivery, { ok: false, errorType: terminal.errorType, syncAttempt, maxSyncAttempts: options.retryMaxAttempts, willRetry: false, terminal: true, elapsedMs: now() - startedAt, })); break; } let result; try { result = await sync(repo, delivery, { deadlineAt, now, signal: options.signal }); } catch (error) { result = { ok: false, retryable: true, errorType: "sync-exception", error: String(error?.message || error) }; } if (now() >= deadlineAt) { result = { ok: false, retryable: true, errorType: "response-deadline-exhausted", sourceCommit: result.sourceCommit ?? null, authorityCommit: result.authorityCommit ?? null, snapshotRef: result.snapshotRef ?? null, committedBeforeDeadlineUnknown: result.ok === true, }; } const retryable = result.retryable !== false; const willRetry = !result.ok && retryable && syncAttempt < options.retryMaxAttempts; terminal = result; safeLog(options.log, deliveryLog("github-to-gitea-sync", repo, delivery, { ok: result.ok, sourceCommit: result.sourceCommit ?? null, authorityCommit: result.authorityCommit ?? null, snapshotRef: result.snapshotRef ?? null, disposition: result.disposition ?? null, errorType: result.errorType ?? null, error: result.error ? redact(result.error) : null, syncAttempt, maxSyncAttempts: options.retryMaxAttempts, willRetry, terminal: !willRetry, elapsedMs: now() - startedAt, })); if (!willRetry) break; try { const remainingMs = Math.max(0, deadlineAt - now()); await wait(Math.min(remainingMs, retryDelayMs(syncAttempt, options.retryInitialDelayMs, options.retryMaxDelayMs))); } catch (error) { safeLog(options.log, deliveryLog("github-to-gitea-retry-delay-error", repo, delivery, { error: redact(String(error?.message || error)) })); } } if (terminal?.ok) state.lastSuccess = { fingerprint, result: terminal }; const previousDeliveryId = state.lastDeliveryId; state.lastDeliveryId = delivery.deliveryId; deliveryOwners.set(delivery.deliveryId, { fingerprint, promise: null, result: terminal }); if (previousDeliveryId && previousDeliveryId !== delivery.deliveryId) deliveryOwners.delete(previousDeliveryId); return terminal ?? { ok: false, errorType: "sync-produced-no-result", retryable: true }; }); const settled = promise.finally(() => { state.active = null; const current = deliveryOwners.get(delivery.deliveryId); if (current?.promise === settled) deliveryOwners.set(delivery.deliveryId, { fingerprint, promise: null, result: null }); }); state.tail = settled.catch(() => undefined); deliveryOwners.set(delivery.deliveryId, { fingerprint, promise: settled }); return settled; } return { execute, stateFor }; } export async function syncRepository(repo, delivery, runtime) { const work = mkdtempSync(join(tmpdir(), `gitea-gh-sync-${repo.key}-`)); let sourceCommit = null; let snapshotRef = null; let authorityCommit = null; const execute = (args) => runWithinDeadline(runtime, args); const executeResult = (args) => runResultWithinDeadline(runtime, args); try { await execute(["git", "init", "--bare", work]); const snapshotRefForRequest = `${repo.snapshot.prefix.replace(/\/+$/u, "")}/${delivery.requestedCommit}`; const giteaUrl = runtime.giteaBaseUrl + new URL(repo.gitea.readUrl).pathname; const branchRef = `refs/heads/${repo.upstream.branch}`; const initialRemoteRefs = parseLsRemote((await execute([ "git", "-C", work, "-c", `http.extraHeader=${runtime.giteaAuthHeader}`, "ls-remote", giteaUrl, branchRef, snapshotRefForRequest, ])).stdout); const currentCommit = initialRemoteRefs.get(branchRef) ?? null; const snapshotCommit = initialRemoteRefs.get(snapshotRefForRequest) ?? null; if (currentCommit) { await execute(["git", "-C", work, "-c", `http.extraHeader=${runtime.giteaAuthHeader}`, "fetch", "--no-tags", giteaUrl, `+${branchRef}:refs/remotes/gitea/${repo.upstream.branch}`]); } if (snapshotCommit && snapshotCommit !== currentCommit) { await execute(["git", "-C", work, "-c", `http.extraHeader=${runtime.giteaAuthHeader}`, "fetch", "--no-tags", giteaUrl, `+${snapshotRefForRequest}:refs/remotes/gitea/requested-snapshot`]); } let requestedObject = await executeResult(["git", "-C", work, "cat-file", "-e", `${delivery.requestedCommit}^{commit}`]); if (requestedObject.status !== 0) { const branchFetch = await executeResult([ "git", "-C", work, "-c", `http.extraHeader=${runtime.githubAuthHeader}`, "fetch", "--no-tags", repo.upstream.cloneUrl, `+refs/heads/${repo.upstream.branch}:refs/remotes/github/${repo.upstream.branch}`, ]); requestedObject = await executeResult(["git", "-C", work, "cat-file", "-e", `${delivery.requestedCommit}^{commit}`]); if (requestedObject.status !== 0) { const exactFetch = await executeResult(["git", "-C", work, "-c", `http.extraHeader=${runtime.githubAuthHeader}`, "fetch", "--no-tags", repo.upstream.cloneUrl, `${delivery.requestedCommit}:refs/remotes/github/delivery`]); if (exactFetch.status !== 0) { return { ok: false, retryable: true, errorType: "requested-commit-temporarily-unavailable", error: `${branchFetch.stderr}\n${exactFetch.stderr}`, }; } requestedObject = await executeResult(["git", "-C", work, "cat-file", "-e", `${delivery.requestedCommit}^{commit}`]); } } if (requestedObject.status !== 0) return { ok: false, retryable: false, errorType: "requested-commit-not-a-commit" }; sourceCommit = (await execute(["git", "-C", work, "rev-parse", `${delivery.requestedCommit}^{commit}`])).stdout.trim(); if (sourceCommit !== delivery.requestedCommit) { return { ok: false, retryable: false, errorType: "source-commit-mismatch", sourceCommit, error: `fetched source commit ${sourceCommit} does not equal webhook after ${delivery.requestedCommit}`, }; } snapshotRef = snapshotRefForRequest; if (snapshotCommit && snapshotCommit !== sourceCommit) { return { ok: false, retryable: false, errorType: "immutable-snapshot-conflict", sourceCommit, authorityCommit: currentCommit, snapshotRef }; } let branchDisposition = "created"; let updateBranch = true; if (currentCommit) { const currentIsAncestor = await ancestry(executeResult, work, currentCommit, sourceCommit); const sourceIsAncestor = await ancestry(executeResult, work, sourceCommit, currentCommit); if (currentCommit === sourceCommit) { branchDisposition = "current"; updateBranch = false; } else if (currentIsAncestor === true) { branchDisposition = "fast-forwarded"; } else if (sourceIsAncestor === true) { branchDisposition = "superseded"; updateBranch = false; } else if (currentIsAncestor === null || sourceIsAncestor === null) { return { ok: false, retryable: true, errorType: "ancestry-proof-failed", sourceCommit, authorityCommit: currentCommit, snapshotRef }; } else { return { ok: false, retryable: false, errorType: "source-history-diverged", sourceCommit, authorityCommit: currentCommit, snapshotRef }; } } const refspecs = []; const pushArgs = ["git", "-C", work, "-c", `http.extraHeader=${runtime.giteaAuthHeader}`, "push", "--atomic"]; if (updateBranch) { pushArgs.push(`--force-with-lease=${branchRef}:${currentCommit ?? ""}`); refspecs.push(`${sourceCommit}:${branchRef}`); } if (!snapshotCommit) refspecs.push(`${sourceCommit}:${snapshotRef}`); if (refspecs.length > 0) { const pushed = await executeResult([...pushArgs, giteaUrl, ...refspecs]); if (pushed.status !== 0) { return { ok: false, retryable: true, errorType: "atomic-push-race-or-failure", sourceCommit, authorityCommit: currentCommit, snapshotRef, error: pushed.stderr }; } } authorityCommit = branchDisposition === "superseded" ? currentCommit : sourceCommit; const proofRefs = parseLsRemote((await execute([ "git", "-C", work, "-c", `http.extraHeader=${runtime.giteaAuthHeader}`, "ls-remote", giteaUrl, branchRef, snapshotRef, ])).stdout); const provenAuthorityCommit = proofRefs.get(branchRef) ?? null; const provenSnapshotCommit = proofRefs.get(snapshotRef) ?? null; if (provenAuthorityCommit !== authorityCommit || provenSnapshotCommit !== sourceCommit) { return { ok: false, retryable: true, errorType: "post-push-authority-proof-failed", sourceCommit, authorityCommit: provenAuthorityCommit, snapshotRef, }; } return { ok: true, sourceCommit, authorityCommit, snapshotRef, disposition: branchDisposition === "superseded" ? "superseded-with-immutable-snapshot" : "atomic-source-authority-committed", }; } catch (error) { return { ok: false, retryable: true, errorType: "git-source-sync-failed", sourceCommit, authorityCommit, snapshotRef, error: String(error?.message || error) }; } finally { rmSync(work, { recursive: true, force: true }); } } export async function run(args, timeoutMs = 120000) { const result = await runResult(args, timeoutMs); if (result.status !== 0) { throw new Error(`${args[0]} failed status=${result.status ?? "error"} stderr=${result.stderr}`); } return { stdout: result.stdout, stderr: result.stderr }; } export function runResult(args, timeoutMs = 120000, signal = null) { const boundedTimeoutMs = Math.max(1, Math.floor(timeoutMs)); if (signal?.aborted) return Promise.resolve({ status: null, stdout: "", stderr: "command aborted before spawn" }); return new Promise((resolve) => { let stdout = ""; let stderr = ""; let timedOut = false; let aborted = false; let settled = false; let deadlineTimer = null; let forceKillTimer = null; let abortCommand = () => {}; const child = spawn(args[0], args.slice(1), { stdio: ["ignore", "pipe", "pipe"], detached: process.platform !== "win32", env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", }, }); const finish = (status, error = "") => { if (settled) return; settled = true; if (deadlineTimer !== null) clearTimeout(deadlineTimer); if (forceKillTimer !== null) clearTimeout(forceKillTimer); signal?.removeEventListener("abort", abortCommand); resolve({ status, stdout, stderr: appendTail(stderr, error, 1600), }); }; child.stdout.on("data", (chunk) => { stdout = appendTail(stdout, String(chunk), 65536); }); child.stderr.on("data", (chunk) => { stderr = appendTail(stderr, String(chunk), 1600); }); child.once("error", (error) => finish(null, String(error?.message || error))); child.once("close", (status, signal) => { const terminalError = aborted ? `command aborted; signal=${signal || "none"}` : timedOut ? `command deadline exceeded after ${boundedTimeoutMs}ms; signal=${signal || "none"}` : ""; finish(aborted || timedOut ? null : status, terminalError); }); abortCommand = () => { aborted = true; signalProcessTree(child, "SIGTERM"); forceKillTimer = setTimeout(() => signalProcessTree(child, "SIGKILL"), 50); }; signal?.addEventListener("abort", abortCommand, { once: true }); if (signal?.aborted) abortCommand(); deadlineTimer = setTimeout(() => { timedOut = true; signalProcessTree(child, "SIGTERM"); forceKillTimer = setTimeout(() => signalProcessTree(child, "SIGKILL"), 50); }, boundedTimeoutMs); }); } export function verifySignature(signatureHeader, body, webhookSecret) { const signature = singleHeader(signatureHeader); if (!signature || !signature.startsWith("sha256=")) throw new HttpError(401, "missing-github-signature"); const expected = `sha256=${createHmac("sha256", webhookSecret).update(body).digest("hex")}`; if (!timingSafeEqualString(signature, expected)) throw new HttpError(401, "invalid-github-signature"); } export function retryDelayMs(syncAttempt, initialDelayMs, maxDelayMs) { return Math.min(maxDelayMs, initialDelayMs * (2 ** Math.max(0, syncAttempt - 1))); } export function loadRuntimeConfig() { const githubToken = requiredEnv("GITHUB_TOKEN"); const giteaUsername = requiredEnv("GITEA_USERNAME"); const giteaPassword = requiredEnv("GITEA_PASSWORD"); return { port: Number.parseInt(process.env.UNIDESK_GITEA_WEBHOOK_PORT || "8080", 10), path: process.env.UNIDESK_GITEA_WEBHOOK_PATH || "/_unidesk/github-to-gitea", responseBudgetMs: positiveIntegerEnv("UNIDESK_GITEA_WEBHOOK_RESPONSE_BUDGET_MS"), repos: JSON.parse(readFileSync(process.env.UNIDESK_GITEA_REPOS_PATH || "/etc/gitea-github-sync/repos.json", "utf8")), webhookSecret: requiredEnv("GITHUB_WEBHOOK_SECRET"), giteaBaseUrl: requiredEnv("UNIDESK_GITEA_SERVICE_BASE_URL").replace(/\/+$/u, ""), inbox: { path: requiredEnv("UNIDESK_GITEA_WEBHOOK_INBOX_PATH"), maxBytes: positiveIntegerEnv("UNIDESK_GITEA_WEBHOOK_INBOX_MAX_BYTES"), committedRetentionMs: positiveIntegerEnv("UNIDESK_GITEA_WEBHOOK_COMMITTED_RETENTION_SECONDS") * 1000, cleanupIntervalMs: positiveIntegerEnv("UNIDESK_GITEA_WEBHOOK_CLEANUP_INTERVAL_MS"), }, failedDeliveryRecoveryStatePath: process.env.UNIDESK_GITEA_HOOK_RECOVERY_STATE_PATH || null, workerRetry: { maxAttempts: positiveIntegerEnv("UNIDESK_GITEA_WEBHOOK_RETRY_MAX_ATTEMPTS"), initialDelayMs: positiveIntegerEnv("UNIDESK_GITEA_WEBHOOK_RETRY_INITIAL_DELAY_MS"), maxDelayMs: positiveIntegerEnv("UNIDESK_GITEA_WEBHOOK_RETRY_MAX_DELAY_MS"), terminalRetryDelayMs: positiveIntegerEnv("UNIDESK_GITEA_WEBHOOK_RETRY_TERMINAL_DELAY_MS"), attemptTimeoutMs: positiveIntegerEnv("UNIDESK_GITEA_WEBHOOK_WORKER_ATTEMPT_TIMEOUT_MS"), scanIntervalMs: positiveIntegerEnv("UNIDESK_GITEA_WEBHOOK_WORKER_SCAN_INTERVAL_MS"), }, shutdownGraceMs: positiveIntegerEnv("UNIDESK_GITEA_WEBHOOK_SHUTDOWN_GRACE_MS"), maxBodyBytes: positiveIntegerEnv("UNIDESK_GITEA_WEBHOOK_MAX_BODY_BYTES"), githubAuthHeader: `Authorization: Basic ${Buffer.from(`x-access-token:${githubToken}`).toString("base64")}`, giteaAuthHeader: `Authorization: Basic ${Buffer.from(`${giteaUsername}:${giteaPassword}`).toString("base64")}`, log, run, runResult, }; } export async function readFailedDeliveryRecoveryStatus(statePath) { if (!statePath) return { enabled: false, ready: true, state: "disabled", counts: {}, entries: [] }; let ledger; try { ledger = JSON.parse(await readFile(statePath, "utf8")); } catch (error) { if (error?.code === "ENOENT") return { enabled: true, ready: false, state: "not-initialized", counts: {}, entries: [] }; return { enabled: true, ready: false, state: "invalid-or-unreadable", errorType: "hook-recovery-ledger-unreadable", counts: {}, entries: [] }; } if (ledger?.version !== 1 || ledger?.kind !== "GiteaGithubHookRecoveryLedger" || !ledger.records || typeof ledger.records !== "object" || Array.isArray(ledger.records)) { return { enabled: true, ready: false, state: "invalid-or-unreadable", errorType: "hook-recovery-ledger-invalid", counts: {}, entries: [] }; } const allowedStates = new Set(["pending", "attempted", "delivery-succeeded-awaiting-source", "source-committed", "terminal-exhausted"]); const records = Object.values(ledger.records); const validRecord = (item) => item && typeof item === "object" && allowedStates.has(item.state) && typeof item.repository === "string" && item.repository.length > 0 && (typeof item.hookId === "number" || typeof item.hookId === "string") && typeof item.deliveryId === "string" && item.deliveryId.length > 0 && Number.isInteger(item.attempts) && item.attempts >= 0 && Number.isInteger(item.cooldownMs) && item.cooldownMs >= 0 && typeof item.updatedAt === "string" && item.updatedAt.length > 0; if (records.some((item) => !validRecord(item))) { return { enabled: true, ready: false, state: "invalid-or-unreadable", errorType: "hook-recovery-ledger-record-invalid", counts: {}, entries: [] }; } const counts = { pending: 0, attempted: 0, "delivery-succeeded-awaiting-source": 0, "source-committed": 0, "terminal-exhausted": 0 }; for (const item of records) counts[item.state] += 1; const entries = records .sort((left, right) => String(right.updatedAt || "").localeCompare(String(left.updatedAt || ""))) .slice(0, 50) .map((item) => ({ repository: item.repository, hookId: item.hookId, deliveryId: item.deliveryId, state: item.state, attempts: item.attempts, cooldownMs: item.cooldownMs, nextAt: item.nextAt ?? null, updatedAt: item.updatedAt, lastStatusCode: item.lastStatusCode ?? null, completionReason: item.completionReason ?? null, })); return { enabled: true, ready: true, state: "ready", counts, entries }; } export function startServer(config = loadRuntimeConfig()) { const controller = config.controller ?? createDurableSyncController(config); const server = createServer(createGithubSyncHandler({ ...config, controller })); void controller.start(); server.listen(config.port, "0.0.0.0", () => { safeLog(config.log, { event: "github-to-gitea-sync-listening", port: config.port, path: config.path, repos: config.repos.map((repo) => repo.key), valuesPrinted: false }); }); const shutdown = async () => { server.close(); await controller.stop(); }; server.once("close", () => void controller.stop()); process.once("SIGTERM", shutdown); process.once("SIGINT", shutdown); return server; } class HttpError extends Error { constructor(status, message) { super(message); this.status = status; } } class DurableInboxError extends Error { constructor(status, message) { super(message); this.status = status; } } class ExecutorTerminalResultError extends Error {} function validateExecutorTerminalResult(result) { if (!result || typeof result !== "object" || typeof result.ok !== "boolean") { throw new ExecutorTerminalResultError("executor terminal result must be an object with boolean ok"); } if (result.ok === false) { if (typeof result.retryable !== "boolean" || !/^[a-z0-9][a-z0-9-]{0,127}$/u.test(result.errorType || "")) { throw new ExecutorTerminalResultError("failed executor terminal result must include retryable and typed errorType"); } return result; } if (!/^[0-9a-f]{40,64}$/u.test(result.sourceCommit || "") || !/^[0-9a-f]{40,64}$/u.test(result.authorityCommit || "") || !/^refs\/[A-Za-z0-9._/-]+$/u.test(result.snapshotRef || "") || !["atomic-source-authority-committed", "superseded-with-immutable-snapshot"].includes(result.disposition)) { throw new ExecutorTerminalResultError("successful executor terminal result must include source, authority, snapshot and disposition proof"); } return result; } function deliveryFingerprint(repo, delivery) { return createHash("sha256") .update([repo.key, delivery.deliveryId, delivery.repository, delivery.ref, delivery.requestedCommit, delivery.payloadSha].join("\0")) .digest("hex"); } function nonRetryableStatus(errorType) { return [ "github-delivery-id-payload-conflict", "source-commit-mismatch", "immutable-snapshot-conflict", "source-history-diverged", ].includes(errorType) ? 409 : 422; } function safeLog(logger, payload) { try { logger(payload); } catch { // Logging must never change delivery success or retry semantics. } } function parseLsRemote(output) { const refs = new Map(); for (const line of String(output || "").split("\n")) { const [commit, ref] = line.trim().split(/\s+/u); if (commit && ref) refs.set(ref, commit); } return refs; } async function ancestry(executeResult, work, ancestor, descendant) { const result = await executeResult(["git", "-C", work, "merge-base", "--is-ancestor", ancestor, descendant]); if (result.status === 0) return true; if (result.status === 1) return false; return null; } async function runWithinDeadline(runtime, args) { const result = await runResultWithinDeadline(runtime, args); if (result.status !== 0) throw new Error(`${args[0]} failed status=${result.status ?? "deadline"} stderr=${result.stderr}`); return { stdout: result.stdout, stderr: result.stderr }; } async function runResultWithinDeadline(runtime, args) { const remainingMs = Math.floor(runtime.deadlineAt - runtime.now()); if (remainingMs < 1) return { status: null, stdout: "", stderr: "response deadline exhausted before command" }; if (runtime.signal?.aborted) return { status: null, stdout: "", stderr: "command aborted before spawn" }; return await runtime.runResult(args, remainingMs, runtime.signal); } function appendTail(previous, next, limit) { return `${previous}${next}`.slice(-limit); } function signalProcessTree(child, signal) { if (process.platform !== "win32" && child.pid) { try { process.kill(-child.pid, signal); return; } catch { // Fall back to the direct child when the process group already exited. } } child.kill(signal); } function recordPath(directory, deliveryId) { return join(directory, `${createHash("sha256").update(String(deliveryId)).digest("hex")}.json`); } function serializeRecord(record) { return `${JSON.stringify(record)}\n`; } async function readRecordPath(path, missingIsNull = false) { try { const parsed = JSON.parse(await readFile(path, "utf8")); return validateInboxRecord(parsed); } catch (error) { if (missingIsNull && error?.code === "ENOENT") return null; if (error instanceof DurableInboxError) throw error; throw new DurableInboxError(503, `durable-inbox-read-failed:${error?.code || "unknown"}`); } } async function durableWrite(path, content) { const temporaryPath = `${path}.tmp-${randomUUID()}`; let handle = null; try { handle = await open(temporaryPath, "wx", 0o600); await handle.writeFile(content, "utf8"); await handle.sync(); await handle.close(); handle = null; await rename(temporaryPath, path); await syncDirectory(join(path, "..")); } catch (error) { if (handle) await handle.close().catch(() => undefined); await unlink(temporaryPath).catch(() => undefined); if (error instanceof DurableInboxError) throw error; throw new DurableInboxError(503, `durable-inbox-write-failed:${error?.code || "unknown"}`); } } async function syncDirectory(directory) { const handle = await open(directory, "r"); try { await handle.sync(); } finally { await handle.close(); } } function typedStorageError(error) { if (error instanceof DurableInboxError) return error.message.split(":", 1)[0]; return "durable-inbox-unavailable"; } function recordDelivery(record) { return { deliveryId: record.deliveryId, repository: record.repository, ref: record.ref, requestedCommit: record.requestedCommit, payloadSha: record.payloadSha, receivedAt: record.receivedAt, }; } function publicSyncResult(result) { return { sourceCommit: result.sourceCommit ?? null, authorityCommit: result.authorityCommit ?? null, snapshotRef: result.snapshotRef ?? null, disposition: result.disposition ?? null, }; } function publicInboxRecord(record) { return { deliveryId: record.deliveryId, repo: record.repoKey, repository: record.repository, ref: record.ref, requestedCommit: record.requestedCommit, payloadSha: record.payloadSha, receivedAt: record.receivedAt, state: record.state, acceptedSequence: record.acceptedSequence, totalAttempts: record.totalAttempts, cycleAttempt: record.cycleAttempt, nextAttemptAt: record.nextAttemptAt, updatedAt: record.updatedAt, result: record.result, lastError: record.lastError, }; } function statusRecords(records) { const selected = new Map(); for (const record of records.slice(-100)) selected.set(record.deliveryId, record); const latestByRepo = new Map(); const latestCommittedByRepo = new Map(); for (const record of records) { const current = latestByRepo.get(record.repoKey); if (!current || record.acceptedSequence > current.acceptedSequence) latestByRepo.set(record.repoKey, record); if (record.state === "committed") { const committed = latestCommittedByRepo.get(record.repoKey); if (!committed || record.acceptedSequence > committed.acceptedSequence) latestCommittedByRepo.set(record.repoKey, record); } } for (const record of latestByRepo.values()) selected.set(record.deliveryId, record); for (const record of latestCommittedByRepo.values()) selected.set(record.deliveryId, record); return sortInboxRecordsByAcceptedSequence([...selected.values()]); } export function sortInboxRecordsByAcceptedSequence(records) { return [...records].sort((left, right) => left.acceptedSequence - right.acceptedSequence); } function validateInboxRecord(record) { const invalid = () => { throw new DurableInboxError(503, "durable-inbox-record-invalid"); }; if (!record || record.version !== 1) invalid(); if (typeof record.deliveryId !== "string" || record.deliveryId.length < 1 || record.deliveryId.length > 160) invalid(); if (!/^[a-zA-Z0-9._-]+$/u.test(record.repoKey || "")) invalid(); if (!/^[^/\s]+\/[^/\s]+$/u.test(record.repository || "")) invalid(); if (!/^refs\/heads\/[^\s]+$/u.test(record.ref || "")) invalid(); if (!/^[0-9a-f]{40,64}$/u.test(record.requestedCommit || "")) invalid(); if (!/^[0-9a-f]{64}$/u.test(record.payloadSha || "")) invalid(); if (!/^[0-9a-f]{64}$/u.test(record.fingerprint || "")) invalid(); if (!["accepted", "processing", "committed", "failed"].includes(record.state)) invalid(); if (!isTimestamp(record.receivedAt) || !isTimestamp(record.acceptedAt) || !isTimestamp(record.updatedAt)) invalid(); if (!Number.isInteger(record.acceptedSequence) || record.acceptedSequence < 1) invalid(); if (!Number.isInteger(record.totalAttempts) || record.totalAttempts < 0 || !Number.isInteger(record.cycleAttempt) || record.cycleAttempt < 0) invalid(); if (record.nextAttemptAt !== null && !isTimestamp(record.nextAttemptAt)) invalid(); if (record.lastError !== null) { if (typeof record.lastError !== "object" || !/^[a-z0-9][a-z0-9-]{0,127}$/u.test(record.lastError.errorType || "")) invalid(); if (typeof record.lastError.retryable !== "boolean") invalid(); if (record.lastError.terminal !== undefined && typeof record.lastError.terminal !== "boolean") invalid(); } const expectedFingerprint = createHash("sha256") .update([record.repoKey, record.deliveryId, record.repository, record.ref, record.requestedCommit, record.payloadSha].join("\0")) .digest("hex"); if (!timingSafeEqualString(record.fingerprint, expectedFingerprint)) invalid(); if (record.state === "accepted" && (!isTimestamp(record.nextAttemptAt) || record.result !== null)) invalid(); if (record.state === "processing" && (!isTimestamp(record.processingAt) || record.result !== null)) invalid(); if (record.state === "failed" && (!isTimestamp(record.failedAt) || !isTimestamp(record.nextAttemptAt) || record.lastError === null || record.result !== null)) invalid(); if (record.state === "committed") { if (!isTimestamp(record.committedAt) || record.nextAttemptAt !== null || record.lastError !== null || !record.result || typeof record.result !== "object") invalid(); if (!/^[0-9a-f]{40,64}$/u.test(record.result.sourceCommit || "") || !/^[0-9a-f]{40,64}$/u.test(record.result.authorityCommit || "")) invalid(); if (record.result.sourceCommit !== record.requestedCommit) invalid(); if (!/^refs\/[a-zA-Z0-9._/-]+$/u.test(record.result.snapshotRef || "") || !record.result.snapshotRef.endsWith(`/${record.requestedCommit}`)) invalid(); if (!["atomic-source-authority-committed", "superseded-with-immutable-snapshot"].includes(record.result.disposition)) invalid(); } return record; } function validateJournalMetadata(metadata) { if (!metadata || metadata.version !== 1 || metadata.kind !== "GiteaGithubSyncDurableInbox" || !isTimestamp(metadata.createdAt)) { throw new DurableInboxError(503, "durable-inbox-metadata-invalid"); } return metadata; } function isTimestamp(value) { return typeof value === "string" && Number.isFinite(Date.parse(value)); } function deliveryLog(event, repo, delivery, extra = {}) { return { event, repo: repo.key, deliveryId: delivery.deliveryId, repository: delivery.repository, ref: delivery.ref, requestedCommit: delivery.requestedCommit, ...extra, valuesPrinted: false, }; } function parsePayload(body) { try { return JSON.parse(body.toString("utf8") || "{}"); } catch { throw new HttpError(400, "invalid-json-payload"); } } function singleHeader(value) { return String(Array.isArray(value) ? value[0] : value || "").trim(); } function timingSafeEqualString(a, b) { const left = Buffer.from(a); const right = Buffer.from(b); return left.length === right.length && timingSafeEqual(left, right); } function readBody(req, maxBodyBytes) { return new Promise((resolve, reject) => { const chunks = []; let bytes = 0; let rejected = false; req.on("data", (chunk) => { bytes += chunk.length; if (!rejected && bytes > maxBodyBytes) { rejected = true; reject(new HttpError(413, "request-body-too-large")); return; } if (!rejected) chunks.push(chunk); }); req.on("end", () => { if (!rejected) resolve(Buffer.concat(chunks)); }); req.on("error", reject); }); } export function waitWithinResponseBudget(promise, deadlineMs, errorType) { const remainingMs = deadlineMs - Date.now(); if (remainingMs <= 0) return Promise.reject(new HttpError(503, errorType)); return new Promise((resolve, reject) => { let settled = false; const timer = setTimeout(() => { if (settled) return; settled = true; reject(new HttpError(503, errorType)); }, remainingMs); Promise.resolve(promise).then( (value) => { if (settled) return; settled = true; clearTimeout(timer); resolve(value); }, (error) => { if (settled) return; settled = true; clearTimeout(timer); reject(error); }, ); }); } function writeJson(res, status, payload) { if (res.headersSent || res.destroyed) return; res.writeHead(status, { "Content-Type": "application/json" }); res.end(`${JSON.stringify(payload)}\n`); } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function log(payload) { console.log(JSON.stringify(payload)); } function redact(value) { return String(value || "") .replace(/Authorization: Basic [A-Za-z0-9+/=]+/gu, "Authorization: Basic ") .replace(/x-access-token:[^@\s]+/gu, "x-access-token:") .slice(-1600); } function requiredEnv(name) { const value = process.env[name]; if (!value) throw new Error(`missing env ${name}`); return value; } function positiveIntegerEnv(name) { const value = Number.parseInt(requiredEnv(name), 10); if (!Number.isFinite(value) || value < 1) throw new Error(`invalid positive integer env ${name}`); return value; }