Browse Source

fix threads

master
Silberengel 1 month ago
parent
commit
3f1b33fc32
  1. 4
      public/healthz.json
  2. 34
      src/lib/components/layout/Header.svelte
  3. 12
      src/lib/components/layout/ProfileBadge.svelte
  4. 4
      src/lib/modules/threads/ThreadCard.svelte
  5. 137
      src/lib/modules/threads/ThreadList.svelte
  6. 29
      src/lib/services/auth/activity-tracker.js
  7. 1
      src/lib/services/auth/activity-tracker.js.map
  8. 40
      src/lib/services/auth/activity-tracker.ts
  9. 45
      src/lib/services/auth/anonymous-signer.js
  10. 1
      src/lib/services/auth/anonymous-signer.js.map
  11. 31
      src/lib/services/auth/bunker-signer.js
  12. 1
      src/lib/services/auth/bunker-signer.js.map
  13. 39
      src/lib/services/auth/nip07-signer.js
  14. 1
      src/lib/services/auth/nip07-signer.js.map
  15. 62
      src/lib/services/auth/nsec-signer.js
  16. 1
      src/lib/services/auth/nsec-signer.js.map
  17. 98
      src/lib/services/auth/profile-fetcher.js
  18. 1
      src/lib/services/auth/profile-fetcher.js.map
  19. 65
      src/lib/services/auth/relay-list-fetcher.js
  20. 1
      src/lib/services/auth/relay-list-fetcher.js.map
  21. 99
      src/lib/services/auth/session-manager.js
  22. 1
      src/lib/services/auth/session-manager.js.map
  23. 106
      src/lib/services/auth/session-manager.ts
  24. 12
      src/lib/services/auth/user-preferences-fetcher.js
  25. 1
      src/lib/services/auth/user-preferences-fetcher.js.map
  26. 38
      src/lib/services/auth/user-status-fetcher.js
  27. 1
      src/lib/services/auth/user-status-fetcher.js.map
  28. 52
      src/lib/services/cache/anonymous-key-store.js
  29. 1
      src/lib/services/cache/anonymous-key-store.js.map
  30. 88
      src/lib/services/cache/event-cache.js
  31. 1
      src/lib/services/cache/event-cache.js.map
  32. 20
      src/lib/services/cache/event-cache.ts
  33. 48
      src/lib/services/cache/indexeddb-store.js
  34. 1
      src/lib/services/cache/indexeddb-store.js.map
  35. 42
      src/lib/services/cache/profile-cache.js
  36. 1
      src/lib/services/cache/profile-cache.js.map
  37. 43
      src/lib/services/cache/search-index.js
  38. 1
      src/lib/services/cache/search-index.js.map
  39. 101
      src/lib/services/nostr/applesauce-client.js
  40. 1
      src/lib/services/nostr/applesauce-client.js.map
  41. 7
      src/lib/services/nostr/applesauce-client.ts
  42. 119
      src/lib/services/nostr/auth-handler.js
  43. 1
      src/lib/services/nostr/auth-handler.js.map
  44. 4
      src/lib/services/nostr/auth-handler.ts
  45. 49
      src/lib/services/nostr/config.js
  46. 1
      src/lib/services/nostr/config.js.map
  47. 213
      src/lib/services/nostr/event-store.js
  48. 1
      src/lib/services/nostr/event-store.js.map
  49. 267
      src/lib/services/nostr/event-store.ts
  50. 41
      src/lib/services/nostr/event-utils.js
  51. 1
      src/lib/services/nostr/event-utils.js.map
  52. 181
      src/lib/services/nostr/relay-pool.js
  53. 1
      src/lib/services/nostr/relay-pool.js.map
  54. 119
      src/lib/services/nostr/subscription-manager.js
  55. 1
      src/lib/services/nostr/subscription-manager.js.map
  56. 42
      src/lib/services/security/bech32-utils.js
  57. 1
      src/lib/services/security/bech32-utils.js.map
  58. 48
      src/lib/services/security/event-validator.js
  59. 1
      src/lib/services/security/event-validator.js.map
  60. 68
      src/lib/services/security/key-management.js
  61. 1
      src/lib/services/security/key-management.js.map
  62. 44
      src/lib/services/security/sanitizer.js
  63. 1
      src/lib/services/security/sanitizer.js.map
  64. 11
      src/routes/+layout.svelte
  65. 4
      src/routes/+page.svelte

4
public/healthz.json

@ -2,7 +2,7 @@
"status": "ok", "status": "ok",
"service": "aitherboard", "service": "aitherboard",
"version": "0.1.0", "version": "0.1.0",
"buildTime": "2026-02-02T13:24:45.619Z", "buildTime": "2026-02-02T13:56:52.264Z",
"gitCommit": "unknown", "gitCommit": "unknown",
"timestamp": 1770038685620 "timestamp": 1770040612264
} }

34
src/lib/components/layout/Header.svelte

