Browse Source

improved codebase

main
Silberengel 4 weeks ago
parent
commit
cdd71a9c04
  1. 25
      src/lib/components/PRDetail.svelte
  2. 8
      src/lib/services/git/file-manager.ts
  3. 1
      src/lib/types/nostr.ts
  4. 53
      src/lib/utils/npub-utils.ts
  5. 24
      src/routes/api/git/[...path]/+server.ts
  6. 29
      src/routes/api/repos/[npub]/[repo]/branch-protection/+server.ts
  7. 19
      src/routes/api/repos/[npub]/[repo]/branches/+server.ts
  8. 8
      src/routes/api/repos/[npub]/[repo]/download/+server.ts
  9. 29
      src/routes/api/repos/[npub]/[repo]/file/+server.ts
  10. 27
      src/routes/api/repos/[npub]/[repo]/fork/+server.ts
  11. 18
      src/routes/api/repos/[npub]/[repo]/highlights/+server.ts
  12. 7
      src/routes/api/repos/[npub]/[repo]/issues/+server.ts
  13. 20
      src/routes/api/repos/[npub]/[repo]/maintainers/+server.ts
  14. 8
      src/routes/api/repos/[npub]/[repo]/prs/+server.ts
  15. 8
      src/routes/api/repos/[npub]/[repo]/raw/+server.ts
  16. 8
      src/routes/api/repos/[npub]/[repo]/readme/+server.ts
  17. 35
      src/routes/api/repos/[npub]/[repo]/settings/+server.ts
  18. 19
      src/routes/api/repos/[npub]/[repo]/tags/+server.ts
  19. 15
      src/routes/api/repos/[npub]/[repo]/transfer/+server.ts
  20. 8
      src/routes/api/repos/[npub]/[repo]/tree/+server.ts
  21. 12
      src/routes/api/repos/[npub]/[repo]/verify/+server.ts
  22. 58
      src/routes/api/search/+server.ts
  23. 5
      src/routes/docs/+page.svelte
  24. 5
      src/routes/docs/nip34/+page.svelte
  25. 5
      src/routes/docs/nip34/spec/+page.svelte
  26. 9
      src/routes/repos/[npub]/[repo]/+page.svelte
  27. 16
      src/routes/search/+page.svelte
  28. 82
      src/routes/signup/+page.svelte

25
src/lib/components/PRDetail.svelte

@ -27,8 +27,27 @@
let { pr, npub, repo, repoOwnerPubkey }: Props = $props(); let { pr, npub, repo, repoOwnerPubkey }: Props = $props();
let highlights = $state<Array<any>>([]); let highlights = $state<Array<{
let comments = $state<Array<any>>([]); id: string;
content: string;
pubkey: string;
created_at: number;
comments?: Array<{
id: string;
content: string;
pubkey: string;
created_at: number;
[key: string]: unknown;
}>;
[key: string]: unknown;
}>>([]);
let comments = $state<Array<{
id: string;
content: string;
pubkey: string;
created_at: number;
[key: string]: unknown;
}>>([]);
let loading = $state(false); let loading = $state(false);
let error = $state<string | null>(null); let error = $state<string | null>(null);
let userPubkey = $state<string | null>(null); let userPubkey = $state<string | null>(null);
@ -105,7 +124,7 @@
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
// Combine all file diffs // Combine all file diffs
prDiff = data.map((d: any) => prDiff = data.map((d: { file: string; diff: string }) =>
`--- ${d.file}\n+++ ${d.file}\n${d.diff}` `--- ${d.file}\n+++ ${d.file}\n${d.diff}`
).join('\n\n'); ).join('\n\n');
} }

8
src/lib/services/git/file-manager.ts

