From cd1ca702e94eed61588c991270ff68b730fe8ec8 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Tue, 17 Feb 2026 06:57:35 +0100 Subject: [PATCH] handled failed publishing --- .../api/repos/[npub]/[repo]/fork/+server.ts | 137 ++++++++++++++++-- src/routes/repos/[npub]/[repo]/+page.svelte | 35 ++++- 2 files changed, 155 insertions(+), 17 deletions(-) diff --git a/src/routes/api/repos/[npub]/[repo]/fork/+server.ts b/src/routes/api/repos/[npub]/[repo]/fork/+server.ts index 2b2ebbb..0ee61b4 100644 --- a/src/routes/api/repos/[npub]/[repo]/fork/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/fork/+server.ts @@ -8,7 +8,7 @@ import { RepoManager } from '$lib/services/git/repo-manager.js'; import { DEFAULT_NOSTR_RELAYS, combineRelays, getGitUrl } from '$lib/config.js'; import { getUserRelays } from '$lib/services/nostr/user-relays.js'; import { NostrClient } from '$lib/services/nostr/nostr-client.js'; -import { KIND } from '$lib/types/nostr.js'; +import { KIND, type NostrEvent } from '$lib/types/nostr.js'; import { nip19 } from 'nostr-tools'; import { signEventWithNIP07 } from '$lib/services/nostr/nip07-signer.js'; import { OwnershipTransferService } from '$lib/services/nostr/ownership-transfer-service.js'; @@ -22,6 +22,45 @@ const repoRoot = process.env.GIT_REPO_ROOT || '/repos'; const repoManager = new RepoManager(repoRoot); const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS); +/** + * Retry publishing an event with exponential backoff + * Attempts up to 3 times with delays: 1s, 2s, 4s + */ +async function publishEventWithRetry( + event: NostrEvent, + relays: string[], + eventName: string, + maxAttempts: number = 3 +): Promise<{ success: string[]; failed: Array<{ relay: string; error: string }> }> { + let lastResult: { success: string[]; failed: Array<{ relay: string; error: string }> } | null = null; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + console.log(`[Fork] Publishing ${eventName} - Attempt ${attempt}/${maxAttempts}...`); + + lastResult = await nostrClient.publishEvent(event, relays); + + if (lastResult.success.length > 0) { + console.log(`[Fork] ✓ ${eventName} published successfully to ${lastResult.success.length} relay(s): ${lastResult.success.join(', ')}`); + if (lastResult.failed.length > 0) { + console.warn(`[Fork] ⚠ Some relays failed: ${lastResult.failed.map(f => `${f.relay}: ${f.error}`).join(', ')}`); + } + return lastResult; + } + + if (attempt < maxAttempts) { + const delayMs = Math.pow(2, attempt - 1) * 1000; // 1s, 2s, 4s + console.warn(`[Fork] ✗ ${eventName} failed on attempt ${attempt}. Retrying in ${delayMs}ms...`); + console.warn(`[Fork] Failed relays: ${lastResult.failed.map(f => `${f.relay}: ${f.error}`).join(', ')}`); + await new Promise(resolve => setTimeout(resolve, delayMs)); + } + } + + // All attempts failed + console.error(`[Fork] ✗ ${eventName} failed after ${maxAttempts} attempts`); + console.error(`[Fork] All relay failures: ${lastResult?.failed.map(f => `${f.relay}: ${f.error}`).join(', ')}`); + return lastResult!; +} + /** * POST - Fork a repository * Body: { userPubkey, forkName? } @@ -57,10 +96,12 @@ export const POST: RequestHandler = async ({ params, request }) => { // Decode user pubkey if needed let userPubkeyHex = userPubkey; try { - const userDecoded = nip19.decode(userPubkey); - if (userDecoded.type === 'npub') { - userPubkeyHex = userDecoded.data as string; + const userDecoded = nip19.decode(userPubkey) as { type: string; data: unknown }; + // Type guard: check if it's an npub + if (userDecoded.type === 'npub' && typeof userDecoded.data === 'string') { + userPubkeyHex = userDecoded.data; } + // If not npub, assume it's already hex } catch { // Assume it's already hex } @@ -150,34 +191,108 @@ export const POST: RequestHandler = async ({ params, request }) => { const { outbox } = await getUserRelays(userPubkeyHex, nostrClient); const combinedRelays = combineRelays(outbox); - const publishResult = await nostrClient.publishEvent(signedForkAnnouncement, combinedRelays); + console.log(`[Fork] Starting fork process for ${forkRepoName} by ${userNpub}`); + console.log(`[Fork] Using ${combinedRelays.length} relay(s): ${combinedRelays.join(', ')}`); + + const publishResult = await publishEventWithRetry( + signedForkAnnouncement, + combinedRelays, + 'fork announcement', + 3 + ); if (publishResult.success.length === 0) { // Clean up repo if announcement failed + console.error(`[Fork] ✗ Fork announcement failed after all retries. Cleaning up repository.`); await execAsync(`rm -rf "${forkRepoPath}"`).catch(() => {}); - return error(500, 'Failed to publish fork announcement to relays'); + const errorDetails = `All relays failed: ${publishResult.failed.map(f => `${f.relay}: ${f.error}`).join('; ')}`; + return json({ + success: false, + error: 'Failed to publish fork announcement to relays after 3 attempts', + details: errorDetails, + eventName: 'fork announcement' + }, { status: 500 }); } // Create and publish initial ownership proof (self-transfer event) + // This MUST succeed for the fork to be valid - without it, there's no proof of ownership on Nostr const ownershipService = new OwnershipTransferService(combinedRelays); const initialOwnershipEvent = ownershipService.createInitialOwnershipEvent(userPubkeyHex, forkRepoName); const signedOwnershipEvent = await signEventWithNIP07(initialOwnershipEvent); - await nostrClient.publishEvent(signedOwnershipEvent, combinedRelays).catch(err => { - console.warn('Failed to publish initial ownership event for fork:', err); - }); + const ownershipPublishResult = await publishEventWithRetry( + signedOwnershipEvent, + combinedRelays, + 'ownership transfer event', + 3 + ); + + if (ownershipPublishResult.success.length === 0) { + // Clean up repo if ownership proof failed + console.error(`[Fork] ✗ Ownership transfer event failed after all retries. Cleaning up repository and publishing deletion request.`); + await execAsync(`rm -rf "${forkRepoPath}"`).catch(() => {}); + + // Publish deletion request (NIP-09) for the announcement since it's invalid without ownership proof + console.log(`[Fork] Publishing deletion request for invalid fork announcement...`); + const deletionRequest = { + kind: 5, // NIP-09: Event Deletion Request + pubkey: userPubkeyHex, + created_at: Math.floor(Date.now() / 1000), + content: 'Fork failed: ownership transfer event could not be published after 3 attempts. This announcement is invalid.', + tags: [ + ['a', `30617:${userPubkeyHex}:${forkRepoName}`], // Reference to the repo announcement + ['k', KIND.REPO_ANNOUNCEMENT.toString()] // Kind of event being deleted + ] + }; + + const signedDeletionRequest = await signEventWithNIP07(deletionRequest); + const deletionResult = await publishEventWithRetry( + signedDeletionRequest, + combinedRelays, + 'deletion request', + 3 + ); + + if (deletionResult.success.length > 0) { + console.log(`[Fork] ✓ Deletion request published successfully`); + } else { + console.error(`[Fork] ✗ Failed to publish deletion request: ${deletionResult.failed.map(f => `${f.relay}: ${f.error}`).join(', ')}`); + } + + const errorDetails = `Fork is invalid without ownership proof. All relays failed: ${ownershipPublishResult.failed.map(f => `${f.relay}: ${f.error}`).join('; ')}. Deletion request ${deletionResult.success.length > 0 ? 'published' : 'failed to publish'}.`; + return json({ + success: false, + error: 'Failed to publish ownership transfer event to relays after 3 attempts', + details: errorDetails, + eventName: 'ownership transfer event' + }, { status: 500 }); + } // Provision the fork repo (this will create verification file and include self-transfer) + console.log(`[Fork] Provisioning fork repository...`); await repoManager.provisionRepo(signedForkAnnouncement, signedOwnershipEvent, false); + console.log(`[Fork] ✓ Fork completed successfully!`); + console.log(`[Fork] - Repository: ${userNpub}/${forkRepoName}`); + console.log(`[Fork] - Announcement ID: ${signedForkAnnouncement.id}`); + console.log(`[Fork] - Ownership transfer ID: ${signedOwnershipEvent.id}`); + console.log(`[Fork] - Published to ${publishResult.success.length} relay(s) for announcement`); + console.log(`[Fork] - Published to ${ownershipPublishResult.success.length} relay(s) for ownership transfer`); + return json({ success: true, fork: { npub: userNpub, repo: forkRepoName, url: forkGitUrl, - announcementId: signedForkAnnouncement.id - } + announcementId: signedForkAnnouncement.id, + ownershipTransferId: signedOwnershipEvent.id, + publishedTo: { + announcement: publishResult.success.length, + ownershipTransfer: ownershipPublishResult.success.length + } + }, + message: `Repository forked successfully! Published to ${publishResult.success.length} relay(s) for announcement and ${ownershipPublishResult.success.length} relay(s) for ownership proof.` }); } catch (err) { console.error('Error forking repository:', err); diff --git a/src/routes/repos/[npub]/[repo]/+page.svelte b/src/routes/repos/[npub]/[repo]/+page.svelte index ef83d85..966fc77 100644 --- a/src/routes/repos/[npub]/[repo]/+page.svelte +++ b/src/routes/repos/[npub]/[repo]/+page.svelte @@ -156,22 +156,45 @@ error = null; try { + console.log(`[Fork UI] Starting fork of ${npub}/${repo}...`); const response = await fetch(`/api/repos/${npub}/${repo}/fork`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userPubkey }) }); - if (response.ok) { - const data = await response.json(); - alert(`Repository forked successfully! Visit /repos/${data.fork.npub}/${data.fork.repo}`); + const data = await response.json(); + + if (response.ok && data.success !== false) { + const message = data.message || `Repository forked successfully! Published to ${data.fork?.publishedTo?.announcement || 0} relay(s).`; + console.log(`[Fork UI] ✓ ${message}`); + console.log(`[Fork UI] - Fork location: /repos/${data.fork.npub}/${data.fork.repo}`); + console.log(`[Fork UI] - Announcement ID: ${data.fork.announcementId}`); + console.log(`[Fork UI] - Ownership Transfer ID: ${data.fork.ownershipTransferId}`); + + alert(`✓ ${message}\n\nRedirecting to your fork...`); goto(`/repos/${data.fork.npub}/${data.fork.repo}`); } else { - const data = await response.json(); - error = data.error || 'Failed to fork repository'; + const errorMessage = data.error || 'Failed to fork repository'; + const errorDetails = data.details ? `\n\nDetails: ${data.details}` : ''; + const fullError = `${errorMessage}${errorDetails}`; + + console.error(`[Fork UI] ✗ Fork failed: ${errorMessage}`); + if (data.details) { + console.error(`[Fork UI] Details: ${data.details}`); + } + if (data.eventName) { + console.error(`[Fork UI] Failed event: ${data.eventName}`); + } + + error = fullError; + alert(`✗ Fork failed!\n\n${fullError}`); } } catch (err) { - error = err instanceof Error ? err.message : 'Failed to fork repository'; + const errorMessage = err instanceof Error ? err.message : 'Failed to fork repository'; + console.error(`[Fork UI] ✗ Unexpected error: ${errorMessage}`, err); + error = errorMessage; + alert(`✗ Fork failed!\n\n${errorMessage}`); } finally { forking = false; }