14 changed files with 1275 additions and 174 deletions
@ -0,0 +1,474 @@
@@ -0,0 +1,474 @@
|
||||
<script lang="ts"> |
||||
import { onMount } from 'svelte'; |
||||
import { NostrClient } from '$lib/services/nostr/nostr-client.js'; |
||||
import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; |
||||
import { KIND } from '$lib/types/nostr.js'; |
||||
import { generateVerificationFile, VERIFICATION_FILE_PATH } from '$lib/services/nostr/repo-verification.js'; |
||||
import { nip19 } from 'nostr-tools'; |
||||
import type { NostrEvent } from '$lib/types/nostr.js'; |
||||
|
||||
let npub = $state(''); |
||||
let repoName = $state(''); |
||||
let loading = $state(false); |
||||
let error = $state<string | null>(null); |
||||
let verificationContent = $state<string | null>(null); |
||||
let announcementEvent = $state<NostrEvent | null>(null); |
||||
|
||||
const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS); |
||||
|
||||
async function generateVerification() { |
||||
if (!npub.trim() || !repoName.trim()) { |
||||
error = 'Please enter both npub and repository name'; |
||||
return; |
||||
} |
||||
|
||||
loading = true; |
||||
error = null; |
||||
verificationContent = null; |
||||
announcementEvent = null; |
||||
|
||||
try { |
||||
// Decode npub to get pubkey |
||||
let ownerPubkey: string; |
||||
try { |
||||
const decoded = nip19.decode(npub.trim()); |
||||
if (decoded.type !== 'npub') { |
||||
error = 'Invalid npub format'; |
||||
loading = false; |
||||
return; |
||||
} |
||||
ownerPubkey = decoded.data as string; |
||||
} catch (err) { |
||||
error = `Failed to decode npub: ${err instanceof Error ? err.message : String(err)}`; |
||||
loading = false; |
||||
return; |
||||
} |
||||
|
||||
// Fetch repository announcement from Nostr |
||||
const events = await nostrClient.fetchEvents([ |
||||
{ |
||||
kinds: [KIND.REPO_ANNOUNCEMENT], |
||||
authors: [ownerPubkey], |
||||
'#d': [repoName.trim()], |
||||
limit: 1 |
||||
} |
||||
]); |
||||
|
||||
if (events.length === 0) { |
||||
error = `No repository announcement found for ${npub}/${repoName.trim()}. Make sure you've published the repository announcement to Nostr.`; |
||||
loading = false; |
||||
return; |
||||
} |
||||
|
||||
const announcement = events[0] as NostrEvent; |
||||
announcementEvent = announcement; |
||||
|
||||
// Generate verification file content |
||||
verificationContent = generateVerificationFile(announcement, ownerPubkey); |
||||
} catch (err) { |
||||
error = `Failed to generate verification file: ${err instanceof Error ? err.message : String(err)}`; |
||||
console.error('Error generating verification:', err); |
||||
} finally { |
||||
loading = false; |
||||
} |
||||
} |
||||
|
||||
function copyToClipboard() { |
||||
if (!verificationContent) return; |
||||
|
||||
navigator.clipboard.writeText(verificationContent).then(() => { |
||||
alert('Verification file content copied to clipboard!'); |
||||
}).catch((err) => { |
||||
console.error('Failed to copy:', err); |
||||
alert('Failed to copy to clipboard. Please select and copy manually.'); |
||||
}); |
||||
} |
||||
|
||||
function downloadFile() { |
||||
if (!verificationContent) return; |
||||
|
||||
const blob = new Blob([verificationContent], { type: 'text/plain' }); |
||||
const url = URL.createObjectURL(blob); |
||||
const a = document.createElement('a'); |
||||
a.href = url; |
||||
a.download = VERIFICATION_FILE_PATH; |
||||
document.body.appendChild(a); |
||||
a.click(); |
||||
document.body.removeChild(a); |
||||
URL.revokeObjectURL(url); |
||||
} |
||||
</script> |
||||
|
||||
<svelte:head> |
||||
<title>Generate Repository Verification File - GitRepublic</title> |
||||
</svelte:head> |
||||
|
||||
<div class="verify-container"> |
||||
<div class="verify-content"> |
||||
<h1>Generate Repository Verification File</h1> |
||||
<p class="description"> |
||||
This tool helps you generate a verification file for a repository that isn't saved to the server yet. |
||||
The verification file proves ownership by linking your Nostr repository announcement to your git repository. |
||||
</p> |
||||
|
||||
<div class="form-section"> |
||||
<h2>Repository Information</h2> |
||||
<form onsubmit={(e) => { e.preventDefault(); generateVerification(); }}> |
||||
<div class="form-group"> |
||||
<label for="npub">Repository Owner (npub):</label> |
||||
<input |
||||
id="npub" |
||||
type="text" |
||||
bind:value={npub} |
||||
placeholder="npub1..." |
||||
disabled={loading} |
||||
required |
||||
/> |
||||
<small>Your Nostr public key in npub format</small> |
||||
</div> |
||||
|
||||
<div class="form-group"> |
||||
<label for="repo">Repository Name:</label> |
||||
<input |
||||
id="repo" |
||||
type="text" |
||||
bind:value={repoName} |
||||
placeholder="my-repo" |
||||
disabled={loading} |
||||
required |
||||
/> |
||||
<small>The repository identifier (d-tag) from your announcement</small> |
||||
</div> |
||||
|
||||
<button type="submit" disabled={loading || !npub.trim() || !repoName.trim()} class="generate-button"> |
||||
{loading ? 'Generating...' : 'Generate Verification File'} |
||||
</button> |
||||
</form> |
||||
</div> |
||||
|
||||
{#if error} |
||||
<div class="error-message"> |
||||
<strong>Error:</strong> {error} |
||||
</div> |
||||
{/if} |
||||
|
||||
{#if verificationContent} |
||||
<div class="verification-section"> |
||||
<h2>Verification File Generated</h2> |
||||
<p class="instructions"> |
||||
<strong>Next steps:</strong> |
||||
</p> |
||||
<ol class="steps"> |
||||
<li>Copy the verification file content below</li> |
||||
<li>Create a file named <code>{VERIFICATION_FILE_PATH}</code> in the root of your git repository</li> |
||||
<li>Paste the content into that file</li> |
||||
<li>Commit and push the file to your repository</li> |
||||
<li>Once the repository is saved to the server, verification will be automatic</li> |
||||
</ol> |
||||
|
||||
{#if announcementEvent} |
||||
<div class="announcement-info"> |
||||
<h3>Announcement Details</h3> |
||||
<ul> |
||||
<li><strong>Event ID:</strong> <code>{announcementEvent.id}</code></li> |
||||
<li><strong>Created:</strong> {new Date(announcementEvent.created_at * 1000).toLocaleString()}</li> |
||||
</ul> |
||||
</div> |
||||
{/if} |
||||
|
||||
<div class="verification-file"> |
||||
<div class="file-header"> |
||||
<span class="filename">{VERIFICATION_FILE_PATH}</span> |
||||
<div class="file-actions"> |
||||
<button onclick={copyToClipboard} class="copy-button">Copy</button> |
||||
<button onclick={downloadFile} class="download-button">Download</button> |
||||
</div> |
||||
</div> |
||||
<pre class="file-content"><code>{verificationContent}</code></pre> |
||||
</div> |
||||
</div> |
||||
{/if} |
||||
|
||||
<div class="info-section"> |
||||
<h2>About Verification</h2> |
||||
<p> |
||||
Repository verification proves that you own both the Nostr repository announcement and the git repository. |
||||
There are two methods: |
||||
</p> |
||||
<ul> |
||||
<li><strong>Self-transfer event</strong> (preferred): A Nostr event that transfers ownership to yourself, proving you control the private key.</li> |
||||
<li><strong>Verification file</strong> (this method): A file in your repository containing the announcement event ID and signature.</li> |
||||
</ul> |
||||
<p> |
||||
The verification file method is useful when your repository isn't on the server yet, as you can generate |
||||
the file and commit it to your repository before the server fetches it. |
||||
</p> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<style> |
||||
.verify-container { |
||||
max-width: 900px; |
||||
margin: 2rem auto; |
||||
padding: 0 1rem; |
||||
} |
||||
|
||||
.verify-content { |
||||
background: var(--bg-primary); |
||||
border: 1px solid var(--border-color); |
||||
border-radius: 0.5rem; |
||||
padding: 2rem; |
||||
} |
||||
|
||||
h1 { |
||||
margin-top: 0; |
||||
color: var(--text-primary); |
||||
} |
||||
|
||||
.description { |
||||
color: var(--text-secondary); |
||||
margin-bottom: 2rem; |
||||
line-height: 1.6; |
||||
} |
||||
|
||||
.form-section { |
||||
margin-bottom: 2rem; |
||||
} |
||||
|
||||
.form-section h2 { |
||||
margin-top: 0; |
||||
margin-bottom: 1rem; |
||||
color: var(--text-primary); |
||||
font-size: 1.25rem; |
||||
} |
||||
|
||||
.form-group { |
||||
margin-bottom: 1.5rem; |
||||
} |
||||
|
||||
.form-group label { |
||||
display: block; |
||||
margin-bottom: 0.5rem; |
||||
color: var(--text-primary); |
||||
font-weight: 500; |
||||
} |
||||
|
||||
.form-group input { |
||||
width: 100%; |
||||
padding: 0.75rem; |
||||
border: 1px solid var(--border-color); |
||||
border-radius: 0.375rem; |
||||
background: var(--bg-secondary); |
||||
color: var(--text-primary); |
||||
font-size: 1rem; |
||||
font-family: monospace; |
||||
} |
||||
|
||||
.form-group input:disabled { |
||||
opacity: 0.6; |
||||
cursor: not-allowed; |
||||
} |
||||
|
||||
.form-group input:focus { |
||||
outline: none; |
||||
border-color: var(--accent); |
||||
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.1); |
||||
} |
||||
|
||||
.form-group small { |
||||
display: block; |
||||
margin-top: 0.25rem; |
||||
color: var(--text-secondary); |
||||
font-size: 0.875rem; |
||||
} |
||||
|
||||
.generate-button { |
||||
padding: 0.75rem 1.5rem; |
||||
background: var(--accent); |
||||
color: var(--accent-text, white); |
||||
border: none; |
||||
border-radius: 0.375rem; |
||||
font-size: 1rem; |
||||
font-weight: 500; |
||||
cursor: pointer; |
||||
transition: all 0.2s ease; |
||||
} |
||||
|
||||
.generate-button:hover:not(:disabled) { |
||||
opacity: 0.9; |
||||
transform: translateY(-1px); |
||||
} |
||||
|
||||
.generate-button:disabled { |
||||
opacity: 0.5; |
||||
cursor: not-allowed; |
||||
} |
||||
|
||||
.error-message { |
||||
padding: 1rem; |
||||
background: rgba(239, 68, 68, 0.1); |
||||
border: 1px solid rgba(239, 68, 68, 0.3); |
||||
border-radius: 0.375rem; |
||||
color: #dc2626; |
||||
margin-bottom: 2rem; |
||||
} |
||||
|
||||
.verification-section { |
||||
margin-top: 2rem; |
||||
padding-top: 2rem; |
||||
border-top: 1px solid var(--border-color); |
||||
} |
||||
|
||||
.verification-section h2 { |
||||
margin-top: 0; |
||||
color: var(--text-primary); |
||||
} |
||||
|
||||
.instructions { |
||||
color: var(--text-primary); |
||||
margin-bottom: 1rem; |
||||
} |
||||
|
||||
.steps { |
||||
color: var(--text-primary); |
||||
margin-bottom: 2rem; |
||||
padding-left: 1.5rem; |
||||
} |
||||
|
||||
.steps li { |
||||
margin-bottom: 0.5rem; |
||||
line-height: 1.6; |
||||
} |
||||
|
||||
.steps code { |
||||
background: var(--bg-secondary); |
||||
padding: 0.125rem 0.375rem; |
||||
border-radius: 0.25rem; |
||||
font-family: monospace; |
||||
color: var(--accent); |
||||
} |
||||
|
||||
.announcement-info { |
||||
background: var(--bg-secondary); |
||||
border: 1px solid var(--border-color); |
||||
border-radius: 0.375rem; |
||||
padding: 1rem; |
||||
margin-bottom: 1.5rem; |
||||
} |
||||
|
||||
.announcement-info h3 { |
||||
margin-top: 0; |
||||
margin-bottom: 0.75rem; |
||||
color: var(--text-primary); |
||||
font-size: 1rem; |
||||
} |
||||
|
||||
.announcement-info ul { |
||||
margin: 0; |
||||
padding-left: 1.5rem; |
||||
color: var(--text-primary); |
||||
} |
||||
|
||||
.announcement-info li { |
||||
margin-bottom: 0.5rem; |
||||
} |
||||
|
||||
.announcement-info code { |
||||
background: var(--bg-primary); |
||||
padding: 0.125rem 0.375rem; |
||||
border-radius: 0.25rem; |
||||
font-family: monospace; |
||||
font-size: 0.875rem; |
||||
word-break: break-all; |
||||
} |
||||
|
||||
.verification-file { |
||||
background: var(--bg-secondary); |
||||
border: 1px solid var(--border-color); |
||||
border-radius: 0.375rem; |
||||
overflow: hidden; |
||||
} |
||||
|
||||
.file-header { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
padding: 0.75rem 1rem; |
||||
background: var(--bg-tertiary); |
||||
border-bottom: 1px solid var(--border-color); |
||||
} |
||||
|
||||
.filename { |
||||
font-family: monospace; |
||||
font-weight: 500; |
||||
color: var(--text-primary); |
||||
} |
||||
|
||||
.file-actions { |
||||
display: flex; |
||||
gap: 0.5rem; |
||||
} |
||||
|
||||
.copy-button, |
||||
.download-button { |
||||
padding: 0.375rem 0.75rem; |
||||
background: var(--bg-primary); |
||||
color: var(--text-primary); |
||||
border: 1px solid var(--border-color); |
||||
border-radius: 0.25rem; |
||||
font-size: 0.875rem; |
||||
cursor: pointer; |
||||
transition: all 0.2s ease; |
||||
} |
||||
|
||||
.copy-button:hover, |
||||
.download-button:hover { |
||||
background: var(--bg-secondary); |
||||
border-color: var(--accent); |
||||
} |
||||
|
||||
.file-content { |
||||
margin: 0; |
||||
padding: 1rem; |
||||
overflow-x: auto; |
||||
background: var(--bg-primary); |
||||
} |
||||
|
||||
.file-content code { |
||||
font-family: 'Courier New', monospace; |
||||
font-size: 0.875rem; |
||||
color: var(--text-primary); |
||||
white-space: pre; |
||||
} |
||||
|
||||
.info-section { |
||||
margin-top: 3rem; |
||||
padding-top: 2rem; |
||||
border-top: 1px solid var(--border-color); |
||||
} |
||||
|
||||
.info-section h2 { |
||||
margin-top: 0; |
||||
color: var(--text-primary); |
||||
} |
||||
|
||||
.info-section p, |
||||
.info-section ul { |
||||
color: var(--text-secondary); |
||||
line-height: 1.6; |
||||
} |
||||
|
||||
.info-section ul { |
||||
margin-top: 0.5rem; |
||||
padding-left: 1.5rem; |
||||
} |
||||
|
||||
.info-section li { |
||||
margin-bottom: 0.5rem; |
||||
} |
||||
|
||||
.info-section strong { |
||||
color: var(--text-primary); |
||||
} |
||||
</style> |
||||
Loading…
Reference in new issue