Browse Source

handled failed publishing

main
Silberengel 4 weeks ago
parent
commit
cd1ca702e9
  1. 135
      src/routes/api/repos/[npub]/[repo]/fork/+server.ts
  2. 33
      src/routes/repos/[npub]/[repo]/+page.svelte

135
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 { DEFAULT_NOSTR_RELAYS, combineRelays, getGitUrl } from '$lib/config.js';
import { getUserRelays } from '$lib/services/nostr/user-relays.js'; import { getUserRelays } from '$lib/services/nostr/user-relays.js';
import { NostrClient } from '$lib/services/nostr/nostr-client.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 { nip19 } from 'nostr-tools';
import { signEventWithNIP07 } from '$lib/services/nostr/nip07-signer.js'; import { signEventWithNIP07 } from '$lib/services/nostr/nip07-signer.js';
import { OwnershipTransferService } from '$lib/services/nostr/ownership-transfer-service.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 repoManager = new RepoManager(repoRoot);
const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS); 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 * POST - Fork a repository
* Body: { userPubkey, forkName? } * Body: { userPubkey, forkName? }
@ -57,10 +96,12 @@ export const POST: RequestHandler = async ({ params, request }) => {
// Decode user pubkey if needed // Decode user pubkey if needed
let userPubkeyHex = userPubkey; let userPubkeyHex = userPubkey;
try { try {
const userDecoded = nip19.decode(userPubkey); const userDecoded = nip19.decode(userPubkey) as { type: string; data: unknown };
if (userDecoded.type === 'npub') { // Type guard: check if it's an npub
userPubkeyHex = userDecoded.data as string; if (userDecoded.type === 'npub' && typeof userDecoded.data === 'string') {
userPubkeyHex = userDecoded.data;
} }
// If not npub, assume it's already hex
} catch { } catch {
// Assume it's already hex // Assume it's already hex
} }
@ -150,34 +191,108 @@ export const POST: RequestHandler = async ({ params, request }) => {
const { outbox } = await getUserRelays(userPubkeyHex, nostrClient); const { outbox } = await getUserRelays(userPubkeyHex, nostrClient);
const combinedRelays = combineRelays(outbox); 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) { if (publishResult.success.length === 0) {
// Clean up repo if announcement failed // Clean up repo if announcement failed
console.error(`[Fork] ✗ Fork announcement failed after all retries. Cleaning up repository.`);
await execAsync(`rm -rf "${forkRepoPath}"`).catch(() => {}); 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) // 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 ownershipService = new OwnershipTransferService(combinedRelays);
const initialOwnershipEvent = ownershipService.createInitialOwnershipEvent(userPubkeyHex, forkRepoName); const initialOwnershipEvent = ownershipService.createInitialOwnershipEvent(userPubkeyHex, forkRepoName);
const signedOwnershipEvent = await signEventWithNIP07(initialOwnershipEvent); const signedOwnershipEvent = await signEventWithNIP07(initialOwnershipEvent);
await nostrClient.publishEvent(signedOwnershipEvent, combinedRelays).catch(err => { const ownershipPublishResult = await publishEventWithRetry(
console.warn('Failed to publish initial ownership event for fork:', err); 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) // 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); 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({ return json({
success: true, success: true,
fork: { fork: {
npub: userNpub, npub: userNpub,
repo: forkRepoName, repo: forkRepoName,
url: forkGitUrl, 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) { } catch (err) {
console.error('Error forking repository:', err); console.error('Error forking repository:', err);

33
src/routes/repos/[npub]/[repo]/+page.svelte

@ -156,22 +156,45 @@
error = null; error = null;
try { try {
console.log(`[Fork UI] Starting fork of ${npub}/${repo}...`);
const response = await fetch(`/api/repos/${npub}/${repo}/fork`, { const response = await fetch(`/api/repos/${npub}/${repo}/fork`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userPubkey }) body: JSON.stringify({ userPubkey })
}); });
if (response.ok) {
const data = await response.json(); const data = await response.json();
alert(`Repository forked successfully! Visit /repos/${data.fork.npub}/${data.fork.repo}`);
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}`); goto(`/repos/${data.fork.npub}/${data.fork.repo}`);
} else { } else {
const data = await response.json(); const errorMessage = data.error || 'Failed to fork repository';
error = 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) { } 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 { } finally {
forking = false; forking = false;
} }

Loading…
Cancel
Save