diff --git a/nostr/commit-signatures.jsonl b/nostr/commit-signatures.jsonl index 8156697..1af4693 100644 --- a/nostr/commit-signatures.jsonl +++ b/nostr/commit-signatures.jsonl @@ -6,3 +6,4 @@ {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771519913,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix swagger"]],"content":"Signed commit: fix swagger","id":"c0eb40be64306e1b11eba642ad357fd877776f50c8e64867cff845b92851c60e","sig":"194bedf752e25da9924bcf16b55eec0b29ae5da8bcd3f2a646e072f08d2d8f499269d42fafe6c056effb22840be0101c243aa1852b41899a73b242e6d6ad2932"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771520422,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","correct icons"]],"content":"Signed commit: correct icons","id":"3d630436d21542aa097b574829ba03f9700db4d707f3d7065bc24000321d0ba2","sig":"6e345bb8ca6fef352400dd10a801d1f41b8798b7a0307eba9af84ea3b4045235b50510905ab2cc9cbdd2894b56a0d1524560a9347c137f39cf756c43ca72c326"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771520523,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix contrast"]],"content":"Signed commit: fix contrast","id":"210177972a67b45a8c56494f2423987ffd30fc5594c539ed6a9f23c0f0992d21","sig":"c3122ebc0055f5a7145d394b9461b811b6e37a7423493d62a6debf7078c006435352e2e2a4259fce6a8a13486bdd137e2e7a49bfdf512a37a73d0c36d405ff2f"} +{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771522633,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","adjusting api for themes"]],"content":"Signed commit: adjusting api for themes","id":"c6125da849827ef6481eed3588231630470289db0176066fc9c1e044f839976b","sig":"7a943b493af9d7108a26fb3bad8166e58ba2ed08eb6c24c178775387620601e6a130ce8a0f344a79e637fc4e75ed2e6d308a242101b14bdb38ccb901c09ff13f"} diff --git a/src/app.css b/src/app.css index 9bcf8f0..a177ea3 100644 --- a/src/app.css +++ b/src/app.css @@ -1030,6 +1030,24 @@ button.theme-option.active img.theme-icon-option, max-width: 200px; } +.repo-badge.transferred { + opacity: 0.6; + filter: grayscale(0.5); + position: relative; +} + +.repo-badge.transferred:hover { + opacity: 0.8; + filter: grayscale(0.3); +} + +.transferred-badge { + font-size: 0.8rem; + color: var(--text-secondary); + margin-left: 0.25rem; + opacity: 0.7; +} + @media (max-width: 768px) { .my-repos-section { padding: 1rem; diff --git a/src/lib/components/TransferNotification.svelte b/src/lib/components/TransferNotification.svelte new file mode 100644 index 0000000..3d54bd0 --- /dev/null +++ b/src/lib/components/TransferNotification.svelte @@ -0,0 +1,233 @@ + + +
+
+
+

Repository Ownership Transfer

+ +
+
+

+ You have been named as the new owner of the repository: {transfer.repoName} +

+

+ Transfer initiated on {formattedDate} +

+

+ Please complete the transfer by publishing a new repo announcement. +

+
+
+ + +
+
+
+ + diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index fc046a5..dfd4cfe 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -5,6 +5,7 @@ import { goto } from '$app/navigation'; import Footer from '$lib/components/Footer.svelte'; import NavBar from '$lib/components/NavBar.svelte'; + import TransferNotification from '$lib/components/TransferNotification.svelte'; import type { Snippet } from 'svelte'; import { getPublicKeyWithNIP07, isNIP07Available } from '$lib/services/nostr/nip07-signer.js'; import { determineUserLevel, decodePubkey } from '$lib/services/nostr/user-level-service.js'; @@ -20,6 +21,21 @@ // User level checking state let checkingUserLevel = $state(false); + // Transfer notification state + type PendingTransfer = { + eventId: string; + fromPubkey: string; + toPubkey: string; + repoTag: string; + repoName: string; + originalOwner: string; + timestamp: number; + createdAt: string; + event: any; + }; + let pendingTransfers = $state([]); + let dismissedTransfers = $state>(new Set()); + onMount(() => { // Only run client-side code if (typeof window === 'undefined') return; @@ -111,8 +127,10 @@ ); // Update activity if user is logged in - if (levelResult.userPubkey) { + if (levelResult.userPubkey && levelResult.userPubkeyHex) { updateActivity(); + // Check for pending transfers + checkPendingTransfers(levelResult.userPubkeyHex); } } catch (err) { console.error('Failed to check user level:', err); @@ -124,6 +142,33 @@ } } + async function checkPendingTransfers(userPubkeyHex: string) { + try { + const response = await fetch('/api/transfers/pending', { + headers: { + 'X-User-Pubkey': userPubkeyHex + } + }); + + if (response.ok) { + const data = await response.json(); + if (data.pendingTransfers && data.pendingTransfers.length > 0) { + // Filter out dismissed transfers + pendingTransfers = data.pendingTransfers.filter( + (t: { eventId: string }) => !dismissedTransfers.has(t.eventId) + ); + } + } + } catch (err) { + console.error('Failed to check for pending transfers:', err); + } + } + + function dismissTransfer(eventId: string) { + dismissedTransfers.add(eventId); + pendingTransfers = pendingTransfers.filter(t => t.eventId !== eventId); + } + function applyTheme() { // Remove all theme attributes first document.documentElement.removeAttribute('data-theme'); @@ -164,12 +209,29 @@ // Subscribe to user store const userState = $derived($userStore); + + // Check for transfers when user logs in + $effect(() => { + const currentUser = $userStore; + if (currentUser.userPubkeyHex && !checkingUserLevel) { + checkPendingTransfers(currentUser.userPubkeyHex); + } else if (!currentUser.userPubkeyHex) { + // Clear transfers when user logs out + pendingTransfers = []; + dismissedTransfers.clear(); + } + }); {#if !isSplashPage} {/if} + +{#each pendingTransfers as transfer (transfer.eventId)} + dismissTransfer(e.detail.eventId)} /> +{/each} + {#if !isSplashPage && checkingUserLevel}
diff --git a/src/routes/api-docs/+page.svelte b/src/routes/api-docs/+page.svelte index fef1de3..b1803c5 100644 --- a/src/routes/api-docs/+page.svelte +++ b/src/routes/api-docs/+page.svelte @@ -240,6 +240,14 @@ margin: 0 !important; } + /* Schemes section - server URL row */ + :global(.swagger-ui .schemes) { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + /* Scheme container and filter */ :global(.swagger-ui .scheme-container), :global(.swagger-ui .filter-container) { @@ -247,7 +255,8 @@ border: 1px solid var(--border-color) !important; border-radius: 0.375rem; padding: 1rem; - margin-bottom: 2rem; + margin-top: 1.5rem; + margin-bottom: 1.5rem; } :global(.swagger-ui .scheme-container label), diff --git a/src/routes/api/repos/[npub]/[repo]/fork/+server.ts b/src/routes/api/repos/[npub]/[repo]/fork/+server.ts index 476201b..7a76cbb 100644 --- a/src/routes/api/repos/[npub]/[repo]/fork/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/fork/+server.ts @@ -362,6 +362,33 @@ export const POST: RequestHandler = async ({ params, request }) => { logger.info({ operation: 'fork', originalRepo: `${npub}/${repo}`, forkRepo: `${userNpub}/${forkRepoName}` }, 'Provisioning fork repository...'); await repoManager.provisionRepo(signedForkAnnouncement, signedOwnershipEvent, false); + // Save fork announcement to repo (offline papertrail) + try { + const { generateVerificationFile, VERIFICATION_FILE_PATH } = await import('$lib/services/nostr/repo-verification.js'); + const { fileManager } = await import('$lib/services/service-registry.js'); + const announcementFileContent = generateVerificationFile(signedForkAnnouncement, userPubkeyHex); + + // Save to repo if it exists locally (should exist after provisioning) + if (fileManager.repoExists(userNpub, forkRepoName)) { + await fileManager.writeFile( + userNpub, + forkRepoName, + VERIFICATION_FILE_PATH, + announcementFileContent, + `Add fork repository announcement: ${signedForkAnnouncement.id.slice(0, 16)}...`, + 'Nostr', + `${userPubkeyHex}@nostr`, + 'main' + ).catch(err => { + // Log but don't fail - publishing to relays is more important + logger.warn({ error: err, npub: userNpub, repo: forkRepoName }, 'Failed to save fork announcement to repo'); + }); + } + } catch (err) { + // Log but don't fail - publishing to relays is more important + logger.warn({ error: err, npub: userNpub, repo: forkRepoName }, 'Failed to save fork announcement to repo'); + } + logger.info({ operation: 'fork', originalRepo: `${npub}/${repo}`, diff --git a/src/routes/api/repos/[npub]/[repo]/settings/+server.ts b/src/routes/api/repos/[npub]/[repo]/settings/+server.ts index 5420ab2..b6649df 100644 --- a/src/routes/api/repos/[npub]/[repo]/settings/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/settings/+server.ts @@ -4,7 +4,7 @@ import { json, error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { nostrClient, maintainerService, ownershipTransferService } from '$lib/services/service-registry.js'; +import { nostrClient, maintainerService, ownershipTransferService, fileManager } from '$lib/services/service-registry.js'; import { DEFAULT_NOSTR_RELAYS, combineRelays } from '$lib/config.js'; import { getUserRelays } from '$lib/services/nostr/user-relays.js'; import { KIND } from '$lib/types/nostr.js'; @@ -12,6 +12,9 @@ import { signEventWithNIP07 } from '$lib/services/nostr/nip07-signer.js'; import { createRepoGetHandler, withRepoValidation } from '$lib/utils/api-handlers.js'; import type { RepoRequestContext, RequestEvent } from '$lib/utils/api-context.js'; import { handleApiError, handleValidationError, handleNotFoundError, handleAuthorizationError } from '$lib/utils/error-handler.js'; +import { generateVerificationFile, VERIFICATION_FILE_PATH } from '$lib/services/nostr/repo-verification.js'; +import { nip19 } from 'nostr-tools'; +import logger from '$lib/services/logger.js'; /** * GET - Get repository settings @@ -188,6 +191,31 @@ export const POST: RequestHandler = withRepoValidation( throw error(500, 'Failed to publish updated announcement to relays'); } + // Save updated announcement to repo (offline papertrail) + try { + const announcementFileContent = generateVerificationFile(signedEvent, currentOwner); + + // Save to repo if it exists locally + if (fileManager.repoExists(repoContext.npub, repoContext.repo)) { + await fileManager.writeFile( + repoContext.npub, + repoContext.repo, + VERIFICATION_FILE_PATH, + announcementFileContent, + `Update repository announcement: ${signedEvent.id.slice(0, 16)}...`, + 'Nostr', + `${currentOwner}@nostr`, + 'main' + ).catch(err => { + // Log but don't fail - publishing to relays is more important + logger.warn({ error: err, npub: repoContext.npub, repo: repoContext.repo }, 'Failed to save updated announcement to repo'); + }); + } + } catch (err) { + // Log but don't fail - publishing to relays is more important + logger.warn({ error: err, npub: repoContext.npub, repo: repoContext.repo }, 'Failed to save updated announcement to repo'); + } + return json({ success: true, event: signedEvent }); }, { operation: 'updateSettings', requireRepoAccess: false } // Override to check owner instead diff --git a/src/routes/api/repos/[npub]/[repo]/transfer/+server.ts b/src/routes/api/repos/[npub]/[repo]/transfer/+server.ts index 09c5d4c..d3abd0e 100644 --- a/src/routes/api/repos/[npub]/[repo]/transfer/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/transfer/+server.ts @@ -4,16 +4,17 @@ import { json, error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { ownershipTransferService, nostrClient } from '$lib/services/service-registry.js'; +import { ownershipTransferService, nostrClient, fileManager } from '$lib/services/service-registry.js'; import { combineRelays } from '$lib/config.js'; import { KIND } from '$lib/types/nostr.js'; -import { verifyEvent } from 'nostr-tools'; +import { verifyEvent, nip19 } from 'nostr-tools'; import type { NostrEvent } from '$lib/types/nostr.js'; import { getUserRelays } from '$lib/services/nostr/user-relays.js'; import { createRepoGetHandler, withRepoValidation } from '$lib/utils/api-handlers.js'; import type { RepoRequestContext } from '$lib/utils/api-context.js'; import type { RequestEvent } from '@sveltejs/kit'; import { handleApiError, handleValidationError, handleAuthorizationError } from '$lib/utils/error-handler.js'; +import logger from '$lib/services/logger.js'; /** * GET - Get current owner and transfer history @@ -119,6 +120,35 @@ export const POST: RequestHandler = withRepoValidation( throw handleApiError(new Error('Failed to publish transfer event to any relays'), { operation: 'transferOwnership', npub: repoContext.npub, repo: repoContext.repo }, 'Failed to publish transfer event to any relays'); } + // Save transfer event to repo (offline papertrail - step 1 requirement) + try { + const transferEventContent = JSON.stringify(transferEvent, null, 2) + '\n'; + // Use consistent filename pattern: .nostr-ownership-transfer-{eventId}.json + const transferFileName = `.nostr-ownership-transfer-${transferEvent.id}.json`; + + // Save to repo if it exists locally + if (fileManager.repoExists(repoContext.npub, repoContext.repo)) { + await fileManager.writeFile( + repoContext.npub, + repoContext.repo, + transferFileName, + transferEventContent, + `Add ownership transfer event: ${transferEvent.id.slice(0, 16)}...`, + 'Nostr', + `${requestContext.userPubkeyHex}@nostr`, + 'main' + ).catch(err => { + // Log but don't fail - publishing to relays is more important + logger.warn({ error: err, npub: repoContext.npub, repo: repoContext.repo }, 'Failed to save transfer event to repo'); + }); + } else { + logger.debug({ npub: repoContext.npub, repo: repoContext.repo }, 'Repo does not exist locally, skipping transfer event save to repo'); + } + } catch (err) { + // Log but don't fail - publishing to relays is more important + logger.warn({ error: err, npub: repoContext.npub, repo: repoContext.repo }, 'Failed to save transfer event to repo'); + } + // Clear cache so new owner is recognized immediately ownershipTransferService.clearCache(repoContext.repoOwnerPubkey, repoContext.repo); @@ -126,7 +156,9 @@ export const POST: RequestHandler = withRepoValidation( success: true, event: transferEvent, published: result, - message: 'Ownership transfer initiated successfully' + message: 'Ownership transfer initiated successfully', + // Signal to client that page should refresh + refresh: true }); }, { operation: 'transferOwnership', requireRepoAccess: false } // Override to check owner instead diff --git a/src/routes/api/transfers/pending/+server.ts b/src/routes/api/transfers/pending/+server.ts new file mode 100644 index 0000000..ad2e2f0 --- /dev/null +++ b/src/routes/api/transfers/pending/+server.ts @@ -0,0 +1,126 @@ +/** + * API endpoint to check for pending ownership transfers for the logged-in user + */ + +import { json, error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { nostrClient } from '$lib/services/service-registry.js'; +import { DEFAULT_NOSTR_RELAYS, DEFAULT_NOSTR_SEARCH_RELAYS, combineRelays } from '$lib/config.js'; +import { KIND } from '$lib/types/nostr.js'; +import type { NostrEvent } from '$lib/types/nostr.js'; +import { verifyEvent } from 'nostr-tools'; +import { getUserRelays } from '$lib/services/nostr/user-relays.js'; +import logger from '$lib/services/logger.js'; + +export const GET: RequestHandler = async ({ request }) => { + const userPubkeyHex = request.headers.get('X-User-Pubkey'); + + if (!userPubkeyHex) { + return json({ pendingTransfers: [] }); + } + + try { + // Get user's relays for comprehensive search + const { inbox, outbox } = await getUserRelays(userPubkeyHex, nostrClient); + // Combine user relays with default and search relays + const userRelays = [...inbox, ...outbox]; + const allRelays = [...new Set([...userRelays, ...DEFAULT_NOSTR_RELAYS, ...DEFAULT_NOSTR_SEARCH_RELAYS])]; + + // Create a new client with all relays for comprehensive search + const { NostrClient } = await import('$lib/services/nostr/nostr-client.js'); + const searchClient = new NostrClient(allRelays); + + // Search for transfer events where this user is the new owner (p tag) + const transferEvents = await searchClient.fetchEvents([ + { + kinds: [KIND.OWNERSHIP_TRANSFER], + '#p': [userPubkeyHex], + limit: 100 + } + ]); + + // Filter for valid, non-self-transfer events that haven't been completed + const pendingTransfers: Array<{ + eventId: string; + fromPubkey: string; + toPubkey: string; + repoTag: string; + repoName: string; + originalOwner: string; + timestamp: number; + createdAt: string; + event: NostrEvent; + }> = []; + + for (const event of transferEvents) { + // Verify event signature + if (!verifyEvent(event)) { + continue; + } + + // Skip self-transfers + if (event.pubkey === userPubkeyHex) { + continue; + } + + // Extract repo tag + const aTag = event.tags.find(t => t[0] === 'a'); + if (!aTag || !aTag[1]) { + continue; + } + + // Parse repo tag: kind:pubkey:repo + const repoTag = aTag[1]; + const parts = repoTag.split(':'); + if (parts.length < 3) { + continue; + } + + const originalOwner = parts[1]; + const repoName = parts[2]; + + // Extract new owner (p tag) + const pTag = event.tags.find(t => t[0] === 'p'); + if (!pTag || !pTag[1] || pTag[1] !== userPubkeyHex) { + continue; + } + + // Check if transfer is already completed by checking for a newer repo announcement from the new owner + // This is a simple check - if there's a newer announcement from the new owner for this repo, transfer is complete + const newerAnnouncements = await searchClient.fetchEvents([ + { + kinds: [KIND.REPO_ANNOUNCEMENT], + authors: [userPubkeyHex], + '#d': [repoName], + since: event.created_at, + limit: 1 + } + ]); + + // If there's a newer announcement from the new owner, transfer is complete + if (newerAnnouncements.length > 0) { + continue; + } + + pendingTransfers.push({ + eventId: event.id, + fromPubkey: event.pubkey, + toPubkey: userPubkeyHex, + repoTag, + repoName, + originalOwner, + timestamp: event.created_at, + createdAt: new Date(event.created_at * 1000).toISOString(), + event + }); + } + + // Sort by timestamp (newest first) + pendingTransfers.sort((a, b) => b.timestamp - a.timestamp); + + return json({ pendingTransfers }); + } catch (err) { + logger.error({ error: err, userPubkeyHex }, 'Error checking for pending transfers'); + return json({ pendingTransfers: [], error: 'Failed to check for pending transfers' }); + } +}; diff --git a/src/routes/repos/+page.svelte b/src/routes/repos/+page.svelte index 35665ba..f8e69e4 100644 --- a/src/routes/repos/+page.svelte +++ b/src/routes/repos/+page.svelte @@ -31,7 +31,8 @@ let deletingRepo = $state<{ npub: string; repo: string } | null>(null); // User's own repositories (where they are owner or maintainer) - let myRepos = $state>([]); + // Also includes repos they transferred away (marked as transferred) + let myRepos = $state>([]); let loadingMyRepos = $state(false); import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; @@ -60,6 +61,13 @@ } }); + // Reload my repos when navigating to repos page (e.g., after transfer) + $effect(() => { + if ($page.url.pathname === '/repos' && userPubkeyHex) { + loadMyRepos().catch(err => console.warn('Failed to reload my repos on page navigation:', err)); + } + }); + // Sync with userStore - if userStore says logged out, clear local state $effect(() => { const currentUser = $userStore; @@ -202,7 +210,7 @@ loadingMyRepos = true; try { - // Fetch all repos where user is owner + // Fetch all repos where user is current owner const ownerRepos = await nostrClient.fetchEvents([ { kinds: [KIND.REPO_ANNOUNCEMENT], @@ -211,8 +219,9 @@ } ]); - const repos: Array<{ event: NostrEvent; npub: string; repoName: string }> = []; + const repos: Array<{ event: NostrEvent; npub: string; repoName: string; transferred?: boolean; currentOwner?: string }> = []; + // Add repos where user is current owner for (const event of ownerRepos) { const dTag = event.tags.find(t => t[0] === 'd')?.[1]; if (!dTag) continue; @@ -222,13 +231,64 @@ repos.push({ event, npub, - repoName: dTag + repoName: dTag, + transferred: false }); } catch (err) { console.warn('Failed to encode npub for repo:', err); } } + // Fetch repos that were transferred FROM this user (where they were original owner) + // Search for transfer events where this user is the 'from' pubkey + const { OwnershipTransferService } = await import('$lib/services/nostr/ownership-transfer-service.js'); + const ownershipService = new OwnershipTransferService(DEFAULT_NOSTR_RELAYS); + + // Get all repos where user was original owner + const originalOwnerRepos = await nostrClient.fetchEvents([ + { + kinds: [KIND.REPO_ANNOUNCEMENT], + authors: [userPubkeyHex], + limit: 100 + } + ]); + + for (const originalEvent of originalOwnerRepos) { + const dTag = originalEvent.tags.find(t => t[0] === 'd')?.[1]; + if (!dTag) continue; + + // Check current owner + const currentOwner = await ownershipService.getCurrentOwner(userPubkeyHex, dTag); + + // If current owner is different, this repo was transferred + if (currentOwner !== userPubkeyHex) { + // Fetch the current announcement from the new owner + const currentAnnouncements = await nostrClient.fetchEvents([ + { + kinds: [KIND.REPO_ANNOUNCEMENT], + authors: [currentOwner], + '#d': [dTag], + limit: 1 + } + ]); + + if (currentAnnouncements.length > 0) { + try { + const npub = nip19.npubEncode(userPubkeyHex); // Original owner npub + repos.push({ + event: currentAnnouncements[0], // Use current announcement + npub, + repoName: dTag, + transferred: true, + currentOwner + }); + } catch (err) { + console.warn('Failed to encode npub for transferred repo:', err); + } + } + } + } + // Sort by created_at descending (newest first) repos.sort((a, b) => b.event.created_at - a.event.created_at); @@ -538,13 +598,22 @@ {#each myRepos as item} {@const repo = item.event} {@const repoImage = getRepoImage(repo)} - + {@const isTransferred = item.transferred || false} + {#if repoImage} {getRepoName(repo)} {:else} Repository {/if} {getRepoName(repo)} + {#if isTransferred} + + {/if} {/each}
diff --git a/src/routes/signup/+page.svelte b/src/routes/signup/+page.svelte index 811e4e9..4de76e8 100644 --- a/src/routes/signup/+page.svelte +++ b/src/routes/signup/+page.svelte @@ -58,12 +58,110 @@ onMount(async () => { nip07Available = isNIP07Available(); - // Check for query params to pre-fill form (for registering local clones) + // Check for query params to pre-fill form (for registering local clones or transfers) const urlParams = $page.url.searchParams; - const npubParam = urlParams.get('npub'); + const transferParam = urlParams.get('transfer'); + const transferEventId = urlParams.get('transferEventId'); + const originalOwnerParam = urlParams.get('originalOwner'); const repoParam = urlParams.get('repo'); + const repoTagParam = urlParams.get('repoTag'); + const npubParam = urlParams.get('npub') || originalOwnerParam; - if (npubParam && repoParam) { + // Handle transfer flow (step 4) + if (transferParam === 'true' && originalOwnerParam && repoParam && repoTagParam) { + try { + // Fetch the original repo announcement to preload data + const decoded = nip19.decode(originalOwnerParam); + if (decoded.type === 'npub') { + const pubkey = decoded.data as string; + const events = await nostrClient.fetchEvents([ + { + kinds: [KIND.REPO_ANNOUNCEMENT], + authors: [pubkey], + '#d': [repoParam], + limit: 1 + } + ]); + + if (events.length > 0) { + const event = events[0]; + + // Pre-fill repo name + repoName = repoParam; + + // Pre-fill description + const descTag = event.tags.find(t => t[0] === 'description')?.[1]; + if (descTag) description = descTag; + + // Pre-fill clone URLs (add current domain URL) + const existingCloneUrls = event.tags + .filter(t => t[0] === 'clone') + .flatMap(t => t.slice(1)) + .filter(url => url && typeof url === 'string'); + + const gitDomain = $page.data.gitDomain || 'localhost:6543'; + const protocol = gitDomain.startsWith('localhost') ? 'http' : 'https'; + const currentDomainUrl = `${protocol}://${gitDomain}/${originalOwnerParam}/${repoParam}.git`; + + // Check if current domain URL already exists + const hasCurrentDomain = existingCloneUrls.some(url => url.includes(gitDomain)); + + if (!hasCurrentDomain) { + cloneUrls = [...existingCloneUrls, currentDomainUrl]; + } else { + cloneUrls = existingCloneUrls.length > 0 ? existingCloneUrls : [currentDomainUrl]; + } + + // Pre-fill other fields + const nameTag = event.tags.find(t => t[0] === 'name')?.[1]; + if (nameTag && !repoName) repoName = nameTag; + + const imageTag = event.tags.find(t => t[0] === 'image')?.[1]; + if (imageTag) imageUrl = imageTag; + + const bannerTag = event.tags.find(t => t[0] === 'banner')?.[1]; + if (bannerTag) bannerUrl = bannerTag; + + const webTags = event.tags.filter(t => t[0] === 'web'); + if (webTags.length > 0) { + webUrls = webTags.flatMap(t => t.slice(1)).filter(url => url && typeof url === 'string'); + } + + const maintainerTags = event.tags.filter(t => t[0] === 'maintainers'); + if (maintainerTags.length > 0) { + maintainers = maintainerTags.flatMap(t => t.slice(1)).filter(m => m && typeof m === 'string'); + } + + const relayTags = event.tags.filter(t => t[0] === 'relays'); + if (relayTags.length > 0) { + relays = relayTags.flatMap(t => t.slice(1)).filter(r => r && typeof r === 'string'); + } + + const isPrivateTag = event.tags.find(t => + (t[0] === 'private' && t[1] === 'true') || + (t[0] === 't' && t[1] === 'private') + ); + if (isPrivateTag) isPrivate = true; + + // Set existing repo ref for updating + existingRepoRef = event.id; + } else { + // No announcement found, just set the clone URL with current domain + repoName = repoParam; + const gitDomain = $page.data.gitDomain || 'localhost:6543'; + const protocol = gitDomain.startsWith('localhost') ? 'http' : 'https'; + cloneUrls = [`${protocol}://${gitDomain}/${originalOwnerParam}/${repoParam}.git`]; + } + } + } catch (err) { + console.warn('Failed to pre-fill form from transfer data:', err); + // Still set basic info + repoName = repoParam; + const gitDomain = $page.data.gitDomain || 'localhost:6543'; + const protocol = gitDomain.startsWith('localhost') ? 'http' : 'https'; + cloneUrls = [`${protocol}://${originalOwnerParam}/${repoParam}.git`]; + } + } else if (npubParam && repoParam) { // Pre-fill repo name repoName = repoParam; @@ -1478,9 +1576,19 @@ // Redirect to the newly created repository page // Use invalidateAll to ensure the repos list refreshes const userNpub = nip19.npubEncode(pubkey); + + // Check if this is a transfer completion (from query params) + const urlParams = $page.url.searchParams; + const isTransfer = urlParams.get('transfer') === 'true'; + setTimeout(() => { // Invalidate all caches and redirect - goto(`/repos/${userNpub}/${dTag}`, { invalidateAll: true, replaceState: false }); + if (isTransfer) { + // After transfer, redirect to repos page to see updated state + goto('/repos', { invalidateAll: true, replaceState: false }); + } else { + goto(`/repos/${userNpub}/${dTag}`, { invalidateAll: true, replaceState: false }); + } }, 2000); } else { // Show detailed error information