@ -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 , 2 s , 4 s
* /
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 ) ;