Browse Source

fix verification badge

main
Silberengel 4 weeks ago
parent
commit
17457b78d8
  1. 2
      README.md
  2. 40
      docs/GIT_CREDENTIAL_HELPER.md
  3. 1
      scripts/Untitled
  4. 12
      scripts/git-credential-nostr.js
  5. 4
      src/lib/services/nostr/ownership-transfer-service.ts
  6. 113
      src/routes/repos/[npub]/[repo]/+page.svelte
  7. 42
      vite.config.ts

2
README.md

@ -351,7 +351,7 @@ See `docs/SECURITY.md` and `docs/SECURITY_IMPLEMENTATION.md` for detailed inform
## Environment Variables ## Environment Variables
- `NOSTRGIT_SECRET_KEY_CLIENT`: User's nsec (bech32 or hex) for client-side git operations via credential helper (optional) - `NOSTRGIT_SECRET_KEY`: User's nsec (bech32 or hex) for client-side git operations via credential helper (optional)
- `GIT_REPO_ROOT`: Path to store git repositories (default: `/repos`) - `GIT_REPO_ROOT`: Path to store git repositories (default: `/repos`)
- `GIT_DOMAIN`: Domain for git repositories (default: `localhost:6543`) - `GIT_DOMAIN`: Domain for git repositories (default: `localhost:6543`)
- `NOSTR_RELAYS`: Comma-separated list of Nostr relays (default: `wss://theforest.nostr1.com`) - `NOSTR_RELAYS`: Comma-separated list of Nostr relays (default: `wss://theforest.nostr1.com`)

40
docs/GIT_CREDENTIAL_HELPER.md

@ -14,7 +14,7 @@ GitRepublic uses NIP-98 HTTP Authentication for git operations. The credential h
chmod +x scripts/git-credential-nostr.js chmod +x scripts/git-credential-nostr.js
``` ```
### 2. Set your NOSTRGIT_SECRET_KEY_CLIENT environment variable ### 2. Set your NOSTRGIT_SECRET_KEY environment variable
**Important:** **Important:**
- This is YOUR user private key (for authenticating your git operations) - This is YOUR user private key (for authenticating your git operations)
@ -22,14 +22,14 @@ chmod +x scripts/git-credential-nostr.js
```bash ```bash
# Option 1: Export in your shell session # Option 1: Export in your shell session
export NOSTRGIT_SECRET_KEY_CLIENT="nsec1..." export NOSTRGIT_SECRET_KEY="nsec1..."
# Option 2: Add to your ~/.bashrc or ~/.zshrc (for persistent setup) # Option 2: Add to your ~/.bashrc or ~/.zshrc (for persistent setup)
echo 'export NOSTRGIT_SECRET_KEY_CLIENT="nsec1..."' >> ~/.bashrc echo 'export NOSTRGIT_SECRET_KEY="nsec1..."' >> ~/.bashrc
source ~/.bashrc source ~/.bashrc
# Option 3: Use a hex private key (64 characters) # Option 3: Use a hex private key (64 characters)
export NOSTRGIT_SECRET_KEY_CLIENT="<your-64-char-hex-private-key>" export NOSTRGIT_SECRET_KEY="<your-64-char-hex-private-key>"
# Note: The script also supports NOSTR_PRIVATE_KEY and NSEC for backward compatibility # Note: The script also supports NOSTR_PRIVATE_KEY and NSEC for backward compatibility
``` ```
@ -88,7 +88,7 @@ git clone https://your-domain.com/npub1abc123.../my-repo.git
git clone http://localhost:5173/api/git/npub1abc123.../my-repo.git git clone http://localhost:5173/api/git/npub1abc123.../my-repo.git
``` ```
The credential helper will automatically generate a NIP-98 auth token using your NOSTRGIT_SECRET_KEY_CLIENT. The credential helper will automatically generate a NIP-98 auth token using your NOSTRGIT_SECRET_KEY.
## Localhost Setup Example ## Localhost Setup Example
@ -102,10 +102,10 @@ npm run dev
# Server runs on http://localhost:5173 # Server runs on http://localhost:5173
``` ```
### 2. Set your NOSTRGIT_SECRET_KEY_CLIENT ### 2. Set your NOSTRGIT_SECRET_KEY
```bash ```bash
export NOSTRGIT_SECRET_KEY_CLIENT="nsec1..." export NOSTRGIT_SECRET_KEY="nsec1..."
``` ```
### 3. Configure git for localhost ### 3. Configure git for localhost
@ -131,12 +131,12 @@ git clone http://localhost:5173/api/git/npub1abc123.../my-repo.git
cd my-repo cd my-repo
# If you need to add the remote manually # If you need to add the remote manually
git remote add origin http://localhost:5173/api/git/npub1abc123.../my-repo.git git remote add gitrepublic-web http://localhost:5173/api/git/npub1abc123.../my-repo.git
# Make some changes and push # Make some changes and push
git add . git add .
git commit -m "Initial commit" git commit -m "Initial commit"
git push -u origin main git push -u gitrepublic-web main
``` ```
**Note:** The git HTTP backend endpoint is `/api/git/`, so the full URL format is: **Note:** The git HTTP backend endpoint is `/api/git/`, so the full URL format is:
@ -145,7 +145,7 @@ git push -u origin main
### Push changes ### Push changes
```bash ```bash
git push origin main git push gitrepublic-web main
``` ```
The credential helper will generate the appropriate NIP-98 auth token for push operations. The credential helper will generate the appropriate NIP-98 auth token for push operations.
@ -153,14 +153,14 @@ The credential helper will generate the appropriate NIP-98 auth token for push o
### Fetch/Pull ### Fetch/Pull
```bash ```bash
git fetch origin git fetch gitrepublic-web
git pull origin main git pull gitrepublic-web main
``` ```
## How It Works ## How It Works
1. When git needs credentials, it calls the credential helper with the repository URL 1. When git needs credentials, it calls the credential helper with the repository URL
2. The helper reads your `NOSTRGIT_SECRET_KEY_CLIENT` environment variable (with fallbacks for backward compatibility) 2. The helper reads your `NOSTRGIT_SECRET_KEY` environment variable (with fallbacks for backward compatibility)
3. It creates a NIP-98 authentication event signed with your private key 3. It creates a NIP-98 authentication event signed with your private key
4. The signed event is base64-encoded and returned as the "password" 4. The signed event is base64-encoded and returned as the "password"
5. Git sends this in the `Authorization: Nostr <base64-event>` header 5. Git sends this in the `Authorization: Nostr <base64-event>` header
@ -168,14 +168,14 @@ git pull origin main
## Troubleshooting ## Troubleshooting
### Error: NOSTRGIT_SECRET_KEY_CLIENT environment variable is not set ### Error: NOSTRGIT_SECRET_KEY environment variable is not set
Make sure you've exported the NOSTRGIT_SECRET_KEY_CLIENT variable: Make sure you've exported the NOSTRGIT_SECRET_KEY variable:
```bash ```bash
export NOSTRGIT_SECRET_KEY_CLIENT="nsec1..." export NOSTRGIT_SECRET_KEY="nsec1..."
``` ```
**Note:** The script also supports `NOSTR_PRIVATE_KEY` and `NSEC` for backward compatibility, but `NOSTRGIT_SECRET_KEY_CLIENT` is the preferred name. **Note:** The script also supports `NOSTR_PRIVATE_KEY` and `NSEC` for backward compatibility, but `NOSTRGIT_SECRET_KEY` is the preferred name.
### Error: Invalid nsec format ### Error: Invalid nsec format
@ -195,12 +195,12 @@ Push operations require POST authentication. The credential helper automatically
1. Verify you have maintainer permissions for the repository 1. Verify you have maintainer permissions for the repository
2. Check that branch protection rules allow your push 2. Check that branch protection rules allow your push
3. Ensure your NOSTRGIT_SECRET_KEY_CLIENT is correctly set 3. Ensure your NOSTRGIT_SECRET_KEY is correctly set
## Security Best Practices ## Security Best Practices
1. **Never commit your NOSTRGIT_SECRET_KEY_CLIENT to version control** 1. **Never commit your NOSTRGIT_SECRET_KEY to version control**
- Add `NOSTRGIT_SECRET_KEY_CLIENT` to your `.gitignore` if you store it in a file - Add `NOSTRGIT_SECRET_KEY` to your `.gitignore` if you store it in a file
- Use environment variables instead of hardcoding - Use environment variables instead of hardcoding
- **Important:** This is YOUR user key for client-side operations - **Important:** This is YOUR user key for client-side operations