@ -671,7 +671,11 @@ export class FileManager {
const git: SimpleGit = simpleGit(repoPath); const git: SimpleGit = simpleGit(repoPath);
try { try {
const logOptions: any = { const logOptions: {
maxCount: number;
from: string;
file?: string;
} = {
maxCount: limit, maxCount: limit,
from: branch from: branch
}; };
@ -687,7 +691,7 @@ export class FileManager {
message: commit.message, message: commit.message,
author: `${commit.author_name} <${commit.author_email}>`, author: `${commit.author_name} <${commit.author_email}>`,
date: commit.date, date: commit.date,
files: commit.diff?.files?.map((f: any) => f.file) || [] files: commit.diff?.files?.map((f: { file: string }) => f.file) || []
})); }));
} catch (error) { } catch (error) {
logger.error({ error, repoPath, branch, limit }, 'Error getting commit history'); logger.error({ error, repoPath, branch, limit }, 'Error getting commit history');

1
src/lib/types/nostr.ts

@ -23,6 +23,7 @@ export interface NostrFilter {
since?: number; since?: number;
until?: number; until?: number;
limit?: number; limit?: number;
search?: string; // NIP-50: Search capability
} }
export const KIND = { export const KIND = {

53
src/lib/utils/npub-utils.ts

@ -0,0 +1,53 @@
/**
* Utility functions for working with npub (Nostr public key) encoding/decoding
*/
import { nip19 } from 'nostr-tools';
/**
* Decode an npub to a hex pubkey
* @param npub - The npub string to decode
* @returns The hex pubkey, or null if invalid
*/
export function decodeNpubToHex(npub: string): string | null {
try {
const decoded = nip19.decode(npub);
if (decoded.type === 'npub') {
return decoded.data as string;
}
return null;
} catch {
return null;
}
}
/**
* Decode an npub and return both the type and data
* @param npub - The npub string to decode
* @returns Object with type and hex pubkey, or null if invalid
*/
export function decodeNpub(npub: string): { type: 'npub'; data: string } | null {
try {
const decoded = nip19.decode(npub);
if (decoded.type === 'npub') {
return { type: 'npub', data: decoded.data as string };
}
return null;
} catch {
return null;
}
}
/**
* Validate and decode npub, throwing an error if invalid
* @param npub - The npub string to decode
* @returns The hex pubkey
* @throws Error if npub is invalid
*/
export function requireNpubHex(npub: string): string {
const decoded = decodeNpub(npub);
if (!decoded) {
throw new Error('Invalid npub format');
}
return decoded.data;
}

24
src/routes/api/git/[...path]/+server.ts

@ -7,6 +7,7 @@ import { error } from '@sveltejs/kit';
import type { RequestHandler } from './$types'; import type { RequestHandler } from './$types';
import { RepoManager } from '$lib/services/git/repo-manager.js'; import { RepoManager } from '$lib/services/git/repo-manager.js';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { requireNpubHex } from '$lib/utils/npub-utils.js';
import { spawn, execSync } from 'child_process'; import { spawn, execSync } from 'child_process';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { join, resolve } from 'path'; import { join, resolve } from 'path';
@ -69,11 +70,7 @@ function findGitHttpBackend(): string | null {
*/ */
async function getRepoAnnouncement(npub: string, repoName: string): Promise<NostrEvent | null> { async function getRepoAnnouncement(npub: string, repoName: string): Promise<NostrEvent | null> {
try { try {
const decoded = nip19.decode(npub); const pubkey = requireNpubHex(npub);
if (decoded.type !== 'npub') {
return null;
}
const pubkey = decoded.data as string;
const events = await nostrClient.fetchEvents([ const events = await nostrClient.fetchEvents([
{ {
@ -127,10 +124,7 @@ export const GET: RequestHandler = async ({ params, url, request }) => {
// Validate npub format // Validate npub format
try { try {
const decoded = nip19.decode(npub); pubkey = requireNpubHex(npub);
if (decoded.type !== 'npub') {
return error(400, 'Invalid npub format');
}
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }
@ -150,11 +144,7 @@ export const GET: RequestHandler = async ({ params, url, request }) => {
// Check repository privacy for clone/fetch operations // Check repository privacy for clone/fetch operations
let originalOwnerPubkey: string; let originalOwnerPubkey: string;
try { try {
const decoded = nip19.decode(npub); originalOwnerPubkey = requireNpubHex(npub);
if (decoded.type !== 'npub') {
return error(400, 'Invalid npub format');
}
originalOwnerPubkey = decoded.data as string;
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }
@ -351,11 +341,7 @@ export const POST: RequestHandler = async ({ params, url, request }) => {
// Validate npub format and decode to get pubkey // Validate npub format and decode to get pubkey
let originalOwnerPubkey: string; let originalOwnerPubkey: string;
try { try {
const decoded = nip19.decode(npub); originalOwnerPubkey = requireNpubHex(npub);
if (decoded.type !== 'npub') {
return error(400, 'Invalid npub format');
}
originalOwnerPubkey = decoded.data as string;
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }

29
src/routes/api/repos/[npub]/[repo]/branch-protection/+server.ts

@ -13,6 +13,7 @@ import { NostrClient } from '$lib/services/nostr/nostr-client.js';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import type { BranchProtectionRule } from '$lib/services/nostr/branch-protection-service.js'; import type { BranchProtectionRule } from '$lib/services/nostr/branch-protection-service.js';
import logger from '$lib/services/logger.js'; import logger from '$lib/services/logger.js';
import { requireNpubHex, decodeNpubToHex } from '$lib/utils/npub-utils.js';
const branchProtectionService = new BranchProtectionService(DEFAULT_NOSTR_RELAYS); const branchProtectionService = new BranchProtectionService(DEFAULT_NOSTR_RELAYS);
const ownershipTransferService = new OwnershipTransferService(DEFAULT_NOSTR_RELAYS); const ownershipTransferService = new OwnershipTransferService(DEFAULT_NOSTR_RELAYS);
@ -32,12 +33,7 @@ export const GET: RequestHandler = async ({ params }: { params: { npub?: string;
// Decode npub to get pubkey // Decode npub to get pubkey
let ownerPubkey: string; let ownerPubkey: string;
try { try {
const decoded = nip19.decode(npub); ownerPubkey = requireNpubHex(npub);
if (decoded.type === 'npub' && typeof decoded.data === 'string') {
ownerPubkey = decoded.data;
} else {
return error(400, 'Invalid npub format');
}
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }
@ -82,27 +78,12 @@ export const POST: RequestHandler = async ({ params, request }: { params: { npub
// Decode npub to get pubkey // Decode npub to get pubkey
let ownerPubkey: string; let ownerPubkey: string;
try { try {
const decoded = nip19.decode(npub); ownerPubkey = requireNpubHex(npub);
if (decoded.type === 'npub' && typeof decoded.data === 'string') {
ownerPubkey = decoded.data;
} else {
return error(400, 'Invalid npub format');
}
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }
let userPubkeyHex: string = userPubkey; const userPubkeyHex = decodeNpubToHex(userPubkey) || userPubkey;
try {
const userDecoded = nip19.decode(userPubkey) as { type: string; data: unknown };
// Type guard: check if it's an npub
if (userDecoded.type === 'npub' && typeof userDecoded.data === 'string') {
userPubkeyHex = userDecoded.data;
}
// If not npub, assume it's already hex
} catch {
// Assume it's already hex
}
// Check if user is owner // Check if user is owner
const currentOwner = await ownershipTransferService.getCurrentOwner(ownerPubkey, repo); const currentOwner = await ownershipTransferService.getCurrentOwner(ownerPubkey, repo);
@ -111,7 +92,7 @@ export const POST: RequestHandler = async ({ params, request }: { params: { npub
} }
// Validate rules // Validate rules
const validatedRules: BranchProtectionRule[] = rules.map((rule: any) => ({ const validatedRules: BranchProtectionRule[] = rules.map((rule: { branch: string; requirePullRequest?: boolean; requireReviewers?: string[]; allowForcePush?: boolean; requireStatusChecks?: string[] }) => ({
branch: rule.branch, branch: rule.branch,
requirePullRequest: rule.requirePullRequest || false, requirePullRequest: rule.requirePullRequest || false,
requireReviewers: rule.requireReviewers || [], requireReviewers: rule.requireReviewers || [],

19
src/routes/api/repos/[npub]/[repo]/branches/+server.ts

@ -9,6 +9,7 @@ import { FileManager } from '$lib/services/git/file-manager.js';
import { MaintainerService } from '$lib/services/nostr/maintainer-service.js'; import { MaintainerService } from '$lib/services/nostr/maintainer-service.js';
import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { requireNpubHex, decodeNpubToHex } from '$lib/utils/npub-utils.js';
import logger from '$lib/services/logger.js'; import logger from '$lib/services/logger.js';
const repoRoot = process.env.GIT_REPO_ROOT || '/repos'; const repoRoot = process.env.GIT_REPO_ROOT || '/repos';
@ -64,27 +65,13 @@ export const POST: RequestHandler = async ({ params, request }: { params: { npub
// Check if user is a maintainer // Check if user is a maintainer
let repoOwnerPubkey: string; let repoOwnerPubkey: string;
try { try {
const decoded = nip19.decode(npub); repoOwnerPubkey = requireNpubHex(npub);
if (decoded.type === 'npub') {
repoOwnerPubkey = decoded.data as string;
} else {
return error(400, 'Invalid npub format');
}
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }
// Convert userPubkey to hex if needed // Convert userPubkey to hex if needed
let userPubkeyHex = userPubkey; const userPubkeyHex = decodeNpubToHex(userPubkey) || userPubkey;
try {
const userDecoded = nip19.decode(userPubkey);
// @ts-ignore - nip19 types are incomplete, but we know npub returns string
if (userDecoded.type === 'npub') {
userPubkeyHex = userDecoded.data as unknown as string;
}
} catch {
// Assume it's already a hex pubkey
}
const isMaintainer = await maintainerService.isMaintainer(userPubkeyHex, repoOwnerPubkey, repo); const isMaintainer = await maintainerService.isMaintainer(userPubkeyHex, repoOwnerPubkey, repo);
if (!isMaintainer) { if (!isMaintainer) {

8
src/routes/api/repos/[npub]/[repo]/download/+server.ts

@ -8,6 +8,7 @@ import { FileManager } from '$lib/services/git/file-manager.js';
import { MaintainerService } from '$lib/services/nostr/maintainer-service.js'; import { MaintainerService } from '$lib/services/nostr/maintainer-service.js';
import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { requireNpubHex } from '$lib/utils/npub-utils.js';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { promisify } from 'util'; import { promisify } from 'util';
import { existsSync, readFileSync } from 'fs'; import { existsSync, readFileSync } from 'fs';
@ -37,12 +38,7 @@ export const GET: RequestHandler = async ({ params, url, request }) => {
// Check repository privacy // Check repository privacy
let repoOwnerPubkey: string; let repoOwnerPubkey: string;
try { try {
const decoded = nip19.decode(npub); repoOwnerPubkey = requireNpubHex(npub);
if (decoded.type === 'npub') {
repoOwnerPubkey = decoded.data as string;
} else {
return error(400, 'Invalid npub format');
}
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }

29
src/routes/api/repos/[npub]/[repo]/file/+server.ts

@ -12,6 +12,8 @@ import { nip19 } from 'nostr-tools';
import { verifyNIP98Auth } from '$lib/services/nostr/nip98-auth.js'; import { verifyNIP98Auth } from '$lib/services/nostr/nip98-auth.js';
import { auditLogger } from '$lib/services/security/audit-logger.js'; import { auditLogger } from '$lib/services/security/audit-logger.js';
import logger from '$lib/services/logger.js'; import logger from '$lib/services/logger.js';
import type { NostrEvent } from '$lib/types/nostr.js';
import { requireNpubHex, decodeNpubToHex } from '$lib/utils/npub-utils.js';
const repoRoot = process.env.GIT_REPO_ROOT || '/repos'; const repoRoot = process.env.GIT_REPO_ROOT || '/repos';
const fileManager = new FileManager(repoRoot); const fileManager = new FileManager(repoRoot);
@ -35,12 +37,7 @@ export const GET: RequestHandler = async ({ params, url, request }: { params: {
// Check repository privacy // Check repository privacy
let repoOwnerPubkey: string; let repoOwnerPubkey: string;
try { try {
const decoded = nip19.decode(npub); repoOwnerPubkey = requireNpubHex(npub);
if (decoded.type === 'npub') {
repoOwnerPubkey = decoded.data as string;
} else {
return error(400, 'Invalid npub format');
}
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }
@ -131,27 +128,13 @@ export const POST: RequestHandler = async ({ params, url, request }: { params: {
// Check if user is a maintainer // Check if user is a maintainer
let repoOwnerPubkey: string; let repoOwnerPubkey: string;
try { try {
const decoded = nip19.decode(npub); repoOwnerPubkey = requireNpubHex(npub);
if (decoded.type === 'npub') {
repoOwnerPubkey = decoded.data as string;
} else {
return error(400, 'Invalid npub format');
}
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }
// Convert userPubkey to hex if needed // Convert userPubkey to hex if needed
let userPubkeyHex = userPubkey; const userPubkeyHex = decodeNpubToHex(userPubkey) || userPubkey;
try {
const userDecoded = nip19.decode(userPubkey);
// @ts-ignore - nip19 types are incomplete, but we know npub returns string
if (userDecoded.type === 'npub') {
userPubkeyHex = userDecoded.data as unknown as string;
}
} catch {
// Assume it's already a hex pubkey
}
const isMaintainer = await maintainerService.isMaintainer(userPubkeyHex, repoOwnerPubkey, repo); const isMaintainer = await maintainerService.isMaintainer(userPubkeyHex, repoOwnerPubkey, repo);
if (!isMaintainer) { if (!isMaintainer) {
@ -164,7 +147,7 @@ export const POST: RequestHandler = async ({ params, url, request }: { params: {
// nsecKey is only for server-side use via environment variables. // nsecKey is only for server-side use via environment variables.
const signingOptions: { const signingOptions: {
useNIP07?: boolean; useNIP07?: boolean;
nip98Event?: any; nip98Event?: NostrEvent;
nsecKey?: string; nsecKey?: string;
} = {}; } = {};

27
src/routes/api/repos/[npub]/[repo]/fork/+server.ts

@ -11,6 +11,7 @@ import { NostrClient } from '$lib/services/nostr/nostr-client.js';
import { KIND, type NostrEvent } 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 { requireNpubHex, decodeNpubToHex } from '$lib/utils/npub-utils.js';
import { OwnershipTransferService } from '$lib/services/nostr/ownership-transfer-service.js'; import { OwnershipTransferService } from '$lib/services/nostr/ownership-transfer-service.js';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { promisify } from 'util'; import { promisify } from 'util';
@ -92,28 +93,13 @@ export const POST: RequestHandler = async ({ params, request }) => {
// Decode original repo owner npub // Decode original repo owner npub
let originalOwnerPubkey: string; let originalOwnerPubkey: string;
try { try {
const decoded = nip19.decode(npub); originalOwnerPubkey = requireNpubHex(npub);
if (decoded.type === 'npub') {
originalOwnerPubkey = decoded.data as string;
} else {
return error(400, 'Invalid npub format');
}
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }
// Decode user pubkey if needed (must be done before using it) // Decode user pubkey if needed (must be done before using it)
let userPubkeyHex = userPubkey; const userPubkeyHex = decodeNpubToHex(userPubkey) || userPubkey;
try {
const userDecoded = nip19.decode(userPubkey) as { type: string; data: unknown };
// Type guard: check if it's an npub
if (userDecoded.type === 'npub' && typeof userDecoded.data === 'string') {
userPubkeyHex = userDecoded.data;
}
// If not npub, assume it's already hex
} catch {
// Assume it's already hex
}
// Convert to npub for resource check and path construction // Convert to npub for resource check and path construction
const userNpub = nip19.npubEncode(userPubkeyHex); const userNpub = nip19.npubEncode(userPubkeyHex);
@ -394,12 +380,7 @@ export const GET: RequestHandler = async ({ params }) => {
// Decode repo owner npub // Decode repo owner npub
let ownerPubkey: string; let ownerPubkey: string;
try { try {
const decoded = nip19.decode(npub); ownerPubkey = requireNpubHex(npub);
if (decoded.type === 'npub') {
ownerPubkey = decoded.data as string;
} else {
return error(400, 'Invalid npub format');
}
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }

18
src/routes/api/repos/[npub]/[repo]/highlights/+server.ts

@ -7,6 +7,7 @@ import type { RequestHandler } from './$types';
import { HighlightsService } from '$lib/services/nostr/highlights-service.js'; import { HighlightsService } from '$lib/services/nostr/highlights-service.js';
import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { requireNpubHex, decodeNpubToHex } from '$lib/utils/npub-utils.js';
import { verifyEvent } from 'nostr-tools'; import { verifyEvent } from 'nostr-tools';
import type { NostrEvent } from '$lib/types/nostr.js'; import type { NostrEvent } from '$lib/types/nostr.js';
import { combineRelays } from '$lib/config.js'; import { combineRelays } from '$lib/config.js';
@ -38,26 +39,13 @@ export const GET: RequestHandler = async ({ params, url }) => {
// Decode npub to get pubkey // Decode npub to get pubkey
let repoOwnerPubkey: string; let repoOwnerPubkey: string;
try { try {
const decoded = nip19.decode(npub); repoOwnerPubkey = requireNpubHex(npub);
if (decoded.type === 'npub') {
repoOwnerPubkey = decoded.data as string;
} else {
return error(400, 'Invalid npub format');
}
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }
// Decode prAuthor if it's an npub // Decode prAuthor if it's an npub
let prAuthorPubkey = prAuthor; const prAuthorPubkey = decodeNpubToHex(prAuthor) || prAuthor;
try {
const decoded = nip19.decode(prAuthor);
if (decoded.type === 'npub') {
prAuthorPubkey = decoded.data as string;
}
} catch {
// Assume it's already hex
}
// Get highlights for the PR // Get highlights for the PR
const highlights = await highlightsService.getHighlightsForPR( const highlights = await highlightsService.getHighlightsForPR(

7
src/routes/api/repos/[npub]/[repo]/issues/+server.ts

@ -19,11 +19,12 @@ export const GET: RequestHandler = async ({ params, url, request }) => {
try { try {
// Convert npub to pubkey // Convert npub to pubkey
const decoded = nip19.decode(npub); let repoOwnerPubkey: string;
if (decoded.type !== 'npub') { try {
repoOwnerPubkey = requireNpubHex(npub);
} catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }
const repoOwnerPubkey = decoded.data as string;
// Check repository privacy // Check repository privacy
const { checkRepoAccess } = await import('$lib/utils/repo-privacy.js'); const { checkRepoAccess } = await import('$lib/utils/repo-privacy.js');

20
src/routes/api/repos/[npub]/[repo]/maintainers/+server.ts

@ -8,6 +8,7 @@ import type { RequestHandler } from './$types';
import { MaintainerService } from '$lib/services/nostr/maintainer-service.js'; import { MaintainerService } from '$lib/services/nostr/maintainer-service.js';
import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { requireNpubHex, decodeNpubToHex } from '$lib/utils/npub-utils.js';
import logger from '$lib/services/logger.js'; import logger from '$lib/services/logger.js';
const maintainerService = new MaintainerService(DEFAULT_NOSTR_RELAYS); const maintainerService = new MaintainerService(DEFAULT_NOSTR_RELAYS);
@ -24,12 +25,7 @@ export const GET: RequestHandler = async ({ params, url }: { params: { npub?: st
// Convert npub to pubkey // Convert npub to pubkey
let repoOwnerPubkey: string; let repoOwnerPubkey: string;
try { try {
const decoded = nip19.decode(npub); repoOwnerPubkey = requireNpubHex(npub);
if (decoded.type === 'npub') {
repoOwnerPubkey = decoded.data as string;
} else {
return error(400, 'Invalid npub format');
}
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }
@ -38,17 +34,7 @@ export const GET: RequestHandler = async ({ params, url }: { params: { npub?: st
// If userPubkey provided, check if they're a maintainer // If userPubkey provided, check if they're a maintainer
if (userPubkey) { if (userPubkey) {
let userPubkeyHex = userPubkey; const userPubkeyHex = decodeNpubToHex(userPubkey) || userPubkey;
try {
// Try to decode if it's an npub
const userDecoded = nip19.decode(userPubkey);
// @ts-ignore - nip19 types are incomplete, but we know npub returns string
if (userDecoded.type === 'npub') {
userPubkeyHex = userDecoded.data as unknown as string;
}
} catch {
// Assume it's already a hex pubkey
}
const isMaintainer = maintainers.includes(userPubkeyHex); const isMaintainer = maintainers.includes(userPubkeyHex);
return json({ return json({

8
src/routes/api/repos/[npub]/[repo]/prs/+server.ts

@ -8,6 +8,7 @@ import type { RequestHandler } from './$types';
import { PRsService } from '$lib/services/nostr/prs-service.js'; import { PRsService } from '$lib/services/nostr/prs-service.js';
import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { requireNpubHex } from '$lib/utils/npub-utils.js';
import logger from '$lib/services/logger.js'; import logger from '$lib/services/logger.js';
export const GET: RequestHandler = async ({ params, url, request }: { params: { npub?: string; repo?: string }; url: URL; request: Request }) => { export const GET: RequestHandler = async ({ params, url, request }: { params: { npub?: string; repo?: string }; url: URL; request: Request }) => {
@ -22,12 +23,7 @@ export const GET: RequestHandler = async ({ params, url, request }: { params: {
// Convert npub to pubkey // Convert npub to pubkey
let repoOwnerPubkey: string; let repoOwnerPubkey: string;
try { try {
const decoded = nip19.decode(npub); repoOwnerPubkey = requireNpubHex(npub);
if (decoded.type === 'npub') {
repoOwnerPubkey = decoded.data as string;
} else {
return error(400, 'Invalid npub format');
}
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }

8
src/routes/api/repos/[npub]/[repo]/raw/+server.ts

@ -8,6 +8,7 @@ import { FileManager } from '$lib/services/git/file-manager.js';
import { MaintainerService } from '$lib/services/nostr/maintainer-service.js'; import { MaintainerService } from '$lib/services/nostr/maintainer-service.js';
import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { requireNpubHex } from '$lib/utils/npub-utils.js';
import logger from '$lib/services/logger.js'; import logger from '$lib/services/logger.js';
const repoRoot = process.env.GIT_REPO_ROOT || '/repos'; const repoRoot = process.env.GIT_REPO_ROOT || '/repos';
@ -32,12 +33,7 @@ export const GET: RequestHandler = async ({ params, url, request }) => {
// Check repository privacy // Check repository privacy
let repoOwnerPubkey: string; let repoOwnerPubkey: string;
try { try {
const decoded = nip19.decode(npub); repoOwnerPubkey = requireNpubHex(npub);
if (decoded.type === 'npub') {
repoOwnerPubkey = decoded.data as string;
} else {
return error(400, 'Invalid npub format');
}
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }

8
src/routes/api/repos/[npub]/[repo]/readme/+server.ts

@ -8,6 +8,7 @@ import { FileManager } from '$lib/services/git/file-manager.js';
import { MaintainerService } from '$lib/services/nostr/maintainer-service.js'; import { MaintainerService } from '$lib/services/nostr/maintainer-service.js';
import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { requireNpubHex } from '$lib/utils/npub-utils.js';
import logger from '$lib/services/logger.js'; import logger from '$lib/services/logger.js';
const repoRoot = process.env.GIT_REPO_ROOT || '/repos'; const repoRoot = process.env.GIT_REPO_ROOT || '/repos';
@ -42,12 +43,7 @@ export const GET: RequestHandler = async ({ params, url, request }) => {
// Check repository privacy // Check repository privacy
let repoOwnerPubkey: string; let repoOwnerPubkey: string;
try { try {
const decoded = nip19.decode(npub); repoOwnerPubkey = requireNpubHex(npub);
if (decoded.type === 'npub') {
repoOwnerPubkey = decoded.data as string;
} else {
return error(400, 'Invalid npub format');
}
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }

35
src/routes/api/repos/[npub]/[repo]/settings/+server.ts

@ -9,6 +9,7 @@ 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 { KIND } from '$lib/types/nostr.js'; import { KIND } from '$lib/types/nostr.js';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { requireNpubHex, decodeNpubToHex } from '$lib/utils/npub-utils.js';
import { signEventWithNIP07 } from '$lib/services/nostr/nip07-signer.js'; import { signEventWithNIP07 } from '$lib/services/nostr/nip07-signer.js';
import { MaintainerService } from '$lib/services/nostr/maintainer-service.js'; import { MaintainerService } from '$lib/services/nostr/maintainer-service.js';
import logger from '$lib/services/logger.js'; import logger from '$lib/services/logger.js';
@ -33,12 +34,7 @@ export const GET: RequestHandler = async ({ params, url, request }) => {
// Decode npub to get pubkey // Decode npub to get pubkey
let repoOwnerPubkey: string; let repoOwnerPubkey: string;
try { try {
const decoded = nip19.decode(npub) as { type: string; data: unknown }; repoOwnerPubkey = requireNpubHex(npub);
if (decoded.type === 'npub' && typeof decoded.data === 'string') {
repoOwnerPubkey = decoded.data;
} else {
return error(400, 'Invalid npub format');
}
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }
@ -48,15 +44,7 @@ export const GET: RequestHandler = async ({ params, url, request }) => {
return error(401, 'Authentication required'); return error(401, 'Authentication required');
} }
let userPubkeyHex = userPubkey; const userPubkeyHex = decodeNpubToHex(userPubkey) || userPubkey;
try {
const userDecoded = nip19.decode(userPubkey) as { type: string; data: unknown };
if (userDecoded.type === 'npub' && typeof userDecoded.data === 'string') {
userPubkeyHex = userDecoded.data;
}
} catch {
// Assume it's already hex
}
const currentOwner = await ownershipTransferService.getCurrentOwner(repoOwnerPubkey, repo); const currentOwner = await ownershipTransferService.getCurrentOwner(repoOwnerPubkey, repo);
if (userPubkeyHex !== currentOwner) { if (userPubkeyHex !== currentOwner) {
@ -127,25 +115,12 @@ export const POST: RequestHandler = async ({ params, request }) => {
// Decode npub to get pubkey // Decode npub to get pubkey
let repoOwnerPubkey: string; let repoOwnerPubkey: string;
try { try {
const decoded = nip19.decode(npub) as { type: string; data: unknown }; repoOwnerPubkey = requireNpubHex(npub);
if (decoded.type === 'npub' && typeof decoded.data === 'string') {
repoOwnerPubkey = decoded.data;
} else {
return error(400, 'Invalid npub format');
}
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }
let userPubkeyHex = userPubkey; const userPubkeyHex = decodeNpubToHex(userPubkey) || userPubkey;
try {
const userDecoded = nip19.decode(userPubkey) as { type: string; data: unknown };
if (userDecoded.type === 'npub' && typeof userDecoded.data === 'string') {
userPubkeyHex = userDecoded.data;
}
} catch {
// Assume it's already hex
}
// Check if user is owner // Check if user is owner
const currentOwner = await ownershipTransferService.getCurrentOwner(repoOwnerPubkey, repo); const currentOwner = await ownershipTransferService.getCurrentOwner(repoOwnerPubkey, repo);

19
src/routes/api/repos/[npub]/[repo]/tags/+server.ts

@ -9,6 +9,7 @@ import { FileManager } from '$lib/services/git/file-manager.js';
import { MaintainerService } from '$lib/services/nostr/maintainer-service.js'; import { MaintainerService } from '$lib/services/nostr/maintainer-service.js';
import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { requireNpubHex, decodeNpubToHex } from '$lib/utils/npub-utils.js';
import logger from '$lib/services/logger.js'; import logger from '$lib/services/logger.js';
const repoRoot = process.env.GIT_REPO_ROOT || '/repos'; const repoRoot = process.env.GIT_REPO_ROOT || '/repos';
@ -73,27 +74,13 @@ export const POST: RequestHandler = async ({ params, request }: { params: { npub
// Check if user is a maintainer // Check if user is a maintainer
let repoOwnerPubkey: string; let repoOwnerPubkey: string;
try { try {
const decoded = nip19.decode(npub); repoOwnerPubkey = requireNpubHex(npub);
if (decoded.type === 'npub') {
repoOwnerPubkey = decoded.data as string;
} else {
return error(400, 'Invalid npub format');
}
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }
// Convert userPubkey to hex if needed // Convert userPubkey to hex if needed
let userPubkeyHex = userPubkey; const userPubkeyHex = decodeNpubToHex(userPubkey) || userPubkey;
try {
const userDecoded = nip19.decode(userPubkey);
// @ts-ignore - nip19 types are incomplete, but we know npub returns string
if (userDecoded.type === 'npub') {
userPubkeyHex = userDecoded.data as unknown as string;
}
} catch {
// Assume it's already a hex pubkey
}
const isMaintainer = await maintainerService.isMaintainer(userPubkeyHex, repoOwnerPubkey, repo); const isMaintainer = await maintainerService.isMaintainer(userPubkeyHex, repoOwnerPubkey, repo);
if (!isMaintainer) { if (!isMaintainer) {

15
src/routes/api/repos/[npub]/[repo]/transfer/+server.ts

@ -9,6 +9,7 @@ import { NostrClient } from '$lib/services/nostr/nostr-client.js';
import { DEFAULT_NOSTR_RELAYS, combineRelays } from '$lib/config.js'; import { DEFAULT_NOSTR_RELAYS, combineRelays } from '$lib/config.js';
import { KIND } from '$lib/types/nostr.js'; import { KIND } from '$lib/types/nostr.js';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { requireNpubHex, decodeNpubToHex } from '$lib/utils/npub-utils.js';
import { verifyEvent } from 'nostr-tools'; import { verifyEvent } from 'nostr-tools';
import type { NostrEvent } from '$lib/types/nostr.js'; import type { NostrEvent } from '$lib/types/nostr.js';
import { getUserRelays } from '$lib/services/nostr/user-relays.js'; import { getUserRelays } from '$lib/services/nostr/user-relays.js';
@ -31,12 +32,7 @@ export const GET: RequestHandler = async ({ params }) => {
// Decode npub to get pubkey // Decode npub to get pubkey
let originalOwnerPubkey: string; let originalOwnerPubkey: string;
try { try {
const decoded = nip19.decode(npub); originalOwnerPubkey = requireNpubHex(npub);
if (decoded.type === 'npub') {
originalOwnerPubkey = decoded.data as string;
} else {
return error(400, 'Invalid npub format');
}
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }
@ -109,12 +105,7 @@ export const POST: RequestHandler = async ({ params, request }) => {
// Decode npub to get original owner pubkey // Decode npub to get original owner pubkey
let originalOwnerPubkey: string; let originalOwnerPubkey: string;
try { try {
const decoded = nip19.decode(npub); originalOwnerPubkey = requireNpubHex(npub);
if (decoded.type === 'npub') {
originalOwnerPubkey = decoded.data as string;
} else {
return error(400, 'Invalid npub format');
}
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }

8
src/routes/api/repos/[npub]/[repo]/tree/+server.ts

@ -8,6 +8,7 @@ import { FileManager } from '$lib/services/git/file-manager.js';
import { MaintainerService } from '$lib/services/nostr/maintainer-service.js'; import { MaintainerService } from '$lib/services/nostr/maintainer-service.js';
import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { requireNpubHex } from '$lib/utils/npub-utils.js';
import logger from '$lib/services/logger.js'; import logger from '$lib/services/logger.js';
const repoRoot = process.env.GIT_REPO_ROOT || '/repos'; const repoRoot = process.env.GIT_REPO_ROOT || '/repos';
@ -32,12 +33,7 @@ export const GET: RequestHandler = async ({ params, url, request }) => {
// Check repository privacy // Check repository privacy
let repoOwnerPubkey: string; let repoOwnerPubkey: string;
try { try {
const decoded = nip19.decode(npub); repoOwnerPubkey = requireNpubHex(npub);
if (decoded.type === 'npub') {
repoOwnerPubkey = decoded.data as string;
} else {
return error(400, 'Invalid npub format');
}
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }

12
src/routes/api/repos/[npub]/[repo]/verify/+server.ts

@ -32,12 +32,7 @@ export const GET: RequestHandler = async ({ params }: { params: { npub?: string;
// Decode npub to get pubkey // Decode npub to get pubkey
let ownerPubkey: string; let ownerPubkey: string;
try { try {
const decoded = nip19.decode(npub) as { type: string; data: unknown }; ownerPubkey = requireNpubHex(npub);
if (decoded.type === 'npub' && typeof decoded.data === 'string') {
ownerPubkey = decoded.data;
} else {
return error(400, 'Invalid npub format');
}
} catch { } catch {
return error(400, 'Invalid npub format'); return error(400, 'Invalid npub format');
} }
@ -87,10 +82,7 @@ export const GET: RequestHandler = async ({ params }: { params: { npub?: string;
// Decode npub if needed // Decode npub if needed
if (toPubkey) { if (toPubkey) {
try { try {
const decoded = nip19.decode(toPubkey) as { type: string; data: unknown }; toPubkey = decodeNpubToHex(toPubkey) || toPubkey;
if (decoded.type === 'npub' && typeof decoded.data === 'string') {
toPubkey = decoded.data;
}
} catch { } catch {
// Assume it's already hex // Assume it's already hex
} }

58
src/routes/api/search/+server.ts

@ -5,7 +5,7 @@
import { json, error } from '@sveltejs/kit'; import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types'; import type { RequestHandler } from './$types';
import { NostrClient } from '$lib/services/nostr/nostr-client.js'; import { NostrClient } from '$lib/services/nostr/nostr-client.js';
import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; import { DEFAULT_NOSTR_RELAYS, DEFAULT_NOSTR_SEARCH_RELAYS } from '$lib/config.js';
import { KIND } from '$lib/types/nostr.js'; import { KIND } from '$lib/types/nostr.js';
import { FileManager } from '$lib/services/git/file-manager.js'; import { FileManager } from '$lib/services/git/file-manager.js';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
@ -30,8 +30,8 @@ export const GET: RequestHandler = async ({ url }) => {
} }
try { try {
// Create a new client instance for each search to ensure fresh connections // Use search relays which are more likely to support NIP-50
const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS); const nostrClient = new NostrClient(DEFAULT_NOSTR_SEARCH_RELAYS);
const results: { const results: {
repos: Array<{ id: string; name: string; description: string; owner: string; npub: string }>; repos: Array<{ id: string; name: string; description: string; owner: string; npub: string }>;
@ -41,27 +41,53 @@ export const GET: RequestHandler = async ({ url }) => {
code: [] code: []
}; };
// Search repositories // Search repositories using NIP-50
if (type === 'repos' || type === 'all') { if (type === 'repos' || type === 'all') {
const events = await nostrClient.fetchEvents([ let events: Array<{ id: string; pubkey: string; tags: string[][]; content: string; created_at: number }> = [];
try {
// Try NIP-50 search first (relays that support it will return results sorted by relevance)
events = await nostrClient.fetchEvents([
{
kinds: [KIND.REPO_ANNOUNCEMENT],
search: query, // NIP-50: Search field
limit: limit * 2 // Get more results to account for different relay implementations
}
]);
logger.info({ query, eventCount: events.length }, 'NIP-50 search results');
} catch (nip50Error) {
// Fallback to manual filtering if NIP-50 fails or isn't supported
logger.warn({ error: nip50Error, query }, 'NIP-50 search failed, falling back to manual filtering');
const allEvents = await nostrClient.fetchEvents([
{ {
kinds: [KIND.REPO_ANNOUNCEMENT], kinds: [KIND.REPO_ANNOUNCEMENT],
limit: 100 limit: 500 // Get more events for manual filtering
} }
]); ]);
const searchLower = query.toLowerCase(); const searchLower = query.toLowerCase();
events = allEvents.filter(event => {
const name = event.tags.find(t => t[0] === 'name')?.[1] || '';
const description = event.tags.find(t => t[0] === 'description')?.[1] || '';
const repoId = event.tags.find(t => t[0] === 'd')?.[1] || '';
const content = event.content || '';
return name.toLowerCase().includes(searchLower) ||
description.toLowerCase().includes(searchLower) ||
repoId.toLowerCase().includes(searchLower) ||
content.toLowerCase().includes(searchLower);
});
}
// Process events into results
const searchLower = query.toLowerCase();
for (const event of events) { for (const event of events) {
const name = event.tags.find(t => t[0] === 'name')?.[1] || ''; const name = event.tags.find(t => t[0] === 'name')?.[1] || '';
const description = event.tags.find(t => t[0] === 'description')?.[1] || ''; const description = event.tags.find(t => t[0] === 'description')?.[1] || '';
const repoId = event.tags.find(t => t[0] === 'd')?.[1] || ''; const repoId = event.tags.find(t => t[0] === 'd')?.[1] || '';
const nameMatch = name.toLowerCase().includes(searchLower);
const descMatch = description.toLowerCase().includes(searchLower);
const repoMatch = repoId.toLowerCase().includes(searchLower);
if (nameMatch || descMatch || repoMatch) {
try { try {
const npub = nip19.npubEncode(event.pubkey); const npub = nip19.npubEncode(event.pubkey);
results.repos.push({ results.repos.push({
@ -75,14 +101,20 @@ export const GET: RequestHandler = async ({ url }) => {
// Skip if npub encoding fails // Skip if npub encoding fails
} }
} }
}
// Sort by relevance (name matches first) // Sort by relevance (name matches first, then description)
// Note: NIP-50 compliant relays should already return results sorted by relevance
results.repos.sort((a, b) => { results.repos.sort((a, b) => {
const aNameMatch = a.name.toLowerCase().includes(searchLower); const aNameMatch = a.name.toLowerCase().includes(searchLower);
const bNameMatch = b.name.toLowerCase().includes(searchLower); const bNameMatch = b.name.toLowerCase().includes(searchLower);
if (aNameMatch && !bNameMatch) return -1; if (aNameMatch && !bNameMatch) return -1;
if (!aNameMatch && bNameMatch) return 1; if (!aNameMatch && bNameMatch) return 1;
const aDescMatch = a.description.toLowerCase().includes(searchLower);
const bDescMatch = b.description.toLowerCase().includes(searchLower);
if (aDescMatch && !bDescMatch) return -1;
if (!aDescMatch && bDescMatch) return 1;
return 0; return 0;
}); });

5
src/routes/docs/+page.svelte

@ -21,7 +21,10 @@
return '<pre class="hljs"><code>' + return '<pre class="hljs"><code>' +
hljs.highlight(str, { language: lang }).value + hljs.highlight(str, { language: lang }).value +
'</code></pre>'; '</code></pre>';
} catch (__) {} } catch (err) {
// Fallback to escaped HTML if highlighting fails
// This is expected for unsupported languages
}
} }
return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>'; return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>';
} }

5
src/routes/docs/nip34/+page.svelte

@ -21,7 +21,10 @@
return '<pre class="hljs"><code>' + return '<pre class="hljs"><code>' +
hljs.highlight(str, { language: lang }).value + hljs.highlight(str, { language: lang }).value +
'</code></pre>'; '</code></pre>';
} catch (__) {} } catch (err) {
// Fallback to escaped HTML if highlighting fails
// This is expected for unsupported languages
}
} }
return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>'; return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>';
} }

5
src/routes/docs/nip34/spec/+page.svelte

@ -21,7 +21,10 @@
return '<pre class="hljs"><code>' + return '<pre class="hljs"><code>' +
hljs.highlight(str, { language: lang }).value + hljs.highlight(str, { language: lang }).value +
'</code></pre>'; '</code></pre>';
} catch (__) {} } catch (err) {
// Fallback to escaped HTML if highlighting fails
// This is expected for unsupported languages
}
} }
return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>'; return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>';
} }

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

@ -135,7 +135,10 @@
return '<pre class="hljs"><code>' + return '<pre class="hljs"><code>' +
hljs.highlight(str, { language: lang }).value + hljs.highlight(str, { language: lang }).value +
'</code></pre>'; '</code></pre>';
} catch (__) {} } catch (err) {
// Fallback to escaped HTML if highlighting fails
// This is expected for unsupported languages
}
} }
return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>'; return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>';
} }
@ -881,7 +884,7 @@
const response = await fetch(`/api/repos/${npub}/${repo}/issues`); const response = await fetch(`/api/repos/${npub}/${repo}/issues`);
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
issues = data.map((issue: any) => ({ issues = data.map((issue: { id: string; tags: string[][]; content: string; status?: string; pubkey: string; created_at: number }) => ({
id: issue.id, id: issue.id,
subject: issue.tags.find((t: string[]) => t[0] === 'subject')?.[1] || 'Untitled', subject: issue.tags.find((t: string[]) => t[0] === 'subject')?.[1] || 'Untitled',
content: issue.content, content: issue.content,
@ -955,7 +958,7 @@
const response = await fetch(`/api/repos/${npub}/${repo}/prs`); const response = await fetch(`/api/repos/${npub}/${repo}/prs`);
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
prs = data.map((pr: any) => ({ prs = data.map((pr: { id: string; tags: string[][]; content: string; status?: string; pubkey: string; created_at: number; commitId?: string }) => ({
id: pr.id, id: pr.id,
subject: pr.tags.find((t: string[]) => t[0] === 'subject')?.[1] || 'Untitled', subject: pr.tags.find((t: string[]) => t[0] === 'subject')?.[1] || 'Untitled',
content: pr.content, content: pr.content,

16
src/routes/search/+page.svelte

@ -52,7 +52,7 @@
<input <input
type="text" type="text"
bind:value={query} bind:value={query}
placeholder="Search repositories or code..." placeholder="Search repositories or code... (NIP-50 search)"
class="search-input" class="search-input"
/> />
<div class="search-controls"> <div class="search-controls">
@ -65,6 +65,9 @@
{loading ? 'Searching...' : 'Search'} {loading ? 'Searching...' : 'Search'}
</button> </button>
</div> </div>
<div class="search-info">
<small>Using NIP-50 search across multiple relays for better results</small>
</div>
</form> </form>
{#if error} {#if error}
@ -146,3 +149,14 @@
</main> </main>
</div> </div>
<style>
.search-info {
margin-top: 0.5rem;
color: var(--text-secondary, #666);
font-size: 0.875rem;
}
.search-info small {
color: inherit;
}
</style>

82
src/routes/signup/+page.svelte

@ -45,7 +45,8 @@
// Lookup state // Lookup state
let lookupLoading = $state<{ [key: string]: boolean }>({}); let lookupLoading = $state<{ [key: string]: boolean }>({});
let lookupError = $state<{ [key: string]: string | null }>({}); let lookupError = $state<{ [key: string]: string | null }>({});
let lookupResults = $state<{ [key: string]: any }>({}); type ProfileData = { pubkey: string; npub: string; name?: string; about?: string; picture?: string };
let lookupResults = $state<{ [key: string]: Array<ProfileData | NostrEvent> | null }>({});
import { DEFAULT_NOSTR_RELAYS, DEFAULT_NOSTR_SEARCH_RELAYS, combineRelays } from '../../lib/config.js'; import { DEFAULT_NOSTR_RELAYS, DEFAULT_NOSTR_SEARCH_RELAYS, combineRelays } from '../../lib/config.js';
@ -434,7 +435,13 @@
} }
]); ]);
let profileData: any = { let profileData: {
pubkey: string;
npub: string;
name?: string;
about?: string;
picture?: string;
} = {
pubkey, pubkey,
npub: query.startsWith('npub') ? query : nip19.npubEncode(pubkey) npub: query.startsWith('npub') ? query : nip19.npubEncode(pubkey)
}; };
@ -1212,22 +1219,24 @@
<img src="/icons/x.svg" alt="Clear" class="icon-small" /> <img src="/icons/x.svg" alt="Clear" class="icon-small" />
</button> </button>
</div> </div>
{#each lookupResults['repo-existingRepoRef'] as result} {#each (lookupResults['repo-existingRepoRef'] || []) as result}
{@const nameTag = result.tags.find((t: string[]) => t[0] === 'name')?.[1]} {#if 'tags' in result}
{@const dTag = result.tags.find((t: string[]) => t[0] === 'd')?.[1]} {@const event = result as NostrEvent}
{@const descTag = result.tags.find((t: string[]) => t[0] === 'description')?.[1]} {@const nameTag = event.tags.find((t: string[]) => t[0] === 'name')?.[1]}
{@const imageTag = result.tags.find((t: string[]) => t[0] === 'image')?.[1]} {@const dTag = event.tags.find((t: string[]) => t[0] === 'd')?.[1]}
{@const ownerNpub = nip19.npubEncode(result.pubkey)} {@const descTag = event.tags.find((t: string[]) => t[0] === 'description')?.[1]}
{@const tags = result.tags.filter((t: string[]) => t[0] === 't' && t[1] && t[1] !== 'private' && t[1] !== 'fork').map((t: string[]) => t[1])} {@const imageTag = event.tags.find((t: string[]) => t[0] === 'image')?.[1]}
{@const ownerNpub = nip19.npubEncode(event.pubkey)}
{@const tags = event.tags.filter((t: string[]) => t[0] === 't' && t[1] && t[1] !== 'private' && t[1] !== 'fork').map((t: string[]) => t[1])}
<div <div
class="lookup-result-item repo-result" class="lookup-result-item repo-result"
role="button" role="button"
tabindex="0" tabindex="0"
onclick={() => selectRepoResult(result, 'existingRepoRef')} onclick={() => selectRepoResult(event, 'existingRepoRef')}
onkeydown={(e) => { onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') { if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault(); e.preventDefault();
selectRepoResult(result, 'existingRepoRef'); selectRepoResult(event, 'existingRepoRef');
} }
}} }}
> >
@ -1245,7 +1254,7 @@
{/if} {/if}
<div class="result-meta"> <div class="result-meta">
<small>Owner: {ownerNpub.slice(0, 16)}...</small> <small>Owner: {ownerNpub.slice(0, 16)}...</small>
<small>Event: {result.id.slice(0, 16)}...</small> <small>Event: {event.id.slice(0, 16)}...</small>
</div> </div>
{#if tags.length > 0} {#if tags.length > 0}
<div class="result-tags"> <div class="result-tags">
@ -1257,6 +1266,7 @@
</div> </div>
</div> </div>
</div> </div>
{/if}
{/each} {/each}
</div> </div>
{/if} {/if}
@ -1435,7 +1445,7 @@
{#if lookupResults[`npub-maintainers-${index}`]} {#if lookupResults[`npub-maintainers-${index}`]}
<div class="lookup-results"> <div class="lookup-results">
<div class="lookup-results-header"> <div class="lookup-results-header">
<span>Found {lookupResults[`npub-maintainers-${index}`].length} profile(s):</span> <span>Found {(lookupResults[`npub-maintainers-${index}`] || []).length} profile(s):</span>
<button <button
type="button" type="button"
onclick={() => clearLookupResults(`npub-maintainers-${index}`)} onclick={() => clearLookupResults(`npub-maintainers-${index}`)}
@ -1445,36 +1455,39 @@
<img src="/icons/x.svg" alt="Clear" class="icon-small" /> <img src="/icons/x.svg" alt="Clear" class="icon-small" />
</button> </button>
</div> </div>
{#each lookupResults[`npub-maintainers-${index}`] as result} {#each (lookupResults[`npub-maintainers-${index}`] || []) as result}
{#if 'npub' in result}
{@const profile = result as ProfileData}
<div <div
class="lookup-result-item profile-result" class="lookup-result-item profile-result"
role="button" role="button"
tabindex="0" tabindex="0"
onclick={() => selectNpubResult(result, 'maintainers', index)} onclick={() => selectNpubResult(profile, 'maintainers', index)}
onkeydown={(e) => { onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') { if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault(); e.preventDefault();
selectNpubResult(result, 'maintainers', index); selectNpubResult(profile, 'maintainers', index);
} }
}} }}
> >
<div class="result-header"> <div class="result-header">
{#if result.picture} {#if profile.picture}
<img src={result.picture} alt="" class="result-avatar" /> <img src={profile.picture} alt="" class="result-avatar" />
{:else} {:else}
<div class="result-avatar-placeholder"> <div class="result-avatar-placeholder">
{(result.name || result.npub).slice(0, 2).toUpperCase()} {(profile.name || profile.npub).slice(0, 2).toUpperCase()}
</div> </div>
{/if} {/if}
<div class="result-info"> <div class="result-info">
<strong>{result.name || 'Unknown'}</strong> <strong>{profile.name || 'Unknown'}</strong>
{#if result.about} {#if profile.about}
<p class="result-description">{result.about}</p> <p class="result-description">{profile.about}</p>
{/if} {/if}
<small class="npub-display">{result.npub}</small> <small class="npub-display">{profile.npub}</small>
</div> </div>
</div> </div>
</div> </div>
{/if}
{/each} {/each}
</div> </div>
{/if} {/if}
@ -1750,22 +1763,24 @@
<img src="/icons/x.svg" alt="Clear" class="icon-small" /> <img src="/icons/x.svg" alt="Clear" class="icon-small" />
</button> </button>
</div> </div>
{#each lookupResults['repo-forkOriginalRepo'] as result} {#each (lookupResults['repo-forkOriginalRepo'] || []) as result}
{@const nameTag = result.tags.find((t: string[]) => t[0] === 'name')?.[1]} {#if 'tags' in result}
{@const dTag = result.tags.find((t: string[]) => t[0] === 'd')?.[1]} {@const event = result as NostrEvent}
{@const descTag = result.tags.find((t: string[]) => t[0] === 'description')?.[1]} {@const nameTag = event.tags.find((t: string[]) => t[0] === 'name')?.[1]}
{@const imageTag = result.tags.find((t: string[]) => t[0] === 'image')?.[1]} {@const dTag = event.tags.find((t: string[]) => t[0] === 'd')?.[1]}
{@const ownerNpub = nip19.npubEncode(result.pubkey)} {@const descTag = event.tags.find((t: string[]) => t[0] === 'description')?.[1]}
{@const tags = result.tags.filter((t: string[]) => t[0] === 't' && t[1] && t[1] !== 'private' && t[1] !== 'fork').map((t: string[]) => t[1])} {@const imageTag = event.tags.find((t: string[]) => t[0] === 'image')?.[1]}
{@const ownerNpub = nip19.npubEncode(event.pubkey)}
{@const tags = event.tags.filter((t: string[]) => t[0] === 't' && t[1] && t[1] !== 'private' && t[1] !== 'fork').map((t: string[]) => t[1])}
<div <div
class="lookup-result-item repo-result" class="lookup-result-item repo-result"
role="button" role="button"
tabindex="0" tabindex="0"
onclick={() => selectRepoResult(result, 'forkOriginalRepo')} onclick={() => selectRepoResult(event, 'forkOriginalRepo')}
onkeydown={(e) => { onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') { if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault(); e.preventDefault();
selectRepoResult(result, 'forkOriginalRepo'); selectRepoResult(event, 'forkOriginalRepo');
} }
}} }}
> >
@ -1783,7 +1798,7 @@
{/if} {/if}
<div class="result-meta"> <div class="result-meta">
<small>Owner: {ownerNpub.slice(0, 16)}...</small> <small>Owner: {ownerNpub.slice(0, 16)}...</small>
<small>Event: {result.id.slice(0, 16)}...</small> <small>Event: {event.id.slice(0, 16)}...</small>
</div> </div>
{#if tags.length > 0} {#if tags.length > 0}
<div class="result-tags"> <div class="result-tags">
@ -1795,6 +1810,7 @@
</div> </div>
</div> </div>
</div> </div>
{/if}
{/each} {/each}
</div> </div>
{/if} {/if}

Loading…
Cancel
Save