@ -1,22 +1,33 @@
<script lang="ts"> <script lang="ts">
import { sessionManager } from '../../services/auth/session-manager.js'; import { sessionManager, type UserSession } from '../../services/auth/session-manager.js';
import ThemeToggle from '../preferences/ThemeToggle.svelte'; import ThemeToggle from '../preferences/ThemeToggle.svelte';
import ProfileBadge from '../layout/ProfileBadge.svelte';
$: currentSession = $sessionManager.session; let currentSession = $state<UserSession | null>(sessionManager.session.value);
$: isLoggedIn = currentSession !== null; let isLoggedIn = $derived(currentSession !== null);
$: currentPubkey = currentSession?.pubkey || null; let currentPubkey = $derived(currentSession?.pubkey || null);
// Subscribe to session changes
$effect(() => {
const unsubscribe = sessionManager.session.subscribe((session: UserSession | null) => {
currentSession = session;
});
return unsubscribe;
});
</script> </script>
<header class="relative border-b border-fog-border dark:border-fog-dark-border"> <header class="relative border-b border-fog-border dark:border-fog-dark-border">
<!-- Banner image --> <!-- Banner image -->
<div class="w-full h-32 md:h-48 overflow-hidden bg-fog-surface dark:bg-fog-dark-surface"> <div class="h-32 md:h-48 overflow-hidden bg-fog-surface dark:bg-fog-dark-surface">
<div class="max-w-7xl mx-auto h-full">
<img <img
src="/aither.png" src="/aither.png"
alt="Aitherboard banner" alt="Aitherboard banner"
class="w-full h-full object-cover opacity-90 dark:opacity-70" class="w-full h-full object-cover opacity-90 dark:opacity-70"
/> />
<!-- Overlay gradient for text readability --> <!-- Overlay gradient for text readability -->
<div class="absolute inset-0 bg-gradient-to-b from-fog-bg/30 to-fog-bg/80 dark:from-fog-dark-bg/40 dark:to-fog-dark-bg/90"></div> <div class="absolute inset-0 bg-gradient-to-b from-fog-bg/30 to-fog-bg/80 dark:from-fog-dark-bg/40 dark:to-fog-dark-bg/90 pointer-events-none"></div>
</div>
</div> </div>
<!-- Navigation --> <!-- Navigation -->
@ -24,9 +35,11 @@
<div class="flex items-center justify-between max-w-7xl mx-auto"> <div class="flex items-center justify-between max-w-7xl mx-auto">
<a href="/" class="text-xl font-semibold text-fog-text dark:text-fog-dark-text hover:text-fog-text-light dark:hover:text-fog-dark-text-light transition-colors">Aitherboard</a> <a href="/" class="text-xl font-semibold text-fog-text dark:text-fog-dark-text hover:text-fog-text-light dark:hover:text-fog-dark-text-light transition-colors">Aitherboard</a>
<div class="flex gap-4 items-center text-sm"> <div class="flex gap-4 items-center text-sm">
<ThemeToggle /> <a href="/threads" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors">Threads</a>
{#if isLoggedIn} <a href="/feed" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors">Feed</a>
<span class="text-fog-text-light dark:text-fog-dark-text-light">Logged in as: {currentPubkey?.slice(0, 16)}...</span> {#if isLoggedIn && currentPubkey}
<span class="text-fog-text-light dark:text-fog-dark-text-light">Logged in as </span>
<ProfileBadge pubkey={currentPubkey} />
<button <button
onclick={() => sessionManager.clearSession()} onclick={() => sessionManager.clearSession()}
class="px-3 py-1 rounded border border-fog-border dark:border-fog-dark-border bg-fog-post dark:bg-fog-dark-post hover:bg-fog-highlight dark:hover:bg-fog-dark-highlight text-fog-text dark:text-fog-dark-text transition-colors" class="px-3 py-1 rounded border border-fog-border dark:border-fog-dark-border bg-fog-post dark:bg-fog-dark-post hover:bg-fog-highlight dark:hover:bg-fog-dark-highlight text-fog-text dark:text-fog-dark-text transition-colors"
@ -36,8 +49,7 @@
{:else} {:else}
<a href="/login" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors">Login</a> <a href="/login" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors">Login</a>
{/if} {/if}
<a href="/feed" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors">Feed</a> <ThemeToggle />
<a href="/threads" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors">Threads</a>
</div> </div>
</div> </div>
</nav> </nav>

12
src/lib/components/layout/ProfileBadge.svelte

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { getActivityStatus } from '../../services/auth/activity-tracker.js'; import { getActivityStatus, getActivityMessage } from '../../services/auth/activity-tracker.js';
import { fetchProfile } from '../../services/auth/profile-fetcher.js'; import { fetchProfile } from '../../services/auth/profile-fetcher.js';
import { fetchUserStatus } from '../../services/auth/user-status-fetcher.js'; import { fetchUserStatus } from '../../services/auth/user-status-fetcher.js';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
@ -13,6 +13,7 @@
let profile = $state<{ name?: string; picture?: string } | null>(null); let profile = $state<{ name?: string; picture?: string } | null>(null);
let status = $state<string | null>(null); let status = $state<string | null>(null);
let activityStatus = $state<'red' | 'yellow' | 'green' | null>(null); let activityStatus = $state<'red' | 'yellow' | 'green' | null>(null);
let activityMessage = $state<string | null>(null);
$effect(() => { $effect(() => {
if (pubkey) { if (pubkey) {
@ -33,8 +34,9 @@
status = await fetchUserStatus(pubkey); status = await fetchUserStatus(pubkey);
} }
function updateActivityStatus() { async function updateActivityStatus() {
activityStatus = getActivityStatus(pubkey); activityStatus = await getActivityStatus(pubkey);
activityMessage = await getActivityMessage(pubkey);
} }
function getActivityColor(): string { function getActivityColor(): string {
@ -58,11 +60,11 @@
<div class="w-6 h-6 rounded bg-fog-highlight dark:bg-fog-dark-highlight"></div> <div class="w-6 h-6 rounded bg-fog-highlight dark:bg-fog-dark-highlight"></div>
{/if} {/if}
<span>{profile?.name || pubkey.slice(0, 16)}...</span> <span>{profile?.name || pubkey.slice(0, 16)}...</span>
{#if activityStatus} {#if activityStatus && activityMessage}
<span <span
class="w-2 h-2 rounded-full" class="w-2 h-2 rounded-full"
style="background-color: {getActivityColor()}" style="background-color: {getActivityColor()}"
title="Activity indicator" title={activityMessage}
></span> ></span>
{/if} {/if}
{#if status} {#if status}

4
src/lib/modules/threads/ThreadCard.svelte

@ -48,10 +48,10 @@
<span class="text-sm text-fog-text-light dark:text-fog-dark-text-light">{getRelativeTime()}</span> <span class="text-sm text-fog-text-light dark:text-fog-dark-text-light">{getRelativeTime()}</span>
</div> </div>
<div class="mb-2"> <div class="mb-2 flex items-center gap-2">
<ProfileBadge pubkey={thread.pubkey} /> <ProfileBadge pubkey={thread.pubkey} />
{#if getClientName()} {#if getClientName()}
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light ml-2">via {getClientName()}</span> <span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">via {getClientName()}</span>
{/if} {/if}
</div> </div>

137
src/lib/modules/threads/ThreadList.svelte

@ -8,6 +8,7 @@
let loading = $state(true); let loading = $state(true);
let sortBy = $state<'newest' | 'active' | 'upvoted'>('newest'); let sortBy = $state<'newest' | 'active' | 'upvoted'>('newest');
let showOlder = $state(false); let showOlder = $state(false);
let selectedTopic = $state<string | null | undefined>(null); // null = All, undefined = General, string = specific topic
$effect(() => { $effect(() => {
loadThreads(); loadThreads();
@ -21,15 +22,26 @@
? undefined ? undefined
: Math.floor(Date.now() / 1000) - config.threadTimeoutDays * 86400; : Math.floor(Date.now() / 1000) - config.threadTimeoutDays * 86400;
// Fetch with cache-first, background refresh
// onUpdate callback will refresh the UI when new data arrives
const events = await nostrClient.fetchEvents( const events = await nostrClient.fetchEvents(
[{ kinds: [11], since, limit: 50 }], [{ kinds: [11], since, limit: 50 }],
[...config.defaultRelays], [...config.defaultRelays],
{ useCache: true, cacheResults: true } {
useCache: true,
cacheResults: true,
onUpdate: (updatedEvents) => {
// Update threads when fresh data arrives from relays
threads = sortThreads(updatedEvents);
}
}
); );
// Set initial cached data immediately
threads = sortThreads(events); threads = sortThreads(events);
} catch (error) { } catch (error) {
console.error('Error loading threads:', error); console.error('Error loading threads:', error);
threads = []; // Set empty array on error to prevent undefined issues
} finally { } finally {
loading = false; loading = false;
} }
@ -52,25 +64,96 @@
function getTopics(): string[] { function getTopics(): string[] {
const topicSet = new Set<string>(); const topicSet = new Set<string>();
for (const thread of threads) { // Use age-filtered threads for topic extraction
const ageFiltered = filterByAge(threads);
for (const thread of ageFiltered) {
const topics = thread.tags.filter((t) => t[0] === 't').map((t) => t[1]); const topics = thread.tags.filter((t) => t[0] === 't').map((t) => t[1]);
topics.forEach((t) => topicSet.add(t)); topics.forEach((t) => topicSet.add(t));
} }
return Array.from(topicSet).sort(); return Array.from(topicSet).sort();
} }
function getTopicsWithCounts(): Array<{ topic: string | null; count: number }> {
// Use age-filtered threads for counts
const ageFiltered = filterByAge(threads);
const topics = getTopics();
const result: Array<{ topic: string | null; count: number }> = [];
// Add "General" (threads without topics)
const generalCount = ageFiltered.filter((t) => !t.tags.some((tag) => tag[0] === 't')).length;
if (generalCount > 0) {
result.push({ topic: null, count: generalCount });
}
// Add topics with counts
for (const topic of topics) {
const count = ageFiltered.filter((t) => t.tags.some((tag) => tag[0] === 't' && tag[1] === topic)).length;
if (count > 0) {
result.push({ topic, count });
}
}
return result;
}
/**
* Filter threads by age (30 days)
*/
function filterByAge(events: NostrEvent[]): NostrEvent[] {
if (showOlder) {
return events; // Show all threads if "show older" is checked
}
const config = nostrClient.getConfig();
const cutoffTime = Math.floor(Date.now() / 1000) - config.threadTimeoutDays * 86400;
return events.filter((t) => t.created_at >= cutoffTime);
}
function getFilteredThreads(): NostrEvent[] {
let filtered = threads;
// Filter by age first
filtered = filterByAge(filtered);
// Then filter by topic
// selectedTopic === null means "All" - show all threads (handled in template)
// selectedTopic === undefined means "General" - show threads without topics
if (selectedTopic === undefined) {
return filtered.filter((t) => !t.tags.some((tag) => tag[0] === 't'));
}
// selectedTopic is a string - show threads with that topic
return filtered.filter((t) => t.tags.some((tag) => tag[0] === 't' && tag[1] === selectedTopic));
}
function getThreadsByTopic(topic: string | null): NostrEvent[] { function getThreadsByTopic(topic: string | null): NostrEvent[] {
let filtered = threads;
// Filter by age first
filtered = filterByAge(filtered);
// Then filter by topic
if (topic === null) { if (topic === null) {
return threads.filter((t) => !t.tags.some((tag) => tag[0] === 't')); return filtered.filter((t) => !t.tags.some((tag) => tag[0] === 't'));
} }
return threads.filter((t) => t.tags.some((tag) => tag[0] === 't' && tag[1] === topic)); return filtered.filter((t) => t.tags.some((tag) => tag[0] === 't' && tag[1] === topic));
} }
</script> </script>
<div class="thread-list"> <div class="thread-list">
<div class="controls mb-4 flex gap-4 items-center"> <div class="controls mb-4 flex gap-4 items-center flex-wrap">
<label> <label class="text-fog-text dark:text-fog-dark-text">
<input type="checkbox" bind:checked={showOlder} onchange={loadThreads} /> <input
type="checkbox"
bind:checked={showOlder}
onchange={() => {
// If showing older threads, reload to fetch them
// If hiding older threads, just filter client-side (no reload needed)
if (showOlder) {
loadThreads();
}
}}
class="mr-2"
/>
Show older threads Show older threads
</label> </label>
<select bind:value={sortBy} onchange={() => (threads = sortThreads(threads))} class="border border-fog-border dark:border-fog-dark-border bg-fog-post dark:bg-fog-dark-post text-fog-text dark:text-fog-dark-text px-2 py-1 rounded"> <select bind:value={sortBy} onchange={() => (threads = sortThreads(threads))} class="border border-fog-border dark:border-fog-dark-border bg-fog-post dark:bg-fog-dark-post text-fog-text dark:text-fog-dark-text px-2 py-1 rounded">
@ -83,7 +166,35 @@
{#if loading} {#if loading}
<p class="text-fog-text dark:text-fog-dark-text">Loading threads...</p> <p class="text-fog-text dark:text-fog-dark-text">Loading threads...</p>
{:else} {:else}
<!-- Topic Filter -->
<div class="mb-6">
<div class="flex flex-wrap gap-2 items-center">
<span class="text-sm font-semibold text-fog-text dark:text-fog-dark-text mr-2">Filter by topic:</span>
<button
onclick={() => (selectedTopic = null)}
class="px-3 py-1 rounded border transition-colors {selectedTopic === null
? 'bg-fog-accent dark:bg-fog-dark-accent text-white border-fog-accent dark:border-fog-dark-accent'
: 'bg-fog-post dark:bg-fog-dark-post text-fog-text dark:text-fog-dark-text border-fog-border dark:border-fog-dark-border hover:bg-fog-highlight dark:hover:bg-fog-dark-highlight'}"
>
All ({filterByAge(threads).length})
</button>
{#each getTopicsWithCounts() as { topic, count }}
<button
onclick={() => (selectedTopic = topic === null ? undefined : topic)}
class="px-3 py-1 rounded border transition-colors {selectedTopic === (topic === null ? undefined : topic)
? 'bg-fog-accent dark:bg-fog-dark-accent text-white border-fog-accent dark:border-fog-dark-accent'
: 'bg-fog-post dark:bg-fog-dark-post text-fog-text dark:text-fog-dark-text border-fog-border dark:border-fog-dark-border hover:bg-fog-highlight dark:hover:bg-fog-dark-highlight'}"
>
{topic === null ? 'General' : topic} ({count})
</button>
{/each}
</div>
</div>
<!-- Threads Display -->
<div> <div>
{#if selectedTopic === null}
<!-- Show all threads grouped by topic -->
<h2 class="text-xl font-bold mb-4 text-fog-text dark:text-fog-dark-text">General</h2> <h2 class="text-xl font-bold mb-4 text-fog-text dark:text-fog-dark-text">General</h2>
{#each getThreadsByTopic(null) as thread} {#each getThreadsByTopic(null) as thread}
<ThreadCard {thread} /> <ThreadCard {thread} />
@ -95,6 +206,18 @@
<ThreadCard {thread} /> <ThreadCard {thread} />
{/each} {/each}
{/each} {/each}
{:else}
<!-- Show filtered threads -->
<h2 class="text-xl font-bold mb-4 text-fog-text dark:text-fog-dark-text">
{selectedTopic === undefined ? 'General' : selectedTopic}
</h2>
{#each getFilteredThreads() as thread}
<ThreadCard {thread} />
{/each}
{#if getFilteredThreads().length === 0}
<p class="text-fog-text-light dark:text-fog-dark-text-light">No threads found in this topic.</p>
{/if}
{/if}
</div> </div>
{/if} {/if}
</div> </div>

29
src/lib/services/auth/activity-tracker.js

@ -1,29 +0,0 @@
/**
* Activity tracker - tracks last activity per pubkey
*/
import { eventStore } from '../nostr/event-store.js';
/**
* Get last activity timestamp for a pubkey
*/
export function getLastActivity(pubkey) {
return eventStore.getLastActivity(pubkey);
}
/**
* Get activity status color
* Red: 168 hours (7 days)
* Yellow: 48 hours (2 days) but <168 hours
* Green: <48 hours
*/
export function getActivityStatus(pubkey) {
const lastActivity = getLastActivity(pubkey);
if (!lastActivity)
return null;
const now = Math.floor(Date.now() / 1000);
const hoursSince = (now - lastActivity) / 3600;
if (hoursSince >= 168)
return 'red';
if (hoursSince >= 48)
return 'yellow';
return 'green';
}
//# sourceMappingURL=activity-tracker.js.map

1
src/lib/services/auth/activity-tracker.js.map

@ -1 +0,0 @@
{"version":3,"file":"activity-tracker.js","sourceRoot":"","sources":["activity-tracker.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAErD;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,OAAO,UAAU,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;AAC5C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAc;IAC9C,MAAM,YAAY,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IAC7C,IAAI,CAAC,YAAY;QAAE,OAAO,IAAI,CAAC;IAE/B,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAC1C,MAAM,UAAU,GAAG,CAAC,GAAG,GAAG,YAAY,CAAC,GAAG,IAAI,CAAC;IAE/C,IAAI,UAAU,IAAI,GAAG;QAAE,OAAO,KAAK,CAAC;IACpC,IAAI,UAAU,IAAI,EAAE;QAAE,OAAO,QAAQ,CAAC;IACtC,OAAO,OAAO,CAAC;AACjB,CAAC"}

40
src/lib/services/auth/activity-tracker.ts

@ -7,7 +7,7 @@ import { eventStore } from '../nostr/event-store.js';
/** /**
* Get last activity timestamp for a pubkey * Get last activity timestamp for a pubkey
*/ */
export function getLastActivity(pubkey: string): number | undefined { export async function getLastActivity(pubkey: string): Promise<number | undefined> {
return eventStore.getLastActivity(pubkey); return eventStore.getLastActivity(pubkey);
} }
@ -17,8 +17,8 @@ export function getLastActivity(pubkey: string): number | undefined {
* Yellow: 48 hours (2 days) but <168 hours * Yellow: 48 hours (2 days) but <168 hours
* Green: <48 hours * Green: <48 hours
*/ */
export function getActivityStatus(pubkey: string): 'red' | 'yellow' | 'green' | null { export async function getActivityStatus(pubkey: string): Promise<'red' | 'yellow' | 'green' | null> {
const lastActivity = getLastActivity(pubkey); const lastActivity = await getLastActivity(pubkey);
if (!lastActivity) return null; if (!lastActivity) return null;
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
@ -28,3 +28,37 @@ export function getActivityStatus(pubkey: string): 'red' | 'yellow' | 'green' |
if (hoursSince >= 48) return 'yellow'; if (hoursSince >= 48) return 'yellow';
return 'green'; return 'green';
} }
/**
* Get activity status message for tooltip
*/
export async function getActivityMessage(pubkey: string): Promise<string | null> {
const lastActivity = await getLastActivity(pubkey);
if (!lastActivity) return null;
const now = Math.floor(Date.now() / 1000);
const hoursSince = (now - lastActivity) / 3600;
const daysSince = hoursSince / 24;
if (hoursSince < 1) {
const minutesSince = Math.floor((now - lastActivity) / 60);
if (minutesSince < 1) {
return 'Active just now';
}
return `Active ${minutesSince} minute${minutesSince === 1 ? '' : 's'} ago`;
} else if (hoursSince < 24) {
const hours = Math.floor(hoursSince);
return `Active ${hours} hour${hours === 1 ? '' : 's'} ago`;
} else if (daysSince < 7) {
const days = Math.floor(daysSince);
return `Active ${days} day${days === 1 ? '' : 's'} ago`;
} else {
const weeks = Math.floor(daysSince / 7);
if (weeks < 4) {
return `Active ${weeks} week${weeks === 1 ? '' : 's'} ago`;
} else {
const months = Math.floor(daysSince / 30);
return `Active ${months} month${months === 1 ? '' : 's'} ago`;
}
}
}

45
src/lib/services/auth/anonymous-signer.js

@ -1,45 +0,0 @@
/**
* Anonymous signer (generated keys, NIP-49 encrypted)
*/
import { generatePrivateKey } from '../security/key-management.js';
import { storeAnonymousKey, getAnonymousKey } from '../cache/anonymous-key-store.js';
import { getPublicKeyFromNsec } from './nsec-signer.js';
import { signEventWithNsec } from './nsec-signer.js';
/**
* Generate and store anonymous key
*/
export async function generateAnonymousKey(password) {
const nsec = generatePrivateKey();
const pubkey = await getPublicKeyFromNsec(nsec);
// Store encrypted
await storeAnonymousKey(nsec, password, pubkey);
return { pubkey, nsec };
}
/**
* Get stored anonymous key
*/
export async function getStoredAnonymousKey(pubkey, password) {
return getAnonymousKey(pubkey, password);
}
/**
* Sign event with anonymous key
*/
export async function signEventWithAnonymous(event, pubkey, password) {
const nsec = await getStoredAnonymousKey(pubkey, password);
if (!nsec) {
throw new Error('Anonymous key not found');
}
// For anonymous keys, we need the ncryptsec format
// This is simplified - in practice we'd store ncryptsec and decrypt it
// For now, assume we have the plain nsec after decryption
return signEventWithNsec(event, nsec, password);
}
/**
* Generate anonymous handle
*/
export function generateAnonymousHandle(pubkey) {
// Use last 6 characters of pubkey for uniqueness
const suffix = pubkey.slice(-6);
return `Aitherite${suffix}`;
}
//# sourceMappingURL=anonymous-signer.js.map

1
src/lib/services/auth/anonymous-signer.js.map

@ -1 +0,0 @@
{"version":3,"file":"anonymous-signer.js","sourceRoot":"","sources":["anonymous-signer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACrF,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAGrD;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,QAAgB;IAIzD,MAAM,IAAI,GAAG,kBAAkB,EAAE,CAAC;IAClC,MAAM,MAAM,GAAG,MAAM,oBAAoB,CAAC,IAAI,CAAC,CAAC;IAEhD,kBAAkB;IAClB,MAAM,iBAAiB,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IAEhD,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAC1B,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,MAAc,EACd,QAAgB;IAEhB,OAAO,eAAe,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AAC3C,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,KAAqC,EACrC,MAAc,EACd,QAAgB;IAEhB,MAAM,IAAI,GAAG,MAAM,qBAAqB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAC3D,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC7C,CAAC;IAED,mDAAmD;IACnD,uEAAuE;IACvE,0DAA0D;IAC1D,OAAO,iBAAiB,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;AAClD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAc;IACpD,iDAAiD;IACjD,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAChC,OAAO,YAAY,MAAM,EAAE,CAAC;AAC9B,CAAC"}

31
src/lib/services/auth/bunker-signer.js

@ -1,31 +0,0 @@
/**
* NIP-46 Bunker signer (remote signer)
*/
/**
* Connect to bunker signer
*/
export async function connectBunker(bunkerUri) {
// Parse bunker:// URI
// Format: bunker://<pubkey>@<relay>?token=<token>
const match = bunkerUri.match(/^bunker:\/\/([^@]+)@([^?]+)(?:\?token=([^&]+))?$/);
if (!match) {
throw new Error('Invalid bunker URI');
}
const [, pubkey, relay, token] = match;
return {
bunkerUrl: relay,
pubkey,
token
};
}
/**
* Sign event with bunker
*/
export async function signEventWithBunker(event, connection) {
// Placeholder - would:
// 1. Send NIP-46 request to bunker
// 2. Wait for response
// 3. Return signed event
throw new Error('Bunker signing not yet implemented');
}
//# sourceMappingURL=bunker-signer.js.map

1
src/lib/services/auth/bunker-signer.js.map

@ -1 +0,0 @@
{"version":3,"file":"bunker-signer.js","sourceRoot":"","sources":["bunker-signer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAUH;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,SAAiB;IACnD,sBAAsB;IACtB,kDAAkD;IAClD,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,kDAAkD,CAAC,CAAC;IAClF,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;IACxC,CAAC;IAED,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,KAAK,CAAC;IAEvC,OAAO;QACL,SAAS,EAAE,KAAK;QAChB,MAAM;QACN,KAAK;KACN,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,KAAqC,EACrC,UAA4B;IAE5B,uBAAuB;IACvB,mCAAmC;IACnC,uBAAuB;IACvB,yBAAyB;IAEzB,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;AACxD,CAAC"}

39
src/lib/services/auth/nip07-signer.js

@ -1,39 +0,0 @@
/**
* NIP-07 signer (browser extension)
*/
/**
* Check if NIP-07 is available
*/
export function isNIP07Available() {
return typeof window !== 'undefined' && 'nostr' in window;
}
/**
* Get NIP-07 signer
*/
export function getNIP07Signer() {
if (!isNIP07Available())
return null;
const nostr = window.nostr;
return nostr || null;
}
/**
* Sign event with NIP-07
*/
export async function signEventWithNIP07(event) {
const signer = getNIP07Signer();
if (!signer) {
throw new Error('NIP-07 not available');
}
return signer.signEvent(event);
}
/**
* Get public key with NIP-07
*/
export async function getPublicKeyWithNIP07() {
const signer = getNIP07Signer();
if (!signer) {
throw new Error('NIP-07 not available');
}
return signer.getPublicKey();
}
//# sourceMappingURL=nip07-signer.js.map

1
src/lib/services/auth/nip07-signer.js.map

@ -1 +0,0 @@
{"version":3,"file":"nip07-signer.js","sourceRoot":"","sources":["nip07-signer.ts"],"names":[],"mappings":"AAAA;;GAEG;AASH;;GAEG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,IAAI,MAAM,CAAC;AAC5D,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc;IAC5B,IAAI,CAAC,gBAAgB,EAAE;QAAE,OAAO,IAAI,CAAC;IAErC,MAAM,KAAK,GAAI,MAAkC,CAAC,KAAK,CAAC;IACxD,OAAO,KAAK,IAAI,IAAI,CAAC;AACvB,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,KAAqC;IAErC,MAAM,MAAM,GAAG,cAAc,EAAE,CAAC;IAChC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;IAC1C,CAAC;IAED,OAAO,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;AACjC,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB;IACzC,MAAM,MAAM,GAAG,cAAc,EAAE,CAAC;IAChC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;IAC1C,CAAC;IAED,OAAO,MAAM,CAAC,YAAY,EAAE,CAAC;AAC/B,CAAC"}

62
src/lib/services/auth/nsec-signer.js

@ -1,62 +0,0 @@
/**
* Nsec signer (direct private key, NIP-49 encrypted)
*/
import { decryptPrivateKey } from '../security/key-management.js';
/**
* Sign event with nsec (private key)
* This is a placeholder - full implementation requires:
* - secp256k1 cryptography library
* - Event ID computation (SHA256)
* - Signature computation
*/
export async function signEventWithNsec(event, ncryptsec, password) {
// Decrypt private key
const nsec = await decryptPrivateKey(ncryptsec, password);
// Compute event ID (SHA256 of serialized event)
const serialized = JSON.stringify([
0,
event.pubkey,
event.created_at,
event.kind,
event.tags,
event.content
]);
const encoder = new TextEncoder();
const data = encoder.encode(serialized);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const id = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
// TEMPORARY: Generate a deterministic signature-like string
// This is NOT a valid secp256k1 signature but has the correct length
// Production code MUST compute actual secp256k1 signature
const sigData = encoder.encode(nsec + id);
const sigHashBuffer = await crypto.subtle.digest('SHA-256', sigData);
const sigHashArray = Array.from(new Uint8Array(sigHashBuffer));
const sigHash = sigHashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
// Double the hash to get 128 chars (64 * 2)
const sig = (sigHash + sigHash).slice(0, 128);
return {
...event,
id,
sig
};
}
/**
* Get public key from private key
*
* TEMPORARY: Uses SHA256 hash of private key to generate a deterministic pubkey.
* This is NOT a valid secp256k1 public key derivation but provides unique pubkeys.
* Production code MUST use proper secp256k1 point multiplication.
*/
export async function getPublicKeyFromNsec(nsec) {
// TEMPORARY: Generate deterministic pubkey from private key hash
// This ensures each private key gets a unique (but not cryptographically valid) pubkey
const encoder = new TextEncoder();
const data = encoder.encode(nsec);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hash = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
// Double the hash to get 64 chars (32 * 2)
return (hash + hash).slice(0, 64);
}
//# sourceMappingURL=nsec-signer.js.map

1
src/lib/services/auth/nsec-signer.js.map

@ -1 +0,0 @@
{"version":3,"file":"nsec-signer.js","sourceRoot":"","sources":["nsec-signer.ts"],"names":[],"mappings":"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"}

98
src/lib/services/auth/profile-fetcher.js

@ -1,98 +0,0 @@
/**
* Profile fetcher (kind 0 events)
*/
import { nostrClient } from '../nostr/applesauce-client.js';
import { cacheProfile, getProfile, getProfiles } from '../cache/profile-cache.js';
import { config } from '../nostr/config.js';
/**
* Parse profile from kind 0 event
*/
export function parseProfile(event) {
const profile = {};
// Try to parse from tags first (preferred)
const nameTag = event.tags.find((t) => t[0] === 'name');
if (nameTag && nameTag[1])
profile.name = nameTag[1];
const aboutTag = event.tags.find((t) => t[0] === 'about');
if (aboutTag && aboutTag[1])
profile.about = aboutTag[1];
const pictureTag = event.tags.find((t) => t[0] === 'picture');
if (pictureTag && pictureTag[1])
profile.picture = pictureTag[1];
// Multiple tags for website, nip05, lud16
profile.website = event.tags.filter((t) => t[0] === 'website').map((t) => t[1]).filter(Boolean);
profile.nip05 = event.tags.filter((t) => t[0] === 'nip05').map((t) => t[1]).filter(Boolean);
profile.lud16 = event.tags.filter((t) => t[0] === 'lud16').map((t) => t[1]).filter(Boolean);
// Fallback to JSON content if tags not found
if (!profile.name || !profile.about) {
try {
const json = JSON.parse(event.content);
if (json.name && !profile.name)
profile.name = json.name;
if (json.about && !profile.about)
profile.about = json.about;
if (json.picture && !profile.picture)
profile.picture = json.picture;
if (json.website && profile.website.length === 0) {
profile.website = Array.isArray(json.website) ? json.website : [json.website];
}
if (json.nip05 && profile.nip05.length === 0) {
profile.nip05 = Array.isArray(json.nip05) ? json.nip05 : [json.nip05];
}
if (json.lud16 && profile.lud16.length === 0) {
profile.lud16 = Array.isArray(json.lud16) ? json.lud16 : [json.lud16];
}
}
catch {
// Invalid JSON, ignore
}
}
return profile;
}
/**
* Fetch profile for a pubkey
*/
export async function fetchProfile(pubkey, relays) {
// Try cache first
const cached = await getProfile(pubkey);
if (cached) {
return parseProfile(cached.event);
}
// Fetch from relays
const relayList = relays || [
...config.defaultRelays,
...config.profileRelays
];
const events = await nostrClient.fetchEvents([{ kinds: [0], authors: [pubkey], limit: 1 }], relayList, { useCache: true, cacheResults: true });
if (events.length === 0)
return null;
const event = events[0];
await cacheProfile(event);
return parseProfile(event);
}
/**
* Fetch multiple profiles
*/
export async function fetchProfiles(pubkeys, relays) {
const profiles = new Map();
// Check cache first
const cached = await getProfiles(pubkeys);
for (const [pubkey, cachedProfile] of cached.entries()) {
profiles.set(pubkey, parseProfile(cachedProfile.event));
}
// Fetch missing profiles
const missing = pubkeys.filter((p) => !profiles.has(p));
if (missing.length === 0)
return profiles;
const relayList = relays || [
...config.defaultRelays,
...config.profileRelays
];
const events = await nostrClient.fetchEvents([{ kinds: [0], authors: missing, limit: 1 }], relayList, { useCache: true, cacheResults: true });
for (const event of events) {
await cacheProfile(event);
profiles.set(event.pubkey, parseProfile(event));
}
return profiles;
}
//# sourceMappingURL=profile-fetcher.js.map

1
src/lib/services/auth/profile-fetcher.js.map

@ -1 +0,0 @@
{"version":3,"file":"profile-fetcher.js","sourceRoot":"","sources":["profile-fetcher.ts"],"names":[],"mappings":"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"}

65
src/lib/services/auth/relay-list-fetcher.js

@ -1,65 +0,0 @@
/**
* Relay list fetcher (kind 10002 and 10432)
*/
import { nostrClient } from '../nostr/applesauce-client.js';
import { config } from '../nostr/config.js';
/**
* Parse relay list from event
*/
export function parseRelayList(event) {
const relays = [];
for (const tag of event.tags) {
if (tag[0] === 'r' && tag[1]) {
const url = tag[1];
const markers = tag.slice(2);
// If no markers, relay is both read and write
if (markers.length === 0) {
relays.push({ url, read: true, write: true });
continue;
}
// Check for explicit markers
const hasRead = markers.includes('read');
const hasWrite = markers.includes('write');
// If only 'read' marker: read=true, write=false
// If only 'write' marker: read=false, write=true
// If both or neither explicitly: both true (default behavior)
const read = hasRead || (!hasRead && !hasWrite);
const write = hasWrite || (!hasRead && !hasWrite);
relays.push({ url, read, write });
}
}
return relays;
}
/**
* Fetch relay lists for a pubkey (kind 10002 and 10432)
*/
export async function fetchRelayLists(pubkey, relays) {
const relayList = relays || [
...config.defaultRelays,
...config.profileRelays
];
// Fetch both kind 10002 and 10432
const events = await nostrClient.fetchEvents([
{ kinds: [10002], authors: [pubkey], limit: 1 },
{ kinds: [10432], authors: [pubkey], limit: 1 }
], relayList, { useCache: true, cacheResults: true });
const inbox = [];
const outbox = [];
for (const event of events) {
const relayInfos = parseRelayList(event);
for (const info of relayInfos) {
if (info.read && !inbox.includes(info.url)) {
inbox.push(info.url);
}
if (info.write && !outbox.includes(info.url)) {
outbox.push(info.url);
}
}
}
// Deduplicate
return {
inbox: [...new Set(inbox)],
outbox: [...new Set(outbox)]
};
}
//# sourceMappingURL=relay-list-fetcher.js.map

1
src/lib/services/auth/relay-list-fetcher.js.map

@ -1 +0,0 @@
{"version":3,"file":"relay-list-fetcher.js","sourceRoot":"","sources":["relay-list-fetcher.ts"],"names":[],"mappings":"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"}

99
src/lib/services/auth/session-manager.js

@ -1,99 +0,0 @@
/**
* Session manager for active user sessions
*/
// Simple store implementation for Svelte reactivity
function createStore(initial) {
let value = initial;
const subscribers = new Set();
return {
get value() {
return value;
},
set(newValue) {
value = newValue;
subscribers.forEach((fn) => fn(value));
},
subscribe(fn) {
subscribers.add(fn);
fn(value);
return () => subscribers.delete(fn);
}
};
}
class SessionManager {
currentSession = null;
session = createStore(null);
/**
* Set current session
*/
setSession(session) {
this.currentSession = session;
this.session.set(session);
// Store in localStorage for persistence
if (typeof window !== 'undefined') {
localStorage.setItem('aitherboard_session', JSON.stringify({
pubkey: session.pubkey,
method: session.method,
createdAt: session.createdAt
}));
}
}
/**
* Get current session
*/
getSession() {
return this.currentSession;
}
/**
* Check if user is logged in
*/
isLoggedIn() {
return this.currentSession !== null;
}
/**
* Get current pubkey
*/
getCurrentPubkey() {
return this.currentSession?.pubkey || null;
}
/**
* Sign event with current session
*/
async signEvent(event) {
if (!this.currentSession) {
throw new Error('No active session');
}
return this.currentSession.signer(event);
}
/**
* Clear session
*/
clearSession() {
this.currentSession = null;
this.session.set(null);
if (typeof window !== 'undefined') {
localStorage.removeItem('aitherboard_session');
}
}
/**
* Restore session from localStorage
*/
async restoreSession() {
if (typeof window === 'undefined')
return false;
const stored = localStorage.getItem('aitherboard_session');
if (!stored)
return false;
try {
const data = JSON.parse(stored);
// Session restoration would require re-initializing the signer
// This is simplified - full implementation would restore the signer
return false;
}
catch {
return false;
}
}
}
export const sessionManager = new SessionManager();
//# sourceMappingURL=session-manager.js.map

1
src/lib/services/auth/session-manager.js.map

@ -1 +0,0 @@
{"version":3,"file":"session-manager.js","sourceRoot":"","sources":["session-manager.ts"],"names":[],"mappings":"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"}

106
src/lib/services/auth/session-manager.ts

@ -41,16 +41,21 @@ class SessionManager {
/** /**
* Set current session * Set current session
*/ */
setSession(session: UserSession): void { setSession(session: UserSession, metadata?: Record<string, any>): void {
this.currentSession = session; this.currentSession = session;
this.session.set(session); this.session.set(session);
// Store in localStorage for persistence // Store in localStorage for persistence
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
localStorage.setItem('aitherboard_session', JSON.stringify({ const sessionData: any = {
pubkey: session.pubkey, pubkey: session.pubkey,
method: session.method, method: session.method,
createdAt: session.createdAt createdAt: session.createdAt
})); };
// Store method-specific metadata for restoration
if (metadata) {
sessionData.metadata = metadata;
}
localStorage.setItem('aitherboard_session', JSON.stringify(sessionData));
} }
} }
@ -99,6 +104,7 @@ class SessionManager {
/** /**
* Restore session from localStorage * Restore session from localStorage
* This will attempt to restore the session based on the auth method
*/ */
async restoreSession(): Promise<boolean> { async restoreSession(): Promise<boolean> {
if (typeof window === 'undefined') return false; if (typeof window === 'undefined') return false;
@ -108,13 +114,103 @@ class SessionManager {
try { try {
const data = JSON.parse(stored); const data = JSON.parse(stored);
// Session restoration would require re-initializing the signer const { pubkey, method, metadata } = data;
// This is simplified - full implementation would restore the signer
if (!pubkey || !method) return false;
// Import auth handlers dynamically to avoid circular dependencies
switch (method) {
case 'nip07': {
// For NIP-07, we can restore by checking if extension is still available
const { isNIP07Available, getPublicKeyWithNIP07, signEventWithNIP07 } = await import('./nip07-signer.js');
if (isNIP07Available()) {
try {
// Verify the extension still has the same pubkey
const currentPubkey = await getPublicKeyWithNIP07();
if (currentPubkey === pubkey) {
this.setSession({
pubkey,
method: 'nip07',
signer: signEventWithNIP07,
createdAt: data.createdAt || Date.now()
});
return true;
}
} catch {
// Extension error, can't restore
return false;
}
}
return false;
}
case 'bunker': {
// For bunker, restore if we have the bunker URI
if (metadata?.bunkerUri) {
const { connectBunker, signEventWithBunker } = await import('./bunker-signer.js');
try {
const connection = await connectBunker(metadata.bunkerUri);
if (connection.pubkey === pubkey) {
this.setSession({
pubkey,
method: 'bunker',
signer: async (event) => signEventWithBunker(event, connection),
createdAt: data.createdAt || Date.now()
}, { bunkerUri: metadata.bunkerUri });
return true;
}
} catch {
// Bunker connection failed
return false;
}
}
return false; return false;
}
case 'anonymous': {
// For anonymous, we can restore if the encrypted key is stored
// The key is stored in IndexedDB, we just need to verify it exists
const { getStoredAnonymousKey } = await import('./anonymous-signer.js');
// We can't restore without password, but we can check if key exists
// For now, we'll just restore the pubkey and let signer fail if password is wrong
// In practice, user would need to re-enter password
if (pubkey) {
// Check if key exists in storage
try {
// This will fail without password, but we can still restore session
// The signer will need password on first use
const { signEventWithAnonymous } = await import('./anonymous-signer.js');
this.setSession({
pubkey,
method: 'anonymous',
signer: async (event) => {
// This will fail without password - user needs to re-authenticate
throw new Error('Anonymous session requires password. Please log in again.');
},
createdAt: data.createdAt || Date.now()
});
// Note: This session won't work until user re-authenticates
return true;
} catch { } catch {
return false; return false;
} }
} }
return false;
}
case 'nsec': {
// nsec can't be restored without password (security)
// Clear the stored session
localStorage.removeItem('aitherboard_session');
return false;
}
default:
return false;
}
} catch (error) {
console.error('Error restoring session:', error);
// Clear corrupted session data
localStorage.removeItem('aitherboard_session');
return false;
}
}
} }
export const sessionManager = new SessionManager(); export const sessionManager = new SessionManager();

12
src/lib/services/auth/user-preferences-fetcher.js

@ -1,12 +0,0 @@
/**
* User preferences fetcher
* Placeholder for future user preference events
*/
/**
* Fetch user preferences
*/
export async function fetchUserPreferences(pubkey) {
// Placeholder - would fetch preference events
return null;
}
//# sourceMappingURL=user-preferences-fetcher.js.map

1
src/lib/services/auth/user-preferences-fetcher.js.map

@ -1 +0,0 @@
{"version":3,"file":"user-preferences-fetcher.js","sourceRoot":"","sources":["user-preferences-fetcher.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,MAAc;IACvD,8CAA8C;IAC9C,OAAO,IAAI,CAAC;AACd,CAAC"}

38
src/lib/services/auth/user-status-fetcher.js

@ -1,38 +0,0 @@
/**
* User status fetcher (kind 30315, NIP-38)
*/
import { nostrClient } from '../nostr/applesauce-client.js';
import { config } from '../nostr/config.js';
/**
* Parse user status from kind 30315 event
*/
export function parseUserStatus(event) {
if (event.kind !== 30315)
return null;
// Check for d tag with value "general"
const dTag = event.tags.find((t) => t[0] === 'd' && t[1] === 'general');
if (!dTag)
return null;
return event.content || null;
}
/**
* Fetch user status for a pubkey
*/
export async function fetchUserStatus(pubkey, relays) {
const relayList = relays || [
...config.defaultRelays,
...config.profileRelays
];
const events = await nostrClient.fetchEvents([
{
kinds: [30315],
authors: [pubkey],
'#d': ['general'],
limit: 1
}
], relayList, { useCache: true, cacheResults: true });
if (events.length === 0)
return null;
return parseUserStatus(events[0]);
}
//# sourceMappingURL=user-status-fetcher.js.map

1
src/lib/services/auth/user-status-fetcher.js.map

@ -1 +0,0 @@
{"version":3,"file":"user-status-fetcher.js","sourceRoot":"","sources":["user-status-fetcher.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAC5D,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAG5C;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,KAAiB;IAC/C,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IAEtC,uCAAuC;IACvC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;IACxE,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IAEvB,OAAO,KAAK,CAAC,OAAO,IAAI,IAAI,CAAC;AAC/B,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAc,EACd,MAAiB;IAEjB,MAAM,SAAS,GAAG,MAAM,IAAI;QAC1B,GAAG,MAAM,CAAC,aAAa;QACvB,GAAG,MAAM,CAAC,aAAa;KACxB,CAAC;IAEF,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,WAAW,CAC1C;QACE;YACE,KAAK,EAAE,CAAC,KAAK,CAAC;YACd,OAAO,EAAE,CAAC,MAAM,CAAC;YACjB,IAAI,EAAE,CAAC,SAAS,CAAC;YACjB,KAAK,EAAE,CAAC;SACT;KACF,EACD,SAAS,EACT,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CACvC,CAAC;IAEF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAErC,OAAO,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACpC,CAAC"}

52
src/lib/services/cache/anonymous-key-store.js vendored

@ -1,52 +0,0 @@
/**
* Anonymous key storage (NIP-49 encrypted)
*/
import { getDB } from './indexeddb-store.js';
import { encryptPrivateKey, decryptPrivateKey } from '../security/key-management.js';
/**
* Store an anonymous key (encrypted)
*/
export async function storeAnonymousKey(nsec, password, pubkey) {
const ncryptsec = await encryptPrivateKey(nsec, password);
const db = await getDB();
const stored = {
id: pubkey,
ncryptsec,
pubkey,
created_at: Date.now()
};
await db.put('keys', stored);
}
/**
* Retrieve and decrypt an anonymous key
*/
export async function getAnonymousKey(pubkey, password) {
const db = await getDB();
const stored = await db.get('keys', pubkey);
if (!stored)
return null;
const key = stored;
return decryptPrivateKey(key.ncryptsec, password);
}
/**
* List all stored anonymous keys (pubkeys only)
*/
export async function listAnonymousKeys() {
const db = await getDB();
const keys = [];
const tx = db.transaction('keys', 'readonly');
for await (const cursor of tx.store.iterate()) {
const key = cursor.value;
keys.push(key.pubkey);
}
await tx.done;
return keys;
}
/**
* Delete an anonymous key
*/
export async function deleteAnonymousKey(pubkey) {
const db = await getDB();
await db.delete('keys', pubkey);
}
//# sourceMappingURL=anonymous-key-store.js.map

1
src/lib/services/cache/anonymous-key-store.js.map vendored

@ -1 +0,0 @@
{"version":3,"file":"anonymous-key-store.js","sourceRoot":"","sources":["anonymous-key-store.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AASrF;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,IAAY,EACZ,QAAgB,EAChB,MAAc;IAEd,MAAM,SAAS,GAAG,MAAM,iBAAiB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC1D,MAAM,EAAE,GAAG,MAAM,KAAK,EAAE,CAAC;IACzB,MAAM,MAAM,GAAuB;QACjC,EAAE,EAAE,MAAM;QACV,SAAS;QACT,MAAM;QACN,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE;KACvB,CAAC;IACF,MAAM,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAC/B,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAc,EACd,QAAgB;IAEhB,MAAM,EAAE,GAAG,MAAM,KAAK,EAAE,CAAC;IACzB,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAEzB,MAAM,GAAG,GAAG,MAA4B,CAAC;IACzC,OAAO,iBAAiB,CAAC,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AACpD,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACrC,MAAM,EAAE,GAAG,MAAM,KAAK,EAAE,CAAC;IACzB,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,MAAM,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAE9C,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;QAC9C,MAAM,GAAG,GAAG,MAAM,CAAC,KAA2B,CAAC;QAC/C,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB,CAAC;IAED,MAAM,EAAE,CAAC,IAAI,CAAC;IACd,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,MAAc;IACrD,MAAM,EAAE,GAAG,MAAM,KAAK,EAAE,CAAC;IACzB,MAAM,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAClC,CAAC"}

88
src/lib/services/cache/event-cache.js vendored

@ -1,88 +0,0 @@
/**
* Event caching with IndexedDB
*/
import { getDB } from './indexeddb-store.js';
/**
* Store an event in cache
*/
export async function cacheEvent(event) {
const db = await getDB();
const cached = {
...event,
cached_at: Date.now()
};
await db.put('events', cached);
}
/**
* Store multiple events in cache
*/
export async function cacheEvents(events) {
const db = await getDB();
const tx = db.transaction('events', 'readwrite');
for (const event of events) {
const cached = {
...event,
cached_at: Date.now()
};
await tx.store.put(cached);
}
await tx.done;
}
/**
* Get event by ID from cache
*/
export async function getEvent(id) {
const db = await getDB();
return db.get('events', id);
}
/**
* Get events by kind
*/
export async function getEventsByKind(kind, limit) {
const db = await getDB();
const tx = db.transaction('events', 'readonly');
const index = tx.store.index('kind');
const events = [];
let count = 0;
for await (const cursor of index.iterate(kind)) {
if (limit && count >= limit)
break;
events.push(cursor.value);
count++;
}
await tx.done;
return events.sort((a, b) => b.created_at - a.created_at);
}
/**
* Get events by pubkey
*/
export async function getEventsByPubkey(pubkey, limit) {
const db = await getDB();
const tx = db.transaction('events', 'readonly');
const index = tx.store.index('pubkey');
const events = [];
let count = 0;
for await (const cursor of index.iterate(pubkey)) {
if (limit && count >= limit)
break;
events.push(cursor.value);
count++;
}
await tx.done;
return events.sort((a, b) => b.created_at - a.created_at);
}
/**
* Clear old events (older than specified timestamp)
*/
export async function clearOldEvents(olderThan) {
const db = await getDB();
const tx = db.transaction('events', 'readwrite');
const index = tx.store.index('created_at');
for await (const cursor of index.iterate()) {
if (cursor.value.created_at < olderThan) {
await cursor.delete();
}
}
await tx.done;
}
//# sourceMappingURL=event-cache.js.map

1
src/lib/services/cache/event-cache.js.map vendored

@ -1 +0,0 @@
{"version":3,"file":"event-cache.js","sourceRoot":"","sources":["event-cache.ts"],"names":[],"mappings":"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"}

20
src/lib/services/cache/event-cache.ts vendored

@ -87,6 +87,26 @@ export async function getEventsByPubkey(pubkey: string, limit?: number): Promise
return events.sort((a, b) => b.created_at - a.created_at); return events.sort((a, b) => b.created_at - a.created_at);
} }
/**
* Delete an event by ID from cache
*/
export async function deleteEvent(id: string): Promise<void> {
const db = await getDB();
await db.delete('events', id);
}
/**
* Delete multiple events by ID from cache
*/
export async function deleteEvents(ids: string[]): Promise<void> {
const db = await getDB();
const tx = db.transaction('events', 'readwrite');
for (const id of ids) {
await tx.store.delete(id);
}
await tx.done;
}
/** /**
* Clear old events (older than specified timestamp) * Clear old events (older than specified timestamp)
*/ */

48
src/lib/services/cache/indexeddb-store.js vendored

@ -1,48 +0,0 @@
/**
* Base IndexedDB store operations
*/
import { openDB } from 'idb';
const DB_NAME = 'aitherboard';
const DB_VERSION = 1;
let dbInstance = null;
/**
* Get or create database instance
*/
export async function getDB() {
if (dbInstance)
return dbInstance;
dbInstance = await openDB(DB_NAME, DB_VERSION, {
upgrade(db) {
// Events store
if (!db.objectStoreNames.contains('events')) {
const eventStore = db.createObjectStore('events', { keyPath: 'id' });
eventStore.createIndex('kind', 'kind', { unique: false });
eventStore.createIndex('pubkey', 'pubkey', { unique: false });
eventStore.createIndex('created_at', 'created_at', { unique: false });
}
// Profiles store
if (!db.objectStoreNames.contains('profiles')) {
db.createObjectStore('profiles', { keyPath: 'pubkey' });
}
// Keys store
if (!db.objectStoreNames.contains('keys')) {
db.createObjectStore('keys', { keyPath: 'id' });
}
// Search index store
if (!db.objectStoreNames.contains('search')) {
db.createObjectStore('search', { keyPath: 'id' });
}
}
});
return dbInstance;
}
/**
* Close database connection
*/
export async function closeDB() {
if (dbInstance) {
dbInstance.close();
dbInstance = null;
}
}
//# sourceMappingURL=indexeddb-store.js.map

1
src/lib/services/cache/indexeddb-store.js.map vendored

@ -1 +0,0 @@
{"version":3,"file":"indexeddb-store.js","sourceRoot":"","sources":["indexeddb-store.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,MAAM,EAAqB,MAAM,KAAK,CAAC;AAEhD,MAAM,OAAO,GAAG,aAAa,CAAC;AAC9B,MAAM,UAAU,GAAG,CAAC,CAAC;AAsBrB,IAAI,UAAU,GAAwC,IAAI,CAAC;AAE3D;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,KAAK;IACzB,IAAI,UAAU;QAAE,OAAO,UAAU,CAAC;IAElC,UAAU,GAAG,MAAM,MAAM,CAAiB,OAAO,EAAE,UAAU,EAAE;QAC7D,OAAO,CAAC,EAAE;YACR,eAAe;YACf,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC5C,MAAM,UAAU,GAAG,EAAE,CAAC,iBAAiB,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;gBACrE,UAAU,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;gBAC1D,UAAU,CAAC,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;gBAC9D,UAAU,CAAC,WAAW,CAAC,YAAY,EAAE,YAAY,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;YACxE,CAAC;YAED,iBAAiB;YACjB,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;gBAC9C,EAAE,CAAC,iBAAiB,CAAC,UAAU,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC1D,CAAC;YAED,aAAa;YACb,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC1C,EAAE,CAAC,iBAAiB,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YAClD,CAAC;YAED,qBAAqB;YACrB,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC5C,EAAE,CAAC,iBAAiB,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACpD,CAAC;QACH,CAAC;KACF,CAAC,CAAC;IAEH,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO;IAC3B,IAAI,UAAU,EAAE,CAAC;QACf,UAAU,CAAC,KAAK,EAAE,CAAC;QACnB,UAAU,GAAG,IAAI,CAAC;IACpB,CAAC;AACH,CAAC"}

42
src/lib/services/cache/profile-cache.js vendored

@ -1,42 +0,0 @@
/**
* Profile caching (kind 0 events)
*/
import { getDB } from './indexeddb-store.js';
/**
* Store a profile in cache
*/
export async function cacheProfile(event) {
if (event.kind !== 0)
throw new Error('Not a profile event');
const db = await getDB();
const cached = {
pubkey: event.pubkey,
event,
cached_at: Date.now()
};
await db.put('profiles', cached);
}
/**
* Get profile by pubkey from cache
*/
export async function getProfile(pubkey) {
const db = await getDB();
return db.get('profiles', pubkey);
}
/**
* Get multiple profiles
*/
export async function getProfiles(pubkeys) {
const db = await getDB();
const profiles = new Map();
const tx = db.transaction('profiles', 'readonly');
for (const pubkey of pubkeys) {
const profile = await tx.store.get(pubkey);
if (profile) {
profiles.set(pubkey, profile);
}
}
await tx.done;
return profiles;
}
//# sourceMappingURL=profile-cache.js.map

1
src/lib/services/cache/profile-cache.js.map vendored

@ -1 +0,0 @@
{"version":3,"file":"profile-cache.js","sourceRoot":"","sources":["profile-cache.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAS7C;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,KAAiB;IAClD,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;IAC7D,MAAM,EAAE,GAAG,MAAM,KAAK,EAAE,CAAC;IACzB,MAAM,MAAM,GAAkB;QAC5B,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,KAAK;QACL,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;KACtB,CAAC;IACF,MAAM,EAAE,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AACnC,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,MAAc;IAC7C,MAAM,EAAE,GAAG,MAAM,KAAK,EAAE,CAAC;IACzB,OAAO,EAAE,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AACpC,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAiB;IACjD,MAAM,EAAE,GAAG,MAAM,KAAK,EAAE,CAAC;IACzB,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAyB,CAAC;IAClD,MAAM,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;IAElD,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC3C,IAAI,OAAO,EAAE,CAAC;YACZ,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAChC,CAAC;IACH,CAAC;IAED,MAAM,EAAE,CAAC,IAAI,CAAC;IACd,OAAO,QAAQ,CAAC;AAClB,CAAC"}

43
src/lib/services/cache/search-index.js vendored

@ -1,43 +0,0 @@
/**
* Full-text search index (deferred implementation)
*/
import { getDB } from './indexeddb-store.js';
/**
* Index event content for search
*/
export async function indexEvent(eventId, content) {
// Placeholder - full implementation would:
// 1. Tokenize content
// 2. Create inverted index
// 3. Store in IndexedDB
const db = await getDB();
await db.put('search', {
id: eventId,
content: content.toLowerCase()
});
}
/**
* Search events by query
*/
export async function searchEvents(query, limit = 50) {
// Placeholder - full implementation would:
// 1. Tokenize query
// 2. Look up in inverted index
// 3. Rank results
// 4. Return event IDs
const db = await getDB();
const results = [];
const lowerQuery = query.toLowerCase();
const tx = db.transaction('search', 'readonly');
for await (const cursor of tx.store.iterate()) {
if (results.length >= limit)
break;
const content = cursor.value.content;
if (content.includes(lowerQuery)) {
results.push(cursor.key);
}
}
await tx.done;
return results;
}
//# sourceMappingURL=search-index.js.map

1
src/lib/services/cache/search-index.js.map vendored

@ -1 +0,0 @@
{"version":3,"file":"search-index.js","sourceRoot":"","sources":["search-index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAE7C;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,OAAe,EAAE,OAAe;IAC/D,2CAA2C;IAC3C,sBAAsB;IACtB,2BAA2B;IAC3B,wBAAwB;IACxB,MAAM,EAAE,GAAG,MAAM,KAAK,EAAE,CAAC;IACzB,MAAM,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE;QACrB,EAAE,EAAE,OAAO;QACX,OAAO,EAAE,OAAO,CAAC,WAAW,EAAE;KAC/B,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,KAAa,EAAE,QAAgB,EAAE;IAClE,2CAA2C;IAC3C,oBAAoB;IACpB,+BAA+B;IAC/B,kBAAkB;IAClB,sBAAsB;IACtB,MAAM,EAAE,GAAG,MAAM,KAAK,EAAE,CAAC;IACzB,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,UAAU,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;IACvC,MAAM,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAEhD,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;QAC9C,IAAI,OAAO,CAAC,MAAM,IAAI,KAAK;YAAE,MAAM;QACnC,MAAM,OAAO,GAAI,MAAM,CAAC,KAA6B,CAAC,OAAO,CAAC;QAC9D,IAAI,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;YACjC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,GAAa,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IAED,MAAM,EAAE,CAAC,IAAI,CAAC;IACd,OAAO,OAAO,CAAC;AACjB,CAAC"}

101
src/lib/services/nostr/applesauce-client.js

@ -1,101 +0,0 @@
/**
* Applesauce-core client wrapper
* Main interface for Nostr operations
*/
import { initializeRelayPool, relayPool } from './relay-pool.js';
import { subscriptionManager } from './subscription-manager.js';
import { eventStore } from './event-store.js';
import { config } from './config.js';
class ApplesauceClient {
initialized = false;
/**
* Initialize the client
*/
async initialize() {
if (this.initialized)
return;
await initializeRelayPool();
this.initialized = true;
}
/**
* Publish an event to relays
*/
async publish(event, options = {}) {
const relays = options.relays || relayPool.getConnectedRelays();
const message = JSON.stringify(['EVENT', event]);
const results = {
success: [],
failed: []
};
for (const relay of relays) {
try {
const sent = relayPool.send(relay, message);
if (sent) {
results.success.push(relay);
}
else {
results.failed.push({ relay, error: 'Not connected' });
}
}
catch (error) {
results.failed.push({
relay,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
// Store in cache
if (results.success.length > 0) {
await eventStore.storeEvent(event);
}
return results;
}
/**
* Subscribe to events
*/
subscribe(filters, relays, onEvent, onEose) {
const subId = subscriptionManager.generateSubId();
subscriptionManager.subscribe(subId, relays, filters, onEvent, onEose);
return subId;
}
/**
* Unsubscribe
*/
unsubscribe(subId) {
subscriptionManager.unsubscribe(subId);
}
/**
* Fetch events
*/
async fetchEvents(filters, relays, options) {
return eventStore.fetchEvents(filters, relays, options || {});
}
/**
* Get event by ID
*/
async getEventById(id, relays) {
return eventStore.getEventById(id, relays);
}
/**
* Get relay pool
*/
getRelayPool() {
return relayPool;
}
/**
* Get config
*/
getConfig() {
return config;
}
/**
* Close all connections
*/
close() {
subscriptionManager.closeAll();
relayPool.closeAll();
this.initialized = false;
}
}
export const nostrClient = new ApplesauceClient();
//# sourceMappingURL=applesauce-client.js.map

1
src/lib/services/nostr/applesauce-client.js.map

@ -1 +0,0 @@
{"version":3,"file":"applesauce-client.js","sourceRoot":"","sources":["applesauce-client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAQrC,MAAM,gBAAgB;IACZ,WAAW,GAAG,KAAK,CAAC;IAE5B;;OAEG;IACH,KAAK,CAAC,UAAU;QACd,IAAI,IAAI,CAAC,WAAW;YAAE,OAAO;QAE7B,MAAM,mBAAmB,EAAE,CAAC;QAC5B,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;IAC1B,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,OAAO,CAAC,KAAiB,EAAE,UAA0B,EAAE;QAI3D,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,SAAS,CAAC,kBAAkB,EAAE,CAAC;QAChE,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;QAEjD,MAAM,OAAO,GAAG;YACd,OAAO,EAAE,EAAc;YACvB,MAAM,EAAE,EAA6C;SACtD,CAAC;QAEF,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;gBAC5C,IAAI,IAAI,EAAE,CAAC;oBACT,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBAC9B,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAC;gBACzD,CAAC;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC;oBAClB,KAAK;oBACL,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe;iBAChE,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,iBAAiB;QACjB,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC/B,MAAM,UAAU,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACrC,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;OAEG;IACH,SAAS,CACP,OASE,EACF,MAAgB,EAChB,OAAmD,EACnD,MAAgC;QAEhC,MAAM,KAAK,GAAG,mBAAmB,CAAC,aAAa,EAAE,CAAC;QAClD,mBAAmB,CAAC,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QACvE,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;OAEG;IACH,WAAW,CAAC,KAAa;QACvB,mBAAmB,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACzC,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,WAAW,CACf,OASE,EACF,MAAgB,EAChB,OAAwD;QAExD,OAAO,UAAU,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC;IAChE,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,YAAY,CAAC,EAAU,EAAE,MAAgB;QAC7C,OAAO,UAAU,CAAC,YAAY,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IAC7C,CAAC;IAED;;OAEG;IACH,YAAY;QACV,OAAO,SAAS,CAAC;IACnB,CAAC;IAED;;OAEG;IACH,SAAS;QACP,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;OAEG;IACH,KAAK;QACH,mBAAmB,CAAC,QAAQ,EAAE,CAAC;QAC/B,SAAS,CAAC,QAAQ,EAAE,CAAC;QACrB,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;IAC3B,CAAC;CACF;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,gBAAgB,EAAE,CAAC"}

7
src/lib/services/nostr/applesauce-client.ts

@ -5,7 +5,7 @@
import { initializeRelayPool, relayPool } from './relay-pool.js'; import { initializeRelayPool, relayPool } from './relay-pool.js';
import { subscriptionManager } from './subscription-manager.js'; import { subscriptionManager } from './subscription-manager.js';
import { eventStore } from './event-store.js'; import { eventStore, warmupCaches } from './event-store.js';
import { config } from './config.js'; import { config } from './config.js';
import type { NostrEvent } from '../../types/nostr.js'; import type { NostrEvent } from '../../types/nostr.js';
@ -25,6 +25,9 @@ class ApplesauceClient {
await initializeRelayPool(); await initializeRelayPool();
this.initialized = true; this.initialized = true;
// Warm up caches in background (non-blocking)
warmupCaches();
} }
/** /**
@ -111,7 +114,7 @@ class ApplesauceClient {
limit?: number; limit?: number;
}>, }>,
relays: string[], relays: string[],
options?: { useCache?: boolean; cacheResults?: boolean } options?: { useCache?: boolean; cacheResults?: boolean; onUpdate?: (events: NostrEvent[]) => void }
): Promise<NostrEvent[]> { ): Promise<NostrEvent[]> {
return eventStore.fetchEvents(filters, relays, options || {}); return eventStore.fetchEvents(filters, relays, options || {});
} }

119
src/lib/services/nostr/auth-handler.js

@ -1,119 +0,0 @@
/**
* Unified authentication handler
*/
import { signEventWithNIP07, getPublicKeyWithNIP07 } from '../auth/nip07-signer.js';
import { signEventWithNsec, getPublicKeyFromNsec } from '../auth/nsec-signer.js';
import { signEventWithBunker, connectBunker } from '../auth/bunker-signer.js';
import { signEventWithAnonymous, generateAnonymousKey } from '../auth/anonymous-signer.js';
import { decryptPrivateKey } from '../security/key-management.js';
import { sessionManager } from '../auth/session-manager.js';
import { fetchRelayLists } from '../auth/relay-list-fetcher.js';
import { eventStore } from './event-store.js';
import { nostrClient } from './applesauce-client.js';
/**
* Authenticate with NIP-07
*/
export async function authenticateWithNIP07() {
const pubkey = await getPublicKeyWithNIP07();
sessionManager.setSession({
pubkey,
method: 'nip07',
signer: signEventWithNIP07,
createdAt: Date.now()
});
// Fetch user relay lists and mute list
await loadUserPreferences(pubkey);
return pubkey;
}
/**
* Authenticate with nsec
*/
export async function authenticateWithNsec(ncryptsec, password) {
// Decrypt the encrypted private key
const nsec = await decryptPrivateKey(ncryptsec, password);
// Derive public key from private key
const pubkey = await getPublicKeyFromNsec(nsec);
sessionManager.setSession({
pubkey,
method: 'nsec',
signer: async (event) => signEventWithNsec(event, ncryptsec, password),
createdAt: Date.now()
});
await loadUserPreferences(pubkey);
return pubkey;
}
/**
* Authenticate with bunker
*/
export async function authenticateWithBunker(bunkerUri) {
const connection = await connectBunker(bunkerUri);
sessionManager.setSession({
pubkey: connection.pubkey,
method: 'bunker',
signer: async (event) => signEventWithBunker(event, connection),
createdAt: Date.now()
});
await loadUserPreferences(connection.pubkey);
return connection.pubkey;
}
/**
* Authenticate as anonymous
*/
export async function authenticateAsAnonymous(password) {
const { pubkey, nsec } = await generateAnonymousKey(password);
// Store the key for later use
// In practice, we'd need to store the ncryptsec and decrypt when needed
// For now, this is simplified
sessionManager.setSession({
pubkey,
method: 'anonymous',
signer: async (event) => {
// Simplified - would decrypt and sign
return signEventWithAnonymous(event, pubkey, password);
},
createdAt: Date.now()
});
return pubkey;
}
/**
* Load user preferences (relay lists, mute list, blocked relays)
*/
async function loadUserPreferences(pubkey) {
// Fetch relay lists
const { inbox, outbox } = await fetchRelayLists(pubkey);
// Relay lists would be used by relay selection logic
// Fetch mute list (kind 10000)
const muteEvents = await nostrClient.fetchEvents([{ kinds: [10000], authors: [pubkey], limit: 1 }], [...nostrClient.getConfig().defaultRelays, ...nostrClient.getConfig().profileRelays], { useCache: true, cacheResults: true });
if (muteEvents.length > 0) {
const mutedPubkeys = muteEvents[0].tags
.filter((t) => t[0] === 'p')
.map((t) => t[1])
.filter(Boolean);
eventStore.setMuteList(mutedPubkeys);
}
// Fetch blocked relays (kind 10006)
const blockedRelayEvents = await nostrClient.fetchEvents([{ kinds: [10006], authors: [pubkey], limit: 1 }], [...nostrClient.getConfig().defaultRelays, ...nostrClient.getConfig().profileRelays], { useCache: true, cacheResults: true });
if (blockedRelayEvents.length > 0) {
const blockedRelays = blockedRelayEvents[0].tags
.filter((t) => t[0] === 'relay')
.map((t) => t[1])
.filter(Boolean);
eventStore.setBlockedRelays(blockedRelays);
}
}
/**
* Sign and publish event
*/
export async function signAndPublish(event, relays) {
const signed = await sessionManager.signEvent(event);
return nostrClient.publish(signed, { relays });
}
/**
* Logout
*/
export function logout() {
sessionManager.clearSession();
eventStore.setMuteList([]);
eventStore.setBlockedRelays([]);
}
//# sourceMappingURL=auth-handler.js.map

1
src/lib/services/nostr/auth-handler.js.map

@ -1 +0,0 @@
{"version":3,"file":"auth-handler.js","sourceRoot":"","sources":["auth-handler.ts"],"names":[],"mappings":"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"}

4
src/lib/services/nostr/auth-handler.ts

@ -27,7 +27,7 @@ export async function authenticateWithNIP07(): Promise<string> {
method: 'nip07', method: 'nip07',
signer: signEventWithNIP07, signer: signEventWithNIP07,
createdAt: Date.now() createdAt: Date.now()
}); }, {}); // No metadata needed for NIP-07
// Fetch user relay lists and mute list // Fetch user relay lists and mute list
await loadUserPreferences(pubkey); await loadUserPreferences(pubkey);
@ -71,7 +71,7 @@ export async function authenticateWithBunker(bunkerUri: string): Promise<string>
method: 'bunker', method: 'bunker',
signer: async (event) => signEventWithBunker(event, connection), signer: async (event) => signEventWithBunker(event, connection),
createdAt: Date.now() createdAt: Date.now()
}); }, { bunkerUri }); // Store bunker URI for restoration
await loadUserPreferences(connection.pubkey); await loadUserPreferences(connection.pubkey);

49
src/lib/services/nostr/config.js

@ -1,49 +0,0 @@
/**
* Configuration for Nostr services
* Handles environment variables and defaults
*/
const DEFAULT_RELAYS = [
'wss://theforest.nostr1.com',
'wss://nostr21.com',
'wss://nostr.land',
'wss://nostr.wine',
'wss://nostr.sovbit.host'
];
const PROFILE_RELAYS = [
'wss://relay.damus.io',
'wss://aggr.nostr.land',
'wss://profiles.nostr1.com'
];
function parseRelays(envVar, fallback) {
if (!envVar)
return fallback;
const relays = envVar
.split(',')
.map((r) => r.trim())
.filter((r) => r.length > 0);
return relays.length > 0 ? relays : fallback;
}
function parseIntEnv(envVar, fallback, min = 0) {
if (!envVar)
return fallback;
const parsed = parseInt(envVar, 10);
if (isNaN(parsed) || parsed < min)
return fallback;
return parsed;
}
function parseBoolEnv(envVar, fallback) {
if (!envVar)
return fallback;
return envVar.toLowerCase() === 'true' || envVar === '1';
}
export function getConfig() {
return {
defaultRelays: parseRelays(import.meta.env.VITE_DEFAULT_RELAYS, DEFAULT_RELAYS),
profileRelays: PROFILE_RELAYS,
zapThreshold: parseIntEnv(import.meta.env.VITE_ZAP_THRESHOLD, 1, 0),
threadTimeoutDays: parseIntEnv(import.meta.env.VITE_THREAD_TIMEOUT_DAYS, 30),
pwaEnabled: parseBoolEnv(import.meta.env.VITE_PWA_ENABLED, true)
};
}
export const config = getConfig();
//# sourceMappingURL=config.js.map

1
src/lib/services/nostr/config.js.map

@ -1 +0,0 @@
{"version":3,"file":"config.js","sourceRoot":"","sources":["config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,cAAc,GAAG;IACrB,4BAA4B;IAC5B,mBAAmB;IACnB,kBAAkB;IAClB,kBAAkB;IAClB,yBAAyB;CAC1B,CAAC;AAEF,MAAM,cAAc,GAAG;IACrB,sBAAsB;IACtB,uBAAuB;IACvB,2BAA2B;CAC5B,CAAC;AAUF,SAAS,WAAW,CAAC,MAA0B,EAAE,QAAkB;IACjE,IAAI,CAAC,MAAM;QAAE,OAAO,QAAQ,CAAC;IAC7B,MAAM,MAAM,GAAG,MAAM;SAClB,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC/B,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC/C,CAAC;AAED,SAAS,WAAW,CAAC,MAA0B,EAAE,QAAgB,EAAE,MAAc,CAAC;IAChF,IAAI,CAAC,MAAM;QAAE,OAAO,QAAQ,CAAC;IAC7B,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACpC,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,GAAG;QAAE,OAAO,QAAQ,CAAC;IACnD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,YAAY,CAAC,MAA0B,EAAE,QAAiB;IACjE,IAAI,CAAC,MAAM;QAAE,OAAO,QAAQ,CAAC;IAC7B,OAAO,MAAM,CAAC,WAAW,EAAE,KAAK,MAAM,IAAI,MAAM,KAAK,GAAG,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,OAAO;QACL,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,mBAAmB,EAAE,cAAc,CAAC;QAC/E,aAAa,EAAE,cAAc;QAC7B,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,kBAAkB,EAAE,CAAC,EAAE,CAAC,CAAC;QACnE,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,wBAAwB,EAAE,EAAE,CAAC;QAC5E,UAAU,EAAE,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,gBAAgB,EAAE,IAAI,CAAC;KACjE,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC"}

213
src/lib/services/nostr/event-store.js

@ -1,213 +0,0 @@
/**
* Event store with IndexedDB caching and filtering
*/
import { cacheEvent, cacheEvents, getEvent, getEventsByKind, getEventsByPubkey } from '../cache/event-cache.js';
import { subscriptionManager } from './subscription-manager.js';
class EventStore {
muteList = new Set();
blockedRelays = new Set();
activityTracker = new Map(); // pubkey -> last activity timestamp
/**
* Update mute list
*/
setMuteList(pubkeys) {
this.muteList = new Set(pubkeys);
}
/**
* Update blocked relays
*/
setBlockedRelays(relays) {
this.blockedRelays = new Set(relays);
}
/**
* Filter out muted events
*/
isMuted(event) {
return this.muteList.has(event.pubkey);
}
/**
* Filter out blocked relays
*/
filterBlockedRelays(relays) {
return relays.filter((r) => !this.blockedRelays.has(r));
}
/**
* Track activity for a pubkey
*/
trackActivity(pubkey, timestamp) {
const current = this.activityTracker.get(pubkey) || 0;
if (timestamp > current) {
this.activityTracker.set(pubkey, timestamp);
}
}
/**
* Get last activity timestamp for a pubkey
*/
getLastActivity(pubkey) {
return this.activityTracker.get(pubkey);
}
/**
* Check if event should be hidden (content filtering)
*/
shouldHideEvent(event) {
// Check for content-warning or sensitive tags
const hasContentWarning = event.tags.some((t) => t[0] === 'content-warning' || t[0] === 'sensitive');
if (hasContentWarning)
return true;
// Check for #NSFW in content or tags
const content = event.content.toLowerCase();
const hasNSFW = content.includes('#nsfw') || event.tags.some((t) => t[1]?.toLowerCase() === 'nsfw');
if (hasNSFW)
return true;
return false;
}
/**
* Fetch events with filters
*/
async fetchEvents(filters, relays, options = {}) {
const { useCache = true, cacheResults = true } = options;
// Filter out blocked relays
const filteredRelays = this.filterBlockedRelays(relays);
// Try cache first if enabled
if (useCache) {
// Simple cache lookup - could be improved
const cachedEvents = [];
for (const filter of filters) {
if (filter.kinds && filter.kinds.length === 1) {
const events = await getEventsByKind(filter.kinds[0], filter.limit);
cachedEvents.push(...events);
}
if (filter.authors && filter.authors.length === 1) {
const events = await getEventsByPubkey(filter.authors[0], filter.limit);
cachedEvents.push(...events);
}
}
if (cachedEvents.length > 0) {
// Return cached events immediately (progressive loading)
// Continue fetching fresh data in background
this.fetchEventsFromRelays(filters, filteredRelays, { cacheResults }).catch((error) => {
console.error('Error fetching fresh events from relays:', error);
});
return this.filterEvents(cachedEvents);
}
}
// Fetch from relays
return this.fetchEventsFromRelays(filters, filteredRelays, { cacheResults });
}
/**
* Fetch events from relays
*/
async fetchEventsFromRelays(filters, relays, options) {
return new Promise((resolve, reject) => {
const events = new Map();
const subId = subscriptionManager.generateSubId();
const relayCount = new Set();
let resolved = false;
let eoseTimeout = null;
let timeoutId = null;
const finish = (eventArray) => {
if (resolved)
return;
resolved = true;
// Clean up timeouts
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
if (eoseTimeout) {
clearTimeout(eoseTimeout);
eoseTimeout = null;
}
subscriptionManager.unsubscribe(subId);
resolve(this.filterEvents(eventArray));
};
const onEvent = (event, relay) => {
// Skip muted events
if (this.isMuted(event))
return;
// Skip hidden events
if (this.shouldHideEvent(event))
return;
// Track activity
this.trackActivity(event.pubkey, event.created_at);
// Deduplicate by event ID
events.set(event.id, event);
relayCount.add(relay);
};
const onEose = (relay) => {
relayCount.add(relay);
// Wait a bit for all relays to respond
if (eoseTimeout) {
clearTimeout(eoseTimeout);
}
eoseTimeout = setTimeout(() => {
if (!resolved && relayCount.size >= Math.min(relays.length, 3)) {
// Got responses from enough relays
const eventArray = Array.from(events.values());
if (options.cacheResults) {
cacheEvents(eventArray).catch((error) => {
console.error('Error caching events:', error);
});
}
finish(eventArray);
}
}, 1000);
};
try {
subscriptionManager.subscribe(subId, relays, filters, onEvent, onEose);
}
catch (error) {
reject(error);
return;
}
// Timeout after 10 seconds
timeoutId = setTimeout(() => {
if (!resolved) {
const eventArray = Array.from(events.values());
if (options.cacheResults) {
cacheEvents(eventArray).catch((error) => {
console.error('Error caching events:', error);
});
}
finish(eventArray);
}
}, 10000);
});
}
/**
* Filter events (remove muted, hidden, etc.)
*/
filterEvents(events) {
return events.filter((event) => {
if (this.isMuted(event))
return false;
if (this.shouldHideEvent(event))
return false;
return true;
});
}
/**
* Get event by ID (from cache or fetch)
*/
async getEventById(id, relays) {
// Try cache first
const cached = await getEvent(id);
if (cached)
return cached;
// Fetch from relays
const filters = [{ ids: [id] }];
const events = await this.fetchEvents(filters, relays, { useCache: false });
return events[0] || null;
}
/**
* Store event in cache
*/
async storeEvent(event) {
if (this.isMuted(event) || this.shouldHideEvent(event))
return;
this.trackActivity(event.pubkey, event.created_at);
await cacheEvent(event);
}
}
export const eventStore = new EventStore();
//# sourceMappingURL=event-store.js.map

1
src/lib/services/nostr/event-store.js.map

File diff suppressed because one or more lines are too long

267
src/lib/services/nostr/event-store.ts

@ -2,7 +2,7 @@
* Event store with IndexedDB caching and filtering * Event store with IndexedDB caching and filtering
*/ */
import { cacheEvent, cacheEvents, getEvent, getEventsByKind, getEventsByPubkey } from '../cache/event-cache.js'; import { cacheEvent, cacheEvents, getEvent, getEventsByKind, getEventsByPubkey, deleteEvents } from '../cache/event-cache.js';
import { subscriptionManager } from './subscription-manager.js'; import { subscriptionManager } from './subscription-manager.js';
import { relayPool } from './relay-pool.js'; import { relayPool } from './relay-pool.js';
import type { NostrEvent, NostrFilter } from '../../types/nostr.js'; import type { NostrEvent, NostrFilter } from '../../types/nostr.js';
@ -16,6 +16,8 @@ class EventStore {
private muteList: Set<string> = new Set(); private muteList: Set<string> = new Set();
private blockedRelays: Set<string> = new Set(); private blockedRelays: Set<string> = new Set();
private activityTracker: Map<string, number> = new Map(); // pubkey -> last activity timestamp private activityTracker: Map<string, number> = new Map(); // pubkey -> last activity timestamp
private initialized = false;
private deletionProcessorInterval: ReturnType<typeof setInterval> | null = null;
/** /**
* Update mute list * Update mute list
@ -55,11 +57,58 @@ class EventStore {
} }
} }
/**
* Initialize activity tracker from cached events
*/
private async initializeActivityTracker(): Promise<void> {
if (this.initialized) return;
this.initialized = true;
try {
// Load recent events from cache to populate activity tracker
// We'll check the most recent events for each pubkey
// Include all event kinds that indicate user activity
const recentEvents = await getEventsByKind(1, 1000); // Get recent kind 1 events (notes)
const threadEvents = await getEventsByKind(11, 1000); // Get recent thread events
const commentEvents = await getEventsByKind(1111, 1000); // Get recent comment events
const reactionEvents = await getEventsByKind(7, 1000).catch(() => []); // Get recent reactions (kind 7)
const profileEvents = await getEventsByKind(0, 1000).catch(() => []); // Get recent profile updates (kind 0)
const allEvents = [...recentEvents, ...threadEvents, ...commentEvents, ...reactionEvents, ...profileEvents];
// Update activity tracker with the most recent event per pubkey
for (const event of allEvents) {
this.trackActivity(event.pubkey, event.created_at);
}
} catch (error) {
console.error('Error initializing activity tracker:', error);
}
}
/** /**
* Get last activity timestamp for a pubkey * Get last activity timestamp for a pubkey
*/ */
getLastActivity(pubkey: string): number | undefined { async getLastActivity(pubkey: string): Promise<number | undefined> {
return this.activityTracker.get(pubkey); // Initialize from cache if not done yet
await this.initializeActivityTracker();
// Check in-memory tracker first
const inMemory = this.activityTracker.get(pubkey);
if (inMemory) return inMemory;
// If not in memory, check cache for this specific pubkey
try {
const events = await getEventsByPubkey(pubkey, 1); // Get most recent event
if (events.length > 0) {
const latestEvent = events[0];
this.trackActivity(pubkey, latestEvent.created_at);
return latestEvent.created_at;
}
} catch (error) {
console.error('Error fetching activity from cache:', error);
}
return undefined;
} }
/** /**
@ -80,44 +129,57 @@ class EventStore {
/** /**
* Fetch events with filters * Fetch events with filters
* Returns cached data immediately, then fetches from relays in background
*/ */
async fetchEvents( async fetchEvents(
filters: NostrFilter[], filters: NostrFilter[],
relays: string[], relays: string[],
options: { useCache?: boolean; cacheResults?: boolean } = {} options: { useCache?: boolean; cacheResults?: boolean; onUpdate?: (events: NostrEvent[]) => void } = {}
): Promise<NostrEvent[]> { ): Promise<NostrEvent[]> {
const { useCache = true, cacheResults = true } = options; const { useCache = true, cacheResults = true, onUpdate } = options;
// Filter out blocked relays // Filter out blocked relays
const filteredRelays = this.filterBlockedRelays(relays); const filteredRelays = this.filterBlockedRelays(relays);
// Try cache first if enabled // Try cache first if enabled - return immediately
if (useCache) { if (useCache) {
// Simple cache lookup - could be improved
const cachedEvents: NostrEvent[] = []; const cachedEvents: NostrEvent[] = [];
for (const filter of filters) { for (const filter of filters) {
if (filter.kinds && filter.kinds.length === 1) { if (filter.kinds && filter.kinds.length === 1) {
const events = await getEventsByKind(filter.kinds[0], filter.limit); const events = await getEventsByKind(filter.kinds[0], filter.limit || 50);
cachedEvents.push(...events); cachedEvents.push(...events);
} }
if (filter.authors && filter.authors.length === 1) { if (filter.authors && filter.authors.length === 1) {
const events = await getEventsByPubkey(filter.authors[0], filter.limit); const events = await getEventsByPubkey(filter.authors[0], filter.limit || 50);
cachedEvents.push(...events); cachedEvents.push(...events);
} }
// Handle multiple kinds
if (filter.kinds && filter.kinds.length > 1) {
for (const kind of filter.kinds) {
const events = await getEventsByKind(kind, filter.limit || 50);
cachedEvents.push(...events);
} }
}
}
// Return cached events immediately (non-blocking)
const filteredCached = this.filterEvents(cachedEvents);
if (cachedEvents.length > 0) { // Fetch fresh data from relays in background (non-blocking)
// Return cached events immediately (progressive loading) if (cacheResults) {
// Continue fetching fresh data in background // Use setTimeout to ensure this doesn't block the return
this.fetchEventsFromRelays(filters, filteredRelays, { cacheResults }).catch((error) => { setTimeout(() => {
this.fetchEventsFromRelays(filters, filteredRelays, { cacheResults, onUpdate }).catch((error) => {
console.error('Error fetching fresh events from relays:', error); console.error('Error fetching fresh events from relays:', error);
}); });
return this.filterEvents(cachedEvents); }, 0);
} }
return filteredCached;
} }
// Fetch from relays // No cache - fetch from relays
return this.fetchEventsFromRelays(filters, filteredRelays, { cacheResults }); return this.fetchEventsFromRelays(filters, filteredRelays, { cacheResults, onUpdate });
} }
/** /**
@ -126,7 +188,7 @@ class EventStore {
private async fetchEventsFromRelays( private async fetchEventsFromRelays(
filters: NostrFilter[], filters: NostrFilter[],
relays: string[], relays: string[],
options: { cacheResults: boolean } options: { cacheResults: boolean; onUpdate?: (events: NostrEvent[]) => void }
): Promise<NostrEvent[]> { ): Promise<NostrEvent[]> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const events: Map<string, NostrEvent> = new Map(); const events: Map<string, NostrEvent> = new Map();
@ -151,7 +213,14 @@ class EventStore {
} }
subscriptionManager.unsubscribe(subId); subscriptionManager.unsubscribe(subId);
resolve(this.filterEvents(eventArray)); const filtered = this.filterEvents(eventArray);
// Notify callback if provided (for reactive UI updates)
if (options.onUpdate) {
options.onUpdate(filtered);
}
resolve(filtered);
}; };
const onEvent = (event: NostrEvent, relay: string) => { const onEvent = (event: NostrEvent, relay: string) => {
@ -161,6 +230,13 @@ class EventStore {
// Skip hidden events // Skip hidden events
if (this.shouldHideEvent(event)) return; if (this.shouldHideEvent(event)) return;
// Process kind 5 deletion events immediately when received
if (event.kind === 5) {
this.processDeletionEvent(event).catch((error) => {
console.error('Error processing deletion event:', error);
});
}
// Track activity // Track activity
this.trackActivity(event.pubkey, event.created_at); this.trackActivity(event.pubkey, event.created_at);
@ -241,9 +317,164 @@ class EventStore {
*/ */
async storeEvent(event: NostrEvent): Promise<void> { async storeEvent(event: NostrEvent): Promise<void> {
if (this.isMuted(event) || this.shouldHideEvent(event)) return; if (this.isMuted(event) || this.shouldHideEvent(event)) return;
// Process kind 5 deletion events immediately
if (event.kind === 5) {
await this.processDeletionEvent(event);
// Also cache the deletion event itself
this.trackActivity(event.pubkey, event.created_at);
await cacheEvent(event);
return;
}
this.trackActivity(event.pubkey, event.created_at); this.trackActivity(event.pubkey, event.created_at);
await cacheEvent(event); await cacheEvent(event);
} }
/**
* Process a kind 5 deletion event (NIP-09)
* Deletes events referenced in the 'e' tags that belong to the same author
*/
private async processDeletionEvent(deletionEvent: NostrEvent): Promise<void> {
if (deletionEvent.kind !== 5) return;
const authorPubkey = deletionEvent.pubkey;
const eventIdsToDelete: string[] = [];
// Extract event IDs from 'e' tags
for (const tag of deletionEvent.tags) {
if (tag[0] === 'e' && tag[1]) {
eventIdsToDelete.push(tag[1]);
}
}
if (eventIdsToDelete.length === 0) return;
// Verify that the events to delete belong to the same author
// This is a security measure - only delete events from the same pubkey
const eventsToVerify = await Promise.all(
eventIdsToDelete.map(id => getEvent(id))
);
const verifiedIds: string[] = [];
for (let i = 0; i < eventIdsToDelete.length; i++) {
const event = eventsToVerify[i];
if (event && event.pubkey === authorPubkey) {
verifiedIds.push(eventIdsToDelete[i]);
}
}
if (verifiedIds.length > 0) {
await deleteEvents(verifiedIds);
console.log(`Deleted ${verifiedIds.length} event(s) per NIP-09 deletion request from ${authorPubkey.slice(0, 16)}...`);
}
}
/**
* Process all kind 5 deletion events in the cache
* This should be run periodically to clean up deleted events
*/
async processAllDeletionEvents(): Promise<void> {
try {
// Get all kind 5 events from cache
const deletionEvents = await getEventsByKind(5, 1000);
if (deletionEvents.length === 0) return;
// Process each deletion event
for (const deletionEvent of deletionEvents) {
await this.processDeletionEvent(deletionEvent);
}
console.log(`Processed ${deletionEvents.length} deletion event(s)`);
} catch (error) {
console.error('Error processing deletion events:', error);
}
}
/**
* Start the background deletion processor
* Runs on startup and every 15 minutes
*/
startDeletionProcessor(): void {
// Process immediately on startup
this.processAllDeletionEvents().catch((error) => {
console.error('Error in initial deletion processing:', error);
});
// Then run every 15 minutes (900000 ms)
if (this.deletionProcessorInterval) {
clearInterval(this.deletionProcessorInterval);
}
this.deletionProcessorInterval = setInterval(() => {
this.processAllDeletionEvents().catch((error) => {
console.error('Error in periodic deletion processing:', error);
});
}, 15 * 60 * 1000); // 15 minutes
}
/**
* Stop the background deletion processor
*/
stopDeletionProcessor(): void {
if (this.deletionProcessorInterval) {
clearInterval(this.deletionProcessorInterval);
this.deletionProcessorInterval = null;
}
}
} }
export const eventStore = new EventStore(); export const eventStore = new EventStore();
/**
* Warm up caches on app initialization
* This runs in the background and doesn't block the UI
*/
export async function warmupCaches(): Promise<void> {
// Run in background - don't await, just start the process
setTimeout(async () => {
try {
// Initialize activity tracker (loads from cache)
await eventStore.getLastActivity('dummy').catch(() => {}); // This triggers initialization
// Pre-warm common event types
const config = (await import('./config.js')).config;
const defaultRelays = config.defaultRelays;
// Warm up threads (kind 11)
eventStore.fetchEvents(
[{ kinds: [11], limit: 50 }],
defaultRelays,
{ useCache: true, cacheResults: true }
).catch((error) => {
console.error('Error warming thread cache:', error);
});
// Warm up notes (kind 1)
eventStore.fetchEvents(
[{ kinds: [1], limit: 100 }],
defaultRelays,
{ useCache: true, cacheResults: true }
).catch((error) => {
console.error('Error warming notes cache:', error);
});
// Warm up comments (kind 1111)
eventStore.fetchEvents(
[{ kinds: [1111], limit: 100 }],
defaultRelays,
{ useCache: true, cacheResults: true }
).catch((error) => {
console.error('Error warming comments cache:', error);
});
// Start the deletion processor (runs on startup and every 15 minutes)
eventStore.startDeletionProcessor();
console.log('Cache warming started in background');
} catch (error) {
console.error('Error during cache warmup:', error);
}
}, 100); // Small delay to not block initial render
}

41
src/lib/services/nostr/event-utils.js

@ -1,41 +0,0 @@
/**
* Event utilities for creating and signing events
*/
/**
* Create event ID (SHA256 of serialized event)
*/
export async function createEventId(event) {
const serialized = JSON.stringify([
0,
event.pubkey,
event.created_at,
event.kind,
event.tags,
event.content
]);
const encoder = new TextEncoder();
const data = encoder.encode(serialized);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
}
/**
* Sign event (placeholder)
*
* TEMPORARY: Generates a deterministic signature-like string.
* This is NOT a valid secp256k1 signature but has the correct length.
* Production code MUST compute actual secp256k1 signature.
*/
export async function signEvent(event) {
const id = await createEventId(event);
// TEMPORARY: Generate deterministic signature-like string (128 chars)
const encoder = new TextEncoder();
const sigData = encoder.encode(event.pubkey + id);
const sigHashBuffer = await crypto.subtle.digest('SHA-256', sigData);
const sigHashArray = Array.from(new Uint8Array(sigHashBuffer));
const sigHash = sigHashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
// Double the hash to get 128 chars (64 * 2)
const sig = (sigHash + sigHash).slice(0, 128);
return { ...event, id, sig };
}
//# sourceMappingURL=event-utils.js.map

1
src/lib/services/nostr/event-utils.js.map

@ -1 +0,0 @@
{"version":3,"file":"event-utils.js","sourceRoot":"","sources":["event-utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAqC;IACvE,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC;QAChC,CAAC;QACD,KAAK,CAAC,MAAM;QACZ,KAAK,CAAC,UAAU;QAChB,KAAK,CAAC,IAAI;QACV,KAAK,CAAC,IAAI;QACV,KAAK,CAAC,OAAO;KACd,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IACxC,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAC/D,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC;IACzD,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACxE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,KAAqC;IAErC,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,KAAK,CAAC,CAAC;IACtC,sEAAsE;IACtE,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;IAClD,MAAM,aAAa,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACrE,MAAM,YAAY,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,aAAa,CAAC,CAAC,CAAC;IAC/D,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAClF,4CAA4C;IAC5C,MAAM,GAAG,GAAG,CAAC,OAAO,GAAG,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC9C,OAAO,EAAE,GAAG,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC;AAC/B,CAAC"}

181
src/lib/services/nostr/relay-pool.js

@ -1,181 +0,0 @@
/**
* Relay pool management
* Manages WebSocket connections to Nostr relays
*/
import { config } from './config.js';
class RelayPool {
relays = new Map();
status = new Map();
statusCallbacks = new Set();
reconnectTimeouts = new Map();
/**
* Add relay to pool
*/
async addRelay(url) {
if (this.relays.has(url))
return;
this.relays.set(url, null);
this.updateStatus(url, { connected: false });
await this.connect(url);
}
/**
* Remove relay from pool
*/
removeRelay(url) {
const ws = this.relays.get(url);
if (ws) {
ws.close();
}
this.relays.delete(url);
this.status.delete(url);
const timeout = this.reconnectTimeouts.get(url);
if (timeout) {
clearTimeout(timeout);
this.reconnectTimeouts.delete(url);
}
}
/**
* Connect to a relay
*/
async connect(url) {
try {
const ws = new WebSocket(url);
const startTime = Date.now();
ws.onopen = () => {
const latency = Date.now() - startTime;
this.relays.set(url, ws);
this.updateStatus(url, {
connected: true,
latency,
lastConnected: Date.now()
});
};
ws.onerror = (error) => {
this.updateStatus(url, {
connected: false,
lastError: error.message || 'Connection error'
});
this.scheduleReconnect(url);
};
ws.onclose = () => {
this.relays.set(url, null);
this.updateStatus(url, { connected: false });
this.scheduleReconnect(url);
};
// Store WebSocket for message sending
this.relays.set(url, ws);
}
catch (error) {
this.updateStatus(url, {
connected: false,
lastError: error instanceof Error ? error.message : 'Unknown error'
});
this.scheduleReconnect(url);
}
}
/**
* Schedule reconnection attempt
*/
scheduleReconnect(url) {
const existing = this.reconnectTimeouts.get(url);
if (existing)
clearTimeout(existing);
const timeout = setTimeout(() => {
this.reconnectTimeouts.delete(url);
this.connect(url);
}, 5000); // 5 second delay
this.reconnectTimeouts.set(url, timeout);
}
/**
* Update relay status and notify callbacks
*/
updateStatus(url, updates) {
const current = this.status.get(url) || { url, connected: false };
const updated = { ...current, ...updates };
this.status.set(url, updated);
// Notify callbacks
this.statusCallbacks.forEach((cb) => cb(updated));
}
/**
* Get WebSocket for a relay
*/
getRelay(url) {
return this.relays.get(url) || null;
}
/**
* Get all connected relays
*/
getConnectedRelays() {
return Array.from(this.relays.entries())
.filter(([, ws]) => ws && ws.readyState === WebSocket.OPEN)
.map(([url]) => url);
}
/**
* Get relay status
*/
getStatus(url) {
return this.status.get(url);
}
/**
* Get all relay statuses
*/
getAllStatuses() {
return Array.from(this.status.values());
}
/**
* Subscribe to status updates
*/
onStatusUpdate(callback) {
this.statusCallbacks.add(callback);
return () => this.statusCallbacks.delete(callback);
}
/**
* Send message to relay
*/
send(url, message) {
const ws = this.relays.get(url);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(message);
return true;
}
return false;
}
/**
* Send message to all connected relays
*/
broadcast(message) {
const sent = [];
for (const [url, ws] of this.relays.entries()) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(message);
sent.push(url);
}
}
return sent;
}
/**
* Close all connections
*/
closeAll() {
for (const [url, ws] of this.relays.entries()) {
if (ws) {
ws.close();
}
const timeout = this.reconnectTimeouts.get(url);
if (timeout) {
clearTimeout(timeout);
this.reconnectTimeouts.delete(url);
}
}
this.relays.clear();
this.status.clear();
}
}
export const relayPool = new RelayPool();
// Initialize with default relays
export async function initializeRelayPool() {
for (const url of config.defaultRelays) {
await relayPool.addRelay(url);
}
}
//# sourceMappingURL=relay-pool.js.map

1
src/lib/services/nostr/relay-pool.js.map

File diff suppressed because one or more lines are too long

119
src/lib/services/nostr/subscription-manager.js

@ -1,119 +0,0 @@
/**
* Subscription manager for Nostr subscriptions
*/
import { relayPool } from './relay-pool.js';
class SubscriptionManager {
subscriptions = new Map();
nextSubId = 1;
/**
* Create a new subscription
*/
subscribe(subId, relays, filters, onEvent, onEose) {
// Close existing subscription if any
this.unsubscribe(subId);
const subscription = {
id: subId,
relays,
filters,
onEvent,
onEose,
messageHandlers: new Map()
};
// Set up message handlers for each relay
for (const relayUrl of relays) {
const ws = relayPool.getRelay(relayUrl);
if (!ws)
continue;
const handler = (event) => {
try {
const data = JSON.parse(event.data);
if (Array.isArray(data)) {
const [type, ...rest] = data;
if (type === 'EVENT' && rest[0] === subId) {
const event = rest[1];
if (this.matchesFilters(event, filters)) {
onEvent(event, relayUrl);
}
}
else if (type === 'EOSE' && rest[0] === subId) {
onEose?.(relayUrl);
}
}
}
catch (error) {
console.error('Error parsing relay message:', error);
}
};
ws.addEventListener('message', handler);
subscription.messageHandlers.set(relayUrl, handler);
// Send subscription request
const message = JSON.stringify(['REQ', subId, ...filters]);
relayPool.send(relayUrl, message);
}
this.subscriptions.set(subId, subscription);
}
/**
* Check if event matches filters
*/
matchesFilters(event, filters) {
return filters.some((filter) => {
if (filter.ids && !filter.ids.includes(event.id))
return false;
if (filter.authors && !filter.authors.includes(event.pubkey))
return false;
if (filter.kinds && !filter.kinds.includes(event.kind))
return false;
if (filter.since && event.created_at < filter.since)
return false;
if (filter.until && event.created_at > filter.until)
return false;
// Tag filters
if (filter['#e']) {
const hasE = event.tags.some((t) => t[0] === 'e' && filter['#e'].includes(t[1]));
if (!hasE)
return false;
}
if (filter['#p']) {
const hasP = event.tags.some((t) => t[0] === 'p' && filter['#p'].includes(t[1]));
if (!hasP)
return false;
}
return true;
});
}
/**
* Unsubscribe from a subscription
*/
unsubscribe(subId) {
const subscription = this.subscriptions.get(subId);
if (!subscription)
return;
// Remove message handlers
for (const [relayUrl, handler] of subscription.messageHandlers.entries()) {
const ws = relayPool.getRelay(relayUrl);
if (ws) {
ws.removeEventListener('message', handler);
}
// Send close message
const message = JSON.stringify(['CLOSE', subId]);
relayPool.send(relayUrl, message);
}
this.subscriptions.delete(subId);
}
/**
* Generate a unique subscription ID
*/
generateSubId() {
return `sub_${this.nextSubId++}_${Date.now()}`;
}
/**
* Close all subscriptions
*/
closeAll() {
for (const subId of this.subscriptions.keys()) {
this.unsubscribe(subId);
}
}
}
export const subscriptionManager = new SubscriptionManager();
//# sourceMappingURL=subscription-manager.js.map

1
src/lib/services/nostr/subscription-manager.js.map

@ -1 +0,0 @@
{"version":3,"file":"subscription-manager.js","sourceRoot":"","sources":["subscription-manager.ts"],"names":[],"mappings":"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"}

42
src/lib/services/security/bech32-utils.js

@ -1,42 +0,0 @@
/**
* Bech32 utilities for NIP-19 encoding/decoding
*/
/**
* Decode a bech32 string (simplified - full implementation would use bech32 library)
* This is a placeholder - in production, use a proper bech32 library
*/
export function decodeBech32(bech32) {
try {
const prefix = bech32.split('1')[0];
if (!prefix)
return null;
// Basic validation - full implementation needed
if (prefix === 'npub' || prefix === 'nsec' || prefix === 'note') {
return {
type: prefix,
data: new Uint8Array(32) // Placeholder
};
}
return null;
}
catch {
return null;
}
}
/**
* Encode data to bech32 format
*/
export function encodeBech32(type, data, relay) {
// Placeholder - full implementation needed with bech32 library
// For now, return hex representation
return `${type}1${Array.from(data)
.map((b) => b.toString(16).padStart(2, '0'))
.join('')}`;
}
/**
* Validate bech32 string format
*/
export function isValidBech32(bech32) {
return /^(npub|nsec|note|nevent|naddr|nprofile)1[a-z0-9]+$/.test(bech32);
}
//# sourceMappingURL=bech32-utils.js.map

1
src/lib/services/security/bech32-utils.js.map

@ -1 +0,0 @@
{"version":3,"file":"bech32-utils.js","sourceRoot":"","sources":["bech32-utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAQH;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACpC,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QAEzB,gDAAgD;QAChD,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YAChE,OAAO;gBACL,IAAI,EAAE,MAAkC;gBACxC,IAAI,EAAE,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,cAAc;aACxC,CAAC;QACJ,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,IAAgB,EAAE,KAAc;IACzE,+DAA+D;IAC/D,qCAAqC;IACrC,OAAO,GAAG,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;SAC/B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;SAC3C,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,MAAc;IAC1C,OAAO,oDAAoD,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC3E,CAAC"}

48
src/lib/services/security/event-validator.js

@ -1,48 +0,0 @@
/**
* Event validation utilities
*/
/**
* Validate event structure
*/
export function isValidEvent(event) {
if (!event || typeof event !== 'object')
return false;
const e = event;
return (typeof e.kind === 'number' &&
typeof e.pubkey === 'string' &&
typeof e.created_at === 'number' &&
typeof e.content === 'string' &&
typeof e.id === 'string' &&
typeof e.sig === 'string' &&
Array.isArray(e.tags) &&
e.pubkey.length === 64 &&
e.id.length === 64 &&
e.sig.length === 128);
}
/**
* Check if event has required tags for a kind
*/
export function hasRequiredTags(event, kind) {
switch (kind) {
case 0:
// Kind 0 can have tags or JSON content
return true;
case 11:
// Thread - should have title tag
return true;
case 1111:
// Comment - should have K and E tags
return event.tags.some((t) => t[0] === 'K' || t[0] === 'E');
default:
return true;
}
}
/**
* Validate event signature (placeholder - would need crypto library)
*/
export function isValidSignature(event) {
// Placeholder - full implementation would verify signature
// using secp256k1 cryptography
return event.sig.length === 128;
}
//# sourceMappingURL=event-validator.js.map

1
src/lib/services/security/event-validator.js.map

@ -1 +0,0 @@
{"version":3,"file":"event-validator.js","sourceRoot":"","sources":["event-validator.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAEtD,MAAM,CAAC,GAAG,KAAgC,CAAC;IAE3C,OAAO,CACL,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ;QAC1B,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ;QAC5B,OAAO,CAAC,CAAC,UAAU,KAAK,QAAQ;QAChC,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ;QAC7B,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ;QACxB,OAAO,CAAC,CAAC,GAAG,KAAK,QAAQ;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;QACrB,CAAC,CAAC,MAAM,CAAC,MAAM,KAAK,EAAE;QACtB,CAAC,CAAC,EAAE,CAAC,MAAM,KAAK,EAAE;QAClB,CAAC,CAAC,GAAG,CAAC,MAAM,KAAK,GAAG,CACrB,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,KAAiB,EAAE,IAAY;IAC7D,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,CAAC;YACJ,uCAAuC;YACvC,OAAO,IAAI,CAAC;QACd,KAAK,EAAE;YACL,iCAAiC;YACjC,OAAO,IAAI,CAAC;QACd,KAAK,IAAI;YACP,qCAAqC;YACrC,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC;QAC9D;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAiB;IAChD,2DAA2D;IAC3D,+BAA+B;IAC/B,OAAO,KAAK,CAAC,GAAG,CAAC,MAAM,KAAK,GAAG,CAAC;AAClC,CAAC"}

68
src/lib/services/security/key-management.js

@ -1,68 +0,0 @@
/**
* Key management with NIP-49 encryption
* All private keys MUST be encrypted before storage
*/
/**
* Encrypt a private key using NIP-49 (password-based encryption)
* This is a placeholder - full implementation requires:
* - scrypt for key derivation
* - AES-256-GCM for encryption
* - Base64 encoding
*
* WARNING: Current implementation stores keys in plaintext format.
* This is insecure and should only be used for development.
* Production code MUST implement proper NIP-49 encryption.
*/
export async function encryptPrivateKey(nsec, password) {
// TEMPORARY: Store as base64-encoded plaintext with a marker
// This allows the system to function while proper crypto is implemented
// Full NIP-49 implementation would:
// 1. Derive key from password using scrypt
// 2. Generate random salt and nonce
// 3. Encrypt nsec with AES-256-GCM
// 4. Encode as ncryptsec format
const encoded = btoa(JSON.stringify({ nsec, password }));
return `ncryptsec1${encoded}`;
}
/**
* Decrypt a private key using NIP-49
*
* WARNING: Current implementation reads plaintext keys.
* This is insecure and should only be used for development.
* Production code MUST implement proper NIP-49 decryption.
*/
export async function decryptPrivateKey(ncryptsec, password) {
// TEMPORARY: Decode from base64 plaintext format
// This allows the system to function while proper crypto is implemented
// Full NIP-49 implementation would:
// 1. Decode ncryptsec format
// 2. Derive key from password using scrypt
// 3. Decrypt with AES-256-GCM
// 4. Return plain nsec
if (!ncryptsec.startsWith('ncryptsec1')) {
throw new Error('Invalid ncryptsec format');
}
try {
const decoded = JSON.parse(atob(ncryptsec.slice(11)));
if (decoded.password !== password) {
throw new Error('Invalid password');
}
return decoded.nsec;
}
catch (error) {
throw new Error('Failed to decrypt private key: ' + (error instanceof Error ? error.message : 'Unknown error'));
}
}
/**
* Generate a new private key
*/
export function generatePrivateKey() {
// Placeholder - would use crypto.getRandomValues to generate 32 random bytes
// then encode as hex
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
//# sourceMappingURL=key-management.js.map

1
src/lib/services/security/key-management.js.map

@ -1 +0,0 @@
{"version":3,"file":"key-management.js","sourceRoot":"","sources":["key-management.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,IAAY,EAAE,QAAgB;IACpE,6DAA6D;IAC7D,wEAAwE;IACxE,oCAAoC;IACpC,2CAA2C;IAC3C,oCAAoC;IACpC,mCAAmC;IACnC,gCAAgC;IAChC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;IACzD,OAAO,aAAa,OAAO,EAAE,CAAC;AAChC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,SAAiB,EAAE,QAAgB;IACzE,iDAAiD;IACjD,wEAAwE;IACxE,oCAAoC;IACpC,6BAA6B;IAC7B,2CAA2C;IAC3C,8BAA8B;IAC9B,uBAAuB;IACvB,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAC9C,CAAC;IACD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACtD,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAC;QACtC,CAAC;QACD,OAAO,OAAO,CAAC,IAAI,CAAC;IACtB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,iCAAiC,GAAG,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC;IAClH,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB;IAChC,6EAA6E;IAC7E,qBAAqB;IACrB,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC;IACjC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IAC9B,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;SACrB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;SAC3C,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC"}

44
src/lib/services/security/sanitizer.js

@ -1,44 +0,0 @@
/**
* HTML sanitization using DOMPurify
*/
import DOMPurify from 'dompurify';
/**
* Sanitize HTML content
*/
export function sanitizeHtml(dirty) {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: [
'p',
'br',
'strong',
'em',
'u',
's',
'code',
'pre',
'a',
'ul',
'ol',
'li',
'blockquote',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'img',
'video',
'audio'
],
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class', 'controls', 'preload'],
ALLOW_DATA_ATTR: false
});
}
/**
* Sanitize markdown-rendered HTML
*/
export function sanitizeMarkdown(html) {
return sanitizeHtml(html);
}
//# sourceMappingURL=sanitizer.js.map

1
src/lib/services/security/sanitizer.js.map

@ -1 +0,0 @@
{"version":3,"file":"sanitizer.js","sourceRoot":"","sources":["sanitizer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,SAAS,MAAM,WAAW,CAAC;AAElC;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE;QAC/B,YAAY,EAAE;YACZ,GAAG;YACH,IAAI;YACJ,QAAQ;YACR,IAAI;YACJ,GAAG;YACH,GAAG;YACH,MAAM;YACN,KAAK;YACL,GAAG;YACH,IAAI;YACJ,IAAI;YACJ,IAAI;YACJ,YAAY;YACZ,IAAI;YACJ,IAAI;YACJ,IAAI;YACJ,IAAI;YACJ,IAAI;YACJ,IAAI;YACJ,KAAK;YACL,OAAO;YACP,OAAO;SACR;QACD,YAAY,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,CAAC;QAC7E,eAAe,EAAE,KAAK;KACvB,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC"}

11
src/routes/+layout.svelte

@ -1,5 +1,16 @@
<script lang="ts"> <script lang="ts">
import '../app.css'; import '../app.css';
import { sessionManager } from '../lib/services/auth/session-manager.js';
import { onMount } from 'svelte';
// Restore session on app load
onMount(async () => {
try {
await sessionManager.restoreSession();
} catch (error) {
console.error('Failed to restore session:', error);
}
});
</script> </script>
<slot /> <slot />

4
src/routes/+page.svelte

@ -5,7 +5,11 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
onMount(async () => { onMount(async () => {
try {
await nostrClient.initialize(); await nostrClient.initialize();
} catch (error) {
console.error('Failed to initialize Nostr client:', error);
}
}); });
</script> </script>

Loading…
Cancel
Save