1
scripts/Untitled

@ -0,0 +1 @@
git config --global credential.http://localhost.helper '!node /absolute/path/to/gitrepublic-web/scripts/git-credential-nostr.js'

12
scripts/git-credential-nostr.js

@ -13,9 +13,9 @@
* git config --global credential.https://your-domain.com.helper '!node /path/to/gitrepublic-web/scripts/git-credential-nostr.js' * git config --global credential.https://your-domain.com.helper '!node /path/to/gitrepublic-web/scripts/git-credential-nostr.js'
* *
* Environment variables: * Environment variables:
* NOSTRGIT_SECRET_KEY_CLIENT - Your Nostr private key (nsec format or hex) for client-side git operations * NOSTRGIT_SECRET_KEY - Your Nostr private key (nsec format or hex) for client-side git operations
* *
* Security: Keep your NOSTRGIT_SECRET_KEY_CLIENT secure and never commit it to version control! * Security: Keep your NOSTRGIT_SECRET_KEY secure and never commit it to version control!
*/ */
import { createHash } from 'crypto'; import { createHash } from 'crypto';
@ -124,11 +124,11 @@ async function main() {
// For 'get' command, generate credentials // For 'get' command, generate credentials
if (command === 'get') { if (command === 'get') {
// Get private key from environment variable // Get private key from environment variable
// Support NOSTRGIT_SECRET_KEY_CLIENT (preferred), with fallbacks for backward compatibility // Support NOSTRGIT_SECRET_KEY (preferred), with fallbacks for backward compatibility
const nsec = process.env.NOSTRGIT_SECRET_KEY_CLIENT || process.env.NOSTR_PRIVATE_KEY || process.env.NSEC; const nsec = process.env.NOSTRGIT_SECRET_KEY || process.env.NOSTR_PRIVATE_KEY || process.env.NSEC;
if (!nsec) { if (!nsec) {
console.error('Error: NOSTRGIT_SECRET_KEY_CLIENT environment variable is not set'); console.error('Error: NOSTRGIT_SECRET_KEY environment variable is not set');
console.error('Set it with: export NOSTRGIT_SECRET_KEY_CLIENT="nsec1..." or NOSTRGIT_SECRET_KEY_CLIENT="<hex-key>"'); console.error('Set it with: export NOSTRGIT_SECRET_KEY="nsec1..." or NOSTRGIT_SECRET_KEY="<hex-key>"');
process.exit(1); process.exit(1);
} }

4
src/lib/services/nostr/ownership-transfer-service.ts

@ -40,7 +40,7 @@ export class OwnershipTransferService {
*/ */
async getCurrentOwnerFromRepo(npub: string, repoId: string): Promise<string | null> { async getCurrentOwnerFromRepo(npub: string, repoId: string): Promise<string | null> {
try { try {
const { fileManager } = await import('../services/service-registry.js'); const { fileManager } = await import('../service-registry.js');
return await fileManager.getCurrentOwnerFromRepo(npub, repoId); return await fileManager.getCurrentOwnerFromRepo(npub, repoId);
} catch (error) { } catch (error) {
logger.error({ error, npub, repoId }, 'Error getting current owner from repo'); logger.error({ error, npub, repoId }, 'Error getting current owner from repo');
@ -81,7 +81,7 @@ export class OwnershipTransferService {
try { try {
const { nip19 } = await import('nostr-tools'); const { nip19 } = await import('nostr-tools');
const npub = nip19.npubEncode(announcementEvent.pubkey); const npub = nip19.npubEncode(announcementEvent.pubkey);
const { fileManager } = await import('../services/service-registry.js'); const { fileManager } = await import('../service-registry.js');
const localOwner = await fileManager.getCurrentOwnerFromRepo(npub, dTag); const localOwner = await fileManager.getCurrentOwnerFromRepo(npub, dTag);
const localUrl = cloneUrls.find(url => url.includes(npub) || url.includes(announcementEvent.pubkey)); const localUrl = cloneUrls.find(url => url.includes(npub) || url.includes(announcementEvent.pubkey));

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

@ -154,7 +154,12 @@
const cloneTooltip = 'Please clone this repo to use this feature.'; const cloneTooltip = 'Please clone this repo to use this feature.';
// Verification status // Verification status
let verificationStatus = $state<{ verified: boolean; error?: string; message?: string } | null>(null); let verificationStatus = $state<{
verified: boolean;
error?: string;
message?: string;
cloneVerifications?: Array<{ url: string; verified: boolean; ownerPubkey: string | null; error?: string }>;
} | null>(null);
let showVerificationDialog = $state(false); let showVerificationDialog = $state(false);
let verificationFileContent = $state<string | null>(null); let verificationFileContent = $state<string | null>(null);
let loadingVerification = $state(false); let loadingVerification = $state(false);
@ -1480,13 +1485,18 @@
}); });
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
console.log('[Verification] Response:', data);
verificationStatus = data; verificationStatus = data;
} else {
console.warn('[Verification] Response not OK:', response.status, response.statusText);
verificationStatus = { verified: false, error: `Verification check failed: ${response.status}` };
} }
} catch (err) { } catch (err) {
console.error('Failed to check verification:', err); console.error('[Verification] Failed to check verification:', err);
verificationStatus = { verified: false, error: 'Failed to check verification' }; verificationStatus = { verified: false, error: 'Failed to check verification' };
} finally { } finally {
loadingVerification = false; loadingVerification = false;
console.log('[Verification] Status after check:', verificationStatus);
} }
} }
@ -2774,7 +2784,54 @@
<div class="repo-clone-urls"> <div class="repo-clone-urls">
<span class="clone-label">Clone:</span> <span class="clone-label">Clone:</span>
{#each pageData.repoCloneUrls.slice(0, 3) as cloneUrl} {#each pageData.repoCloneUrls.slice(0, 3) as cloneUrl}
<code class="clone-url">{cloneUrl}</code> {@const cloneVerification = verificationStatus?.cloneVerifications?.find(cv => {
// Match URLs more flexibly (handle trailing slashes, http/https differences)
const normalizeUrl = (url: string) => url.replace(/\/$/, '').toLowerCase().replace(/^https?:\/\//, '');
const normalizedCv = normalizeUrl(cv.url);
const normalizedClone = normalizeUrl(cloneUrl);
const matches = normalizedCv === normalizedClone ||
normalizedCv.includes(normalizedClone) ||
normalizedClone.includes(normalizedCv);
if (matches) {
console.log('[Verification] Matched clone URL:', cloneUrl, 'with verification:', cv);
}
return matches;
})}
<div class="clone-url-wrapper">
<code class="clone-url">{cloneUrl}</code>
{#if loadingVerification}
<span class="verification-badge loading" title="Checking verification...">
<span style="opacity: 0.5;"></span>
</span>
{:else if cloneVerification !== undefined}
<span
class="verification-badge"
class:verified={cloneVerification.verified}
class:unverified={!cloneVerification.verified}
title={cloneVerification.verified ? 'Verified ownership' : (cloneVerification.error || 'Unverified')}
>
{#if cloneVerification.verified}
<img src="/icons/check-circle.svg" alt="Verified" class="icon-inline" />
{:else}
<img src="/icons/alert-triangle.svg" alt="Unverified" class="icon-inline" />
{/if}
</span>
{:else if verificationStatus}
{#if verificationStatus.cloneVerifications && verificationStatus.cloneVerifications.length > 0}
<span class="verification-badge unverified" title="Verification status unknown for this clone">
<img src="/icons/alert-triangle.svg" alt="Unknown" class="icon-inline" />
</span>
{:else}
<span class="verification-badge unverified" title="Verification not available for this clone">
<img src="/icons/alert-triangle.svg" alt="Not verified" class="icon-inline" />
</span>
{/if}
{:else}
<span class="verification-badge unverified" title="Verification not checked">
<img src="/icons/alert-triangle.svg" alt="Not checked" class="icon-inline" />
</span>
{/if}
</div>
{/each} {/each}
{#if pageData.repoCloneUrls.length > 3} {#if pageData.repoCloneUrls.length > 3}
<span class="clone-more">+{pageData.repoCloneUrls.length - 3} more</span> <span class="clone-more">+{pageData.repoCloneUrls.length - 3} more</span>
@ -2861,17 +2918,6 @@
{/if} {/if}
{/if} {/if}
</div> </div>
{#if verificationStatus}
<span class="verification-status" class:verified={verificationStatus.verified} class:unverified={!verificationStatus.verified}>
{#if verificationStatus.verified}
<img src="/icons/check-circle.svg" alt="Verified Repo Ownership" class="icon-inline" />
Verified Repo Ownership
{:else}
<img src="/icons/alert-triangle.svg" alt="Unverified" class="icon-inline" />
Unverified
{/if}
</span>
{/if}
</div> </div>
</div> </div>
</header> </header>
@ -4533,6 +4579,12 @@
font-weight: 500; font-weight: 500;
} }
.clone-url-wrapper {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.clone-url { .clone-url {
padding: 0.125rem 0.375rem; padding: 0.125rem 0.375rem;
background: var(--bg-secondary); background: var(--bg-secondary);
@ -5786,22 +5838,31 @@
color: var(--text-primary); color: var(--text-primary);
} }
.verification-status { .verification-badge {
padding: 0.25rem 0.5rem; display: inline-flex;
align-items: center;
padding: 0.125rem 0.25rem;
border-radius: 0.25rem; border-radius: 0.25rem;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 500; flex-shrink: 0;
margin-left: 0.5rem;
} }
.verification-status.verified { .verification-badge.loading {
background: var(--success-bg); opacity: 0.6;
color: var(--success-text);
} }
.verification-status.unverified { .verification-badge.verified {
background: var(--error-bg); color: var(--success-text, #10b981);
color: var(--error-text); }
.verification-badge.unverified {
color: var(--error-text, #f59e0b);
}
.verification-badge .icon-inline {
width: 1em;
height: 1em;
margin: 0;
} }
.icon-inline { .icon-inline {
@ -5824,12 +5885,12 @@
/* Theme-aware icon colors */ /* Theme-aware icon colors */
.verification-status.verified .icon-inline { .verification-badge.verified .icon-inline {
/* Green checkmark for verified */ /* Green checkmark for verified */
filter: brightness(0) saturate(100%) invert(48%) sepia(79%) saturate(2476%) hue-rotate(86deg) brightness(118%) contrast(119%); filter: brightness(0) saturate(100%) invert(48%) sepia(79%) saturate(2476%) hue-rotate(86deg) brightness(118%) contrast(119%);
} }
.verification-status.unverified .icon-inline { .verification-badge.unverified .icon-inline {
/* Orange/yellow warning for unverified */ /* Orange/yellow warning for unverified */
filter: brightness(0) saturate(100%) invert(67%) sepia(93%) saturate(1352%) hue-rotate(358deg) brightness(102%) contrast(106%); filter: brightness(0) saturate(100%) invert(67%) sepia(93%) saturate(1352%) hue-rotate(358deg) brightness(102%) contrast(106%);
} }

42
vite.config.ts

@ -46,14 +46,52 @@ export default defineConfig({
ssr: { ssr: {
// Exclude Node.js-only modules from client bundle // Exclude Node.js-only modules from client bundle
noExternal: [], noExternal: [],
external: [] external: [
'simple-git',
'child_process',
'fs',
'fs/promises',
'path',
'os',
'crypto',
'stream',
'util',
'events',
'buffer'
]
}, },
optimizeDeps: { optimizeDeps: {
// Exclude server-only modules from pre-bundling // Exclude server-only modules from pre-bundling
exclude: ['src/lib/services/messaging/preferences-storage.ts'] exclude: [
'src/lib/services/messaging/preferences-storage.ts',
'simple-git'
]
}, },
build: { build: {
rollupOptions: { rollupOptions: {
external: (id) => {
// Externalize Node.js-only modules to prevent bundling for browser
if (id === 'simple-git' || id.startsWith('simple-git/')) {
return true;
}
// Externalize Node.js built-in modules
if (
id === 'child_process' ||
id === 'fs' ||
id === 'fs/promises' ||
id === 'path' ||
id === 'os' ||
id === 'crypto' ||
id === 'stream' ||
id === 'util' ||
id === 'events' ||
id === 'buffer' ||
id.startsWith('node:')
) {
return true;
}
return false;
},
onwarn(warning, warn) { onwarn(warning, warn) {
// Suppress warnings about externalized modules (expected for SSR builds) // Suppress warnings about externalized modules (expected for SSR builds)
if ( if (

Loading…
Cancel
Save