Browse Source

first successful build

master
Silberengel 1 month ago
parent
commit
3a0b1ba493
  1. 8
      .vscode/settings.json
  2. 5107
      package-lock.json
  3. 2
      package.json
  4. 4
      public/healthz.json
  5. 77
      src/app.css
  6. 15
      src/app.d.ts
  7. 50
      src/app.html
  8. 64
      src/lib/components/content/MarkdownRenderer.svelte
  9. 45
      src/lib/components/layout/Header.svelte
  10. 12
      src/lib/components/layout/ProfileBadge.svelte
  11. 54
      src/lib/components/modals/PublicationStatusModal.svelte
  12. 37
      src/lib/components/preferences/ThemeToggle.svelte
  13. 24
      src/lib/modules/threads/CreateThreadForm.svelte
  14. 16
      src/lib/modules/threads/ThreadCard.svelte
  15. 10
      src/lib/modules/threads/ThreadList.svelte
  16. 29
      src/lib/services/auth/activity-tracker.js
  17. 1
      src/lib/services/auth/activity-tracker.js.map
  18. 45
      src/lib/services/auth/anonymous-signer.js
  19. 1
      src/lib/services/auth/anonymous-signer.js.map
  20. 2
      src/lib/services/auth/anonymous-signer.ts
  21. 31
      src/lib/services/auth/bunker-signer.js
  22. 1
      src/lib/services/auth/bunker-signer.js.map
  23. 39
      src/lib/services/auth/nip07-signer.js
  24. 1
      src/lib/services/auth/nip07-signer.js.map
  25. 62
      src/lib/services/auth/nsec-signer.js
  26. 1
      src/lib/services/auth/nsec-signer.js.map
  27. 54
      src/lib/services/auth/nsec-signer.ts
  28. 98
      src/lib/services/auth/profile-fetcher.js
  29. 1
      src/lib/services/auth/profile-fetcher.js.map
  30. 65
      src/lib/services/auth/relay-list-fetcher.js
  31. 1
      src/lib/services/auth/relay-list-fetcher.js.map
  32. 17
      src/lib/services/auth/relay-list-fetcher.ts
  33. 99
      src/lib/services/auth/session-manager.js
  34. 1
      src/lib/services/auth/session-manager.js.map
  35. 24
      src/lib/services/auth/session-manager.ts
  36. 12
      src/lib/services/auth/user-preferences-fetcher.js
  37. 1
      src/lib/services/auth/user-preferences-fetcher.js.map
  38. 38
      src/lib/services/auth/user-status-fetcher.js
  39. 1
      src/lib/services/auth/user-status-fetcher.js.map
  40. 2
      src/lib/services/auth/user-status-fetcher.ts
  41. 52
      src/lib/services/cache/anonymous-key-store.js
  42. 1
      src/lib/services/cache/anonymous-key-store.js.map
  43. 88
      src/lib/services/cache/event-cache.js
  44. 1
      src/lib/services/cache/event-cache.js.map
  45. 10
      src/lib/services/cache/event-cache.ts
  46. 48
      src/lib/services/cache/indexeddb-store.js
  47. 1
      src/lib/services/cache/indexeddb-store.js.map
  48. 42
      src/lib/services/cache/profile-cache.js
  49. 1
      src/lib/services/cache/profile-cache.js.map
  50. 43
      src/lib/services/cache/search-index.js
  51. 1
      src/lib/services/cache/search-index.js.map
  52. 101
      src/lib/services/nostr/applesauce-client.js
  53. 1
      src/lib/services/nostr/applesauce-client.js.map
  54. 119
      src/lib/services/nostr/auth-handler.js
  55. 1
      src/lib/services/nostr/auth-handler.js.map
  56. 11
      src/lib/services/nostr/auth-handler.ts
  57. 49
      src/lib/services/nostr/config.js
  58. 1
      src/lib/services/nostr/config.js.map
  59. 213
      src/lib/services/nostr/event-store.js
  60. 1
      src/lib/services/nostr/event-store.js.map
  61. 63
      src/lib/services/nostr/event-store.ts
  62. 41
      src/lib/services/nostr/event-utils.js
  63. 1
      src/lib/services/nostr/event-utils.js.map
  64. 27
      src/lib/services/nostr/event-utils.ts
  65. 181
      src/lib/services/nostr/relay-pool.js
  66. 1
      src/lib/services/nostr/relay-pool.js.map
  67. 4
      src/lib/services/nostr/relay-pool.ts
  68. 119
      src/lib/services/nostr/subscription-manager.js
  69. 1
      src/lib/services/nostr/subscription-manager.js.map
  70. 13
      src/lib/services/nostr/subscription-manager.ts
  71. 42
      src/lib/services/security/bech32-utils.js
  72. 1
      src/lib/services/security/bech32-utils.js.map
  73. 48
      src/lib/services/security/event-validator.js
  74. 1
      src/lib/services/security/event-validator.js.map
  75. 68
      src/lib/services/security/key-management.js
  76. 1
      src/lib/services/security/key-management.js.map
  77. 30
      src/lib/services/security/key-management.ts
  78. 44
      src/lib/services/security/sanitizer.js
  79. 1
      src/lib/services/security/sanitizer.js.map
  80. 5
      src/lib/types/nostr.js
  81. 1
      src/lib/types/nostr.js.map
  82. 1
      src/lib/types/nostr.ts
  83. 6
      src/routes/+page.svelte
  84. 10
      src/routes/login/+page.svelte
  85. 6
      src/routes/threads/+page.svelte
  86. 12
      src/vite-env.d.ts
  87. 25
      static/README.md
  88. BIN
      static/aither.png
  89. 24
      static/favicon.svg
  90. 18
      static/manifest.json
  91. 19
      static/og-image.svg
  92. 5
      svelte.config.js
  93. 28
      tailwind.config.js
  94. 16
      tsconfig.json
  95. 28
      vite.config.js
  96. 1
      vite.config.js.map

8
.vscode/settings.json vendored

@ -0,0 +1,8 @@
{
"css.lint.unknownAtRules": "ignore",
"scss.lint.unknownAtRules": "ignore",
"less.lint.unknownAtRules": "ignore",
"css.validate": false,
"scss.validate": false,
"less.validate": false
}

5107
package-lock.json generated

File diff suppressed because it is too large Load Diff

2
package.json

@ -15,7 +15,7 @@
}, },
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "tsc && vite build", "build": "svelte-kit sync && tsc && vite build",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",

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": "2024-01-01T00:00:00.000Z", "buildTime": "2026-02-02T13:24:45.619Z",
"gitCommit": "unknown", "gitCommit": "unknown",
"timestamp": 1704067200000 "timestamp": 1770038685620
} }

77
src/app.css

@ -1,5 +1,8 @@
/* stylelint-disable-next-line at-rule-no-unknown */
@tailwind base; @tailwind base;
/* stylelint-disable-next-line at-rule-no-unknown */
@tailwind components; @tailwind components;
/* stylelint-disable-next-line at-rule-no-unknown */
@tailwind utilities; @tailwind utilities;
:root { :root {
@ -47,6 +50,80 @@
body { body {
font-size: var(--text-size); font-size: var(--text-size);
line-height: var(--line-height); line-height: var(--line-height);
background-color: #f1f5f9;
color: #475569;
transition: background-color 0.3s ease, color 0.3s ease;
}
/* Dark mode body styles */
.dark body {
background-color: #0f172a;
color: #cbd5e1;
}
/* Fog aesthetic base styles */
.bg-fog {
background-color: #f1f5f9;
}
.dark .bg-fog {
background-color: #0f172a;
}
.bg-fog-surface {
background-color: #f8fafc;
}
.dark .bg-fog-surface {
background-color: #1e293b;
}
/* Anon aesthetic: Grayscale with blue tinge for profile pics and emojis */
/* Profile pictures - all instances */
.profile-picture,
.profile-badge img,
img[alt*="profile" i],
img[alt*="avatar" i],
img[src*="avatar" i],
img[src*="profile" i] {
filter: grayscale(100%) sepia(15%) hue-rotate(200deg) saturate(60%) brightness(0.95);
transition: filter 0.3s ease;
}
.dark .profile-picture,
.dark .profile-badge img,
.dark img[alt*="profile" i],
.dark img[alt*="avatar" i],
.dark img[src*="avatar" i],
.dark img[src*="profile" i] {
filter: grayscale(100%) sepia(20%) hue-rotate(200deg) saturate(70%) brightness(0.9);
}
/* Emoji images - grayscale with blue tinge */
.emoji,
[class*="emoji"],
img[alt*="emoji" i],
img[src*="emoji" i] {
filter: grayscale(100%) sepia(15%) hue-rotate(200deg) saturate(60%) brightness(0.95);
display: inline-block;
}
.dark .emoji,
.dark [class*="emoji"],
.dark img[alt*="emoji" i],
.dark img[src*="emoji" i] {
filter: grayscale(100%) sepia(20%) hue-rotate(200deg) saturate(70%) brightness(0.9);
}
/* Apply to all images in markdown content */
.markdown-content img,
.anon-content img {
filter: grayscale(100%) sepia(15%) hue-rotate(200deg) saturate(60%) brightness(0.95);
}
.dark .markdown-content img,
.dark .anon-content img {
filter: grayscale(100%) sepia(20%) hue-rotate(200deg) saturate(70%) brightness(0.9);
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {

15
src/app.d.ts vendored

@ -1,5 +1,7 @@
// See https://kit.svelte.dev/docs/types#app // See https://kit.svelte.dev/docs/types#app
// for information about these interfaces // for information about these interfaces
/// <reference types="@sveltejs/kit" />
declare global { declare global {
namespace App { namespace App {
// interface Error {} // interface Error {}
@ -9,4 +11,17 @@ declare global {
} }
} }
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_DEFAULT_RELAYS?: string;
readonly VITE_ZAP_THRESHOLD?: string;
readonly VITE_THREAD_TIMEOUT_DAYS?: string;
readonly VITE_PWA_ENABLED?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
export {}; export {};

50
src/app.html

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="%sveltekit.assets%/favicon.svg" />
<link rel="apple-touch-icon" href="%sveltekit.assets%/favicon.svg" />
<!-- Primary Meta Tags -->
<title>Aitherboard - Decentralized Messageboard on Nostr</title>
<meta name="title" content="Aitherboard - Decentralized Messageboard on Nostr" />
<meta name="description" content="A decentralized messageboard built on the Nostr protocol. Create threads, comment, react, and zap in a censorship-resistant environment." />
<meta name="keywords" content="nostr, decentralized, messageboard, forum, social media, censorship resistant" />
<meta name="author" content="silberengel@gitcitadel.com" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://aitherboard.com/" />
<meta property="og:title" content="Aitherboard - Decentralized Messageboard on Nostr" />
<meta property="og:description" content="A decentralized messageboard built on the Nostr protocol. Create threads, comment, react, and zap in a censorship-resistant environment." />
<meta property="og:image" content="%sveltekit.assets%/aither.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="Aitherboard - Decentralized Messageboard on Nostr" />
<meta property="og:site_name" content="Aitherboard" />
<meta property="og:locale" content="en_US" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content="https://aitherboard.com/" />
<meta name="twitter:title" content="Aitherboard - Decentralized Messageboard on Nostr" />
<meta name="twitter:description" content="A decentralized messageboard built on the Nostr protocol. Create threads, comment, react, and zap in a censorship-resistant environment." />
<meta name="twitter:image" content="%sveltekit.assets%/aither.png" />
<meta name="twitter:image:alt" content="Aitherboard - Decentralized Messageboard on Nostr" />
<!-- Additional Meta Tags -->
<meta name="theme-color" content="#f1f5f9" />
<meta name="color-scheme" content="light dark" />
<!-- PWA Manifest -->
<link rel="manifest" href="%sveltekit.assets%/manifest.json" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

64
src/lib/components/content/MarkdownRenderer.svelte

@ -1,23 +1,32 @@
<script lang="ts"> <script lang="ts">
import { marked } from 'marked'; import { marked } from 'marked';
import { sanitizeMarkdown } from '../../services/security/sanitizer.js'; import { sanitizeMarkdown } from '../../services/security/sanitizer.js';
import { onMount } from 'svelte';
export let content: string = ''; interface Props {
content?: string;
}
let { content = '' }: Props = $props();
let rendered = $state(''); let rendered = $state('');
$effect(() => { $effect(() => {
if (content) { if (content) {
const html = marked.parse(content); const parseResult = marked.parse(content);
if (parseResult instanceof Promise) {
parseResult.then((html) => {
rendered = sanitizeMarkdown(html); rendered = sanitizeMarkdown(html);
});
} else {
rendered = sanitizeMarkdown(parseResult);
}
} else { } else {
rendered = ''; rendered = '';
} }
}); });
</script> </script>
<div class="markdown-content"> <div class="markdown-content anon-content">
{@html rendered} {@html rendered}
</div> </div>
@ -31,26 +40,67 @@
} }
.markdown-content :global(a) { .markdown-content :global(a) {
color: #0066cc; color: #64748b;
text-decoration: underline; text-decoration: underline;
} }
.dark .markdown-content :global(a) {
color: #94a3b8;
}
.markdown-content :global(a:hover) {
color: #475569;
}
.dark .markdown-content :global(a:hover) {
color: #cbd5e1;
}
.markdown-content :global(code) { .markdown-content :global(code) {
background: #f0f0f0; background: #e2e8f0;
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
border-radius: 3px; border-radius: 3px;
font-family: monospace; font-family: monospace;
color: #475569;
}
.dark .markdown-content :global(code) {
background: #475569;
color: #cbd5e1;
} }
.markdown-content :global(pre) { .markdown-content :global(pre) {
background: #f0f0f0; background: #e2e8f0;
padding: 1em; padding: 1em;
border-radius: 5px; border-radius: 5px;
overflow-x: auto; overflow-x: auto;
border: 1px solid #cbd5e1;
}
.dark .markdown-content :global(pre) {
background: #475569;
border-color: #64748b;
} }
.markdown-content :global(img) { .markdown-content :global(img) {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
filter: grayscale(100%) sepia(15%) hue-rotate(200deg) saturate(60%) brightness(0.95);
}
.dark .markdown-content :global(img) {
filter: grayscale(100%) sepia(20%) hue-rotate(200deg) saturate(70%) brightness(0.9);
}
/* Style emojis in content */
.markdown-content :global(span[role="img"]),
.markdown-content :global(.emoji) {
filter: grayscale(100%) sepia(15%) hue-rotate(200deg) saturate(60%) brightness(0.95);
display: inline-block;
}
.dark .markdown-content :global(span[role="img"]),
.dark .markdown-content :global(.emoji) {
filter: grayscale(100%) sepia(20%) hue-rotate(200deg) saturate(70%) brightness(0.9);
} }
</style> </style>

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

@ -1,19 +1,44 @@
<script lang="ts"> <script lang="ts">
import { sessionManager } from '../../services/auth/session-manager.js'; import { sessionManager } from '../../services/auth/session-manager.js';
import ThemeToggle from '../preferences/ThemeToggle.svelte';
$: currentSession = $sessionManager.session;
$: isLoggedIn = currentSession !== null;
$: currentPubkey = currentSession?.pubkey || null;
</script> </script>
<header class="bg-board-post border-b border-board-border p-4"> <header class="relative border-b border-fog-border dark:border-fog-dark-border">
<nav class="flex items-center justify-between"> <!-- Banner image -->
<a href="/" class="text-xl font-bold">Aitherboard</a> <div class="w-full h-32 md:h-48 overflow-hidden bg-fog-surface dark:bg-fog-dark-surface">
<div class="flex gap-4"> <img
{#if $sessionManager.isLoggedIn()} src="/aither.png"
<span>Logged in as: {$sessionManager.getCurrentPubkey()?.slice(0, 16)}...</span> alt="Aitherboard banner"
<button on:click={() => sessionManager.clearSession()}>Logout</button> class="w-full h-full object-cover opacity-90 dark:opacity-70"
/>
<!-- 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>
<!-- Navigation -->
<nav class="bg-fog-surface/95 dark:bg-fog-dark-surface/95 backdrop-blur-sm border-b border-fog-border dark:border-fog-dark-border px-4 py-3">
<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>
<div class="flex gap-4 items-center text-sm">
<ThemeToggle />
{#if isLoggedIn}
<span class="text-fog-text-light dark:text-fog-dark-text-light">Logged in as: {currentPubkey?.slice(0, 16)}...</span>
<button
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"
>
Logout
</button>
{:else} {:else}
<a href="/login">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">Feed</a> <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>
<a href="/threads">Threads</a> <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>
</nav> </nav>
</header> </header>

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

@ -4,7 +4,11 @@
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';
export let pubkey: string; interface Props {
pubkey: string;
}
let { pubkey }: Props = $props();
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);
@ -49,9 +53,9 @@
<a href="/profile/{pubkey}" class="profile-badge inline-flex items-center gap-2"> <a href="/profile/{pubkey}" class="profile-badge inline-flex items-center gap-2">
{#if profile?.picture} {#if profile?.picture}
<img src={profile.picture} alt={profile.name || pubkey} class="w-6 h-6 rounded" /> <img src={profile.picture} alt={profile.name || pubkey} class="profile-picture w-6 h-6 rounded" />
{:else} {:else}
<div class="w-6 h-6 rounded bg-gray-300"></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}
@ -62,7 +66,7 @@
></span> ></span>
{/if} {/if}
{#if status} {#if status}
<span class="text-sm text-gray-600">({status})</span> <span class="text-sm text-fog-text-light dark:text-fog-dark-text-light">({status})</span>
{/if} {/if}
</a> </a>

54
src/lib/components/modals/PublicationStatusModal.svelte

@ -1,9 +1,13 @@
<script lang="ts"> <script lang="ts">
export let open = $state(false); interface Props {
export let results: { open?: boolean;
results?: {
success: string[]; success: string[];
failed: Array<{ relay: string; error: string }>; failed: Array<{ relay: string; error: string }>;
} | null = $state(null); } | null;
}
let { open = $bindable(false), results = $bindable(null) }: Props = $props();
let autoCloseTimeout: ReturnType<typeof setTimeout> | null = null; let autoCloseTimeout: ReturnType<typeof setTimeout> | null = null;
@ -32,11 +36,11 @@
</script> </script>
{#if open && results} {#if open && results}
<div class="modal-overlay" on:click={close} on:keydown={(e) => e.key === 'Escape' && close()}> <div class="modal-overlay" onclick={close} onkeydown={(e) => e.key === 'Escape' && close()}>
<div class="modal-content" on:click|stopPropagation> <div class="modal-content" onclick={(e) => e.stopPropagation()}>
<div class="modal-header"> <div class="modal-header">
<h2>Publication Status</h2> <h2>Publication Status</h2>
<button on:click={close} class="close-button">×</button> <button onclick={close} class="close-button">×</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@ -66,7 +70,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button on:click={close}>Close</button> <button onclick={close}>Close</button>
</div> </div>
</div> </div>
</div> </div>
@ -79,7 +83,8 @@
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5); background: rgba(15, 23, 42, 0.4);
backdrop-filter: blur(4px);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -87,12 +92,20 @@
} }
.modal-content { .modal-content {
background: white; background: #f8fafc;
border: 1px solid #cbd5e1;
border-radius: 8px; border-radius: 8px;
max-width: 600px; max-width: 600px;
width: 90%; width: 90%;
max-height: 80vh; max-height: 80vh;
overflow: auto; overflow: auto;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
.dark .modal-content {
background: #1e293b;
border-color: #475569;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
} }
.modal-header { .modal-header {
@ -100,7 +113,11 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 1rem; padding: 1rem;
border-bottom: 1px solid #e5e7eb; border-bottom: 1px solid #cbd5e1;
}
.dark .modal-header {
border-bottom-color: #475569;
} }
.close-button { .close-button {
@ -123,11 +140,11 @@
} }
.success-section h3 { .success-section h3 {
color: #22c55e; color: #64748b;
} }
.failed-section h3 { .failed-section h3 {
color: #ef4444; color: #dc2626;
} }
.modal-body ul { .modal-body ul {
@ -142,16 +159,25 @@
.modal-footer { .modal-footer {
padding: 1rem; padding: 1rem;
border-top: 1px solid #e5e7eb; border-top: 1px solid #cbd5e1;
text-align: right; text-align: right;
} }
.dark .modal-footer {
border-top-color: #475569;
}
.modal-footer button { .modal-footer button {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
background: #3b82f6; background: #94a3b8;
color: white; color: white;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s;
}
.modal-footer button:hover {
background: #64748b;
} }
</style> </style>

37
src/lib/components/preferences/ThemeToggle.svelte

@ -0,0 +1,37 @@
<script lang="ts">
import { onMount } from 'svelte';
let isDark = $state(false);
onMount(() => {
// Check localStorage and system preference
const stored = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
isDark = stored === 'dark' || (!stored && prefersDark);
updateTheme();
});
function toggleTheme() {
isDark = !isDark;
updateTheme();
}
function updateTheme() {
if (isDark) {
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
}
}
</script>
<button
onclick={toggleTheme}
class="px-3 py-1 rounded 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 hover:bg-fog-highlight dark:hover:bg-fog-dark-highlight transition-colors"
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
>
<span class="emoji">{#if isDark}{:else}🌙{/if}</span>
</button>

24
src/lib/modules/threads/CreateThreadForm.svelte

@ -73,49 +73,49 @@
} }
</script> </script>
<form on:submit|preventDefault={publish} class="create-thread-form"> <form onsubmit={(e) => { e.preventDefault(); publish(); }} class="create-thread-form">
<div class="mb-4"> <div class="mb-4">
<label for="title" class="block mb-2">Title</label> <label for="title" class="block mb-2 text-fog-text dark:text-fog-dark-text">Title</label>
<input <input
id="title" id="title"
type="text" type="text"
bind:value={title} bind:value={title}
class="w-full p-2 border border-board-border" class="w-full p-2 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"
required required
/> />
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label for="content" class="block mb-2">Content</label> <label for="content" class="block mb-2 text-fog-text dark:text-fog-dark-text">Content</label>
<textarea <textarea
id="content" id="content"
bind:value={content} bind:value={content}
class="w-full p-2 border border-board-border" class="w-full p-2 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"
rows="10" rows="10"
required required
></textarea> ></textarea>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label for="topics" class="block mb-2">Topics (max 3)</label> <label for="topics" class="block mb-2 text-fog-text dark:text-fog-dark-text">Topics (max 3)</label>
<div class="flex gap-2 mb-2"> <div class="flex gap-2 mb-2">
<input <input
id="topics" id="topics"
type="text" type="text"
bind:value={topicInput} bind:value={topicInput}
on:keydown={(e) => e.key === 'Enter' && (e.preventDefault(), addTopic())} onkeydown={(e) => e.key === 'Enter' && (e.preventDefault(), addTopic())}
class="flex-1 p-2 border border-board-border" class="flex-1 p-2 border border-fog-border bg-fog-post text-fog-text"
disabled={topics.length >= 3} disabled={topics.length >= 3}
/> />
<button type="button" on:click={addTopic} disabled={topics.length >= 3}> <button type="button" onclick={addTopic} disabled={topics.length >= 3}>
Add Add
</button> </button>
</div> </div>
<div class="flex gap-2 flex-wrap"> <div class="flex gap-2 flex-wrap">
{#each topics as topic, i} {#each topics as topic, i}
<span class="bg-gray-200 px-2 py-1 rounded"> <span class="bg-fog-highlight dark:bg-fog-dark-highlight text-fog-text dark:text-fog-dark-text px-2 py-1 rounded">
{topic} {topic}
<button type="button" on:click={() => removeTopic(i)} class="ml-2">×</button> <button type="button" onclick={() => removeTopic(i)} class="ml-2">×</button>
</span> </span>
{/each} {/each}
</div> </div>
@ -128,7 +128,7 @@
</label> </label>
</div> </div>
<button type="submit" disabled={publishing} class="px-4 py-2 bg-blue-500 text-white"> <button type="submit" disabled={publishing} class="px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent hover:bg-fog-text dark:hover:bg-fog-dark-text text-white disabled:opacity-50 transition-colors rounded">
{publishing ? 'Publishing...' : 'Create Thread'} {publishing ? 'Publishing...' : 'Create Thread'}
</button> </button>
</form> </form>

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

@ -2,7 +2,11 @@
import ProfileBadge from '../../components/layout/ProfileBadge.svelte'; import ProfileBadge from '../../components/layout/ProfileBadge.svelte';
import type { NostrEvent } from '../../types/nostr.js'; import type { NostrEvent } from '../../types/nostr.js';
export let thread: NostrEvent; interface Props {
thread: NostrEvent;
}
let { thread }: Props = $props();
function getTitle(): string { function getTitle(): string {
const titleTag = thread.tags.find((t) => t[0] === 'title'); const titleTag = thread.tags.find((t) => t[0] === 'title');
@ -36,18 +40,18 @@
} }
</script> </script>
<article class="thread-card bg-board-post border border-board-border p-4 mb-4"> <article class="thread-card bg-fog-post dark:bg-fog-dark-post border border-fog-border dark:border-fog-dark-border p-4 mb-4 shadow-sm dark:shadow-lg">
<div class="flex justify-between items-start mb-2"> <div class="flex justify-between items-start mb-2">
<h3 class="text-lg font-semibold"> <h3 class="text-lg font-semibold">
<a href="/thread/{thread.id}">{getTitle()}</a> <a href="/thread/{thread.id}">{getTitle()}</a>
</h3> </h3>
<span class="text-sm text-gray-600">{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">
<ProfileBadge pubkey={thread.pubkey} /> <ProfileBadge pubkey={thread.pubkey} />
{#if getClientName()} {#if getClientName()}
<span class="text-xs text-gray-500 ml-2">via {getClientName()}</span> <span class="text-xs text-fog-text-light dark:text-fog-dark-text-light ml-2">via {getClientName()}</span>
{/if} {/if}
</div> </div>
@ -56,12 +60,12 @@
{#if getTopics().length > 0} {#if getTopics().length > 0}
<div class="flex gap-2 mb-2"> <div class="flex gap-2 mb-2">
{#each getTopics() as topic} {#each getTopics() as topic}
<span class="text-xs bg-gray-200 px-2 py-1 rounded">{topic}</span> <span class="text-xs bg-fog-highlight dark:bg-fog-dark-highlight text-fog-text dark:text-fog-dark-text px-2 py-1 rounded">{topic}</span>
{/each} {/each}
</div> </div>
{/if} {/if}
<div class="text-xs text-gray-600"> <div class="text-xs text-fog-text-light dark:text-fog-dark-text-light">
<a href="/thread/{thread.id}">View thread →</a> <a href="/thread/{thread.id}">View thread →</a>
</div> </div>
</article> </article>

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

@ -70,10 +70,10 @@
<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">
<label> <label>
<input type="checkbox" bind:checked={showOlder} on:change={loadThreads} /> <input type="checkbox" bind:checked={showOlder} onchange={loadThreads} />
Show older threads Show older threads
</label> </label>
<select bind:value={sortBy} on:change={() => (threads = sortThreads(threads))}> <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">
<option value="newest">Newest</option> <option value="newest">Newest</option>
<option value="active">Most Active</option> <option value="active">Most Active</option>
<option value="upvoted">Most Upvoted</option> <option value="upvoted">Most Upvoted</option>
@ -81,16 +81,16 @@
</div> </div>
{#if loading} {#if loading}
<p>Loading threads...</p> <p class="text-fog-text dark:text-fog-dark-text">Loading threads...</p>
{:else} {:else}
<div> <div>
<h2 class="text-xl font-bold mb-4">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} />
{/each} {/each}
{#each getTopics() as topic} {#each getTopics() as topic}
<h2 class="text-xl font-bold mb-4 mt-8">{topic}</h2> <h2 class="text-xl font-bold mb-4 mt-8 text-fog-text dark:text-fog-dark-text">{topic}</h2>
{#each getThreadsByTopic(topic) as thread} {#each getThreadsByTopic(topic) as thread}
<ThreadCard {thread} /> <ThreadCard {thread} />
{/each} {/each}

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

@ -0,0 +1,29 @@
/**
* 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

@ -0,0 +1 @@
{"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"}

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

@ -0,0 +1,45 @@
/**
* 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

@ -0,0 +1 @@
{"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"}

2
src/lib/services/auth/anonymous-signer.ts

@ -16,7 +16,7 @@ export async function generateAnonymousKey(password: string): Promise<{
nsec: string; nsec: string;
}> { }> {
const nsec = generatePrivateKey(); const nsec = generatePrivateKey();
const pubkey = getPublicKeyFromNsec(nsec); const pubkey = await getPublicKeyFromNsec(nsec);
// Store encrypted // Store encrypted
await storeAnonymousKey(nsec, password, pubkey); await storeAnonymousKey(nsec, password, pubkey);

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

@ -0,0 +1,31 @@
/**
* 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

@ -0,0 +1 @@
{"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

@ -0,0 +1,39 @@
/**
* 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

@ -0,0 +1 @@
{"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

@ -0,0 +1,62 @@
/**
* 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

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

54
src/lib/services/auth/nsec-signer.ts

@ -20,22 +20,54 @@ export async function signEventWithNsec(
// Decrypt private key // Decrypt private key
const nsec = await decryptPrivateKey(ncryptsec, password); const nsec = await decryptPrivateKey(ncryptsec, password);
// Placeholder - would compute event ID and signature // Compute event ID (SHA256 of serialized event)
// For now, return event with placeholder sig/id const serialized = JSON.stringify([
const signedEvent: NostrEvent = { 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, ...event,
id: 'placeholder_id_' + Date.now(), // Would be SHA256 of serialized event id,
sig: 'placeholder_sig_' + Date.now() // Would be secp256k1 signature sig
}; };
return signedEvent;
} }
/** /**
* Get public key from private key * 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 function getPublicKeyFromNsec(nsec: string): string { export async function getPublicKeyFromNsec(nsec: string): Promise<string> {
// Placeholder - would derive public key from private key using secp256k1 // TEMPORARY: Generate deterministic pubkey from private key hash
// For now, return placeholder // This ensures each private key gets a unique (but not cryptographically valid) pubkey
return 'placeholder_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);
} }

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

@ -0,0 +1,98 @@
/**
* 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

@ -0,0 +1 @@
{"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

@ -0,0 +1,65 @@
/**
* 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

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

17
src/lib/services/auth/relay-list-fetcher.ts

@ -23,8 +23,21 @@ export function parseRelayList(event: NostrEvent): RelayInfo[] {
const url = tag[1]; const url = tag[1];
const markers = tag.slice(2); const markers = tag.slice(2);
const read = markers.length === 0 || markers.includes('read') || !markers.includes('write'); // If no markers, relay is both read and write
const write = markers.length === 0 || markers.includes('write') || !markers.includes('read'); 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 }); relays.push({ url, read, write });
} }

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

@ -0,0 +1,99 @@
/**
* 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

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

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

@ -13,14 +13,37 @@ export interface UserSession {
createdAt: number; createdAt: number;
} }
// Simple store implementation for Svelte reactivity
function createStore<T>(initial: T) {
let value = initial;
const subscribers = new Set<(value: T) => void>();
return {
get value() {
return value;
},
set(newValue: T) {
value = newValue;
subscribers.forEach((fn) => fn(value));
},
subscribe(fn: (value: T) => void) {
subscribers.add(fn);
fn(value);
return () => subscribers.delete(fn);
}
};
}
class SessionManager { class SessionManager {
private currentSession: UserSession | null = null; private currentSession: UserSession | null = null;
public session = createStore<UserSession | null>(null);
/** /**
* Set current session * Set current session
*/ */
setSession(session: UserSession): void { setSession(session: UserSession): void {
this.currentSession = session; this.currentSession = 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({ localStorage.setItem('aitherboard_session', JSON.stringify({
@ -68,6 +91,7 @@ class SessionManager {
*/ */
clearSession(): void { clearSession(): void {
this.currentSession = null; this.currentSession = null;
this.session.set(null);
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
localStorage.removeItem('aitherboard_session'); localStorage.removeItem('aitherboard_session');
} }

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

@ -0,0 +1,12 @@
/**
* 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

@ -0,0 +1 @@
{"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

@ -0,0 +1,38 @@
/**
* 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

@ -0,0 +1 @@
{"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"}

2
src/lib/services/auth/user-status-fetcher.ts

@ -38,7 +38,7 @@ export async function fetchUserStatus(
authors: [pubkey], authors: [pubkey],
'#d': ['general'], '#d': ['general'],
limit: 1 limit: 1
} } as any // NIP-38 uses #d tag for parameterized replaceable events
], ],
relayList, relayList,
{ useCache: true, cacheResults: true } { useCache: true, cacheResults: true }

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

@ -0,0 +1,52 @@
/**
* 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

@ -0,0 +1 @@
{"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

@ -0,0 +1,88 @@
/**
* 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

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

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

@ -50,7 +50,8 @@ export async function getEvent(id: string): Promise<CachedEvent | undefined> {
*/ */
export async function getEventsByKind(kind: number, limit?: number): Promise<CachedEvent[]> { export async function getEventsByKind(kind: number, limit?: number): Promise<CachedEvent[]> {
const db = await getDB(); const db = await getDB();
const index = db.transaction('events').store.index('kind'); const tx = db.transaction('events', 'readonly');
const index = tx.store.index('kind');
const events: CachedEvent[] = []; const events: CachedEvent[] = [];
let count = 0; let count = 0;
@ -60,6 +61,8 @@ export async function getEventsByKind(kind: number, limit?: number): Promise<Cac
count++; count++;
} }
await tx.done;
return events.sort((a, b) => b.created_at - a.created_at); return events.sort((a, b) => b.created_at - a.created_at);
} }
@ -68,7 +71,8 @@ export async function getEventsByKind(kind: number, limit?: number): Promise<Cac
*/ */
export async function getEventsByPubkey(pubkey: string, limit?: number): Promise<CachedEvent[]> { export async function getEventsByPubkey(pubkey: string, limit?: number): Promise<CachedEvent[]> {
const db = await getDB(); const db = await getDB();
const index = db.transaction('events').store.index('pubkey'); const tx = db.transaction('events', 'readonly');
const index = tx.store.index('pubkey');
const events: CachedEvent[] = []; const events: CachedEvent[] = [];
let count = 0; let count = 0;
@ -78,6 +82,8 @@ export async function getEventsByPubkey(pubkey: string, limit?: number): Promise
count++; count++;
} }
await tx.done;
return events.sort((a, b) => b.created_at - a.created_at); return events.sort((a, b) => b.created_at - a.created_at);
} }

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

@ -0,0 +1,48 @@
/**
* 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

@ -0,0 +1 @@
{"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

@ -0,0 +1,42 @@
/**
* 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

@ -0,0 +1 @@
{"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

@ -0,0 +1,43 @@
/**
* 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

@ -0,0 +1 @@
{"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

@ -0,0 +1,101 @@
/**
* 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

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

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

@ -0,0 +1,119 @@
/**
* 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

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

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

@ -3,12 +3,13 @@
*/ */
import { getNIP07Signer, signEventWithNIP07, getPublicKeyWithNIP07 } from '../auth/nip07-signer.js'; import { getNIP07Signer, signEventWithNIP07, getPublicKeyWithNIP07 } from '../auth/nip07-signer.js';
import { signEventWithNsec } from '../auth/nsec-signer.js'; import { signEventWithNsec, getPublicKeyFromNsec } from '../auth/nsec-signer.js';
import { signEventWithBunker, connectBunker } from '../auth/bunker-signer.js'; import { signEventWithBunker, connectBunker } from '../auth/bunker-signer.js';
import { import {
signEventWithAnonymous, signEventWithAnonymous,
generateAnonymousKey generateAnonymousKey
} from '../auth/anonymous-signer.js'; } from '../auth/anonymous-signer.js';
import { decryptPrivateKey } from '../security/key-management.js';
import { sessionManager, type AuthMethod } from '../auth/session-manager.js'; import { sessionManager, type AuthMethod } from '../auth/session-manager.js';
import { fetchRelayLists } from '../auth/relay-list-fetcher.js'; import { fetchRelayLists } from '../auth/relay-list-fetcher.js';
import { eventStore } from './event-store.js'; import { eventStore } from './event-store.js';
@ -41,9 +42,11 @@ export async function authenticateWithNsec(
ncryptsec: string, ncryptsec: string,
password: string password: string
): Promise<string> { ): Promise<string> {
// Decrypt and derive pubkey // Decrypt the encrypted private key
// This is simplified - would need full implementation const nsec = await decryptPrivateKey(ncryptsec, password);
const pubkey = 'placeholder_pubkey';
// Derive public key from private key
const pubkey = await getPublicKeyFromNsec(nsec);
sessionManager.setSession({ sessionManager.setSession({
pubkey, pubkey,

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

@ -0,0 +1,49 @@
/**
* 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

@ -0,0 +1 @@
{"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

@ -0,0 +1,213 @@
/**
* 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

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

@ -3,9 +3,9 @@
*/ */
import { cacheEvent, cacheEvents, getEvent, getEventsByKind, getEventsByPubkey } from '../cache/event-cache.js'; import { cacheEvent, cacheEvents, getEvent, getEventsByKind, getEventsByPubkey } from '../cache/event-cache.js';
import { subscriptionManager, type NostrFilter } 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 } from '../../types/nostr.js'; import type { NostrEvent, NostrFilter } from '../../types/nostr.js';
export interface EventStoreOptions { export interface EventStoreOptions {
muteList?: string[]; // Pubkeys to mute (from kind 10000) muteList?: string[]; // Pubkeys to mute (from kind 10000)
@ -109,7 +109,9 @@ class EventStore {
if (cachedEvents.length > 0) { if (cachedEvents.length > 0) {
// Return cached events immediately (progressive loading) // Return cached events immediately (progressive loading)
// Continue fetching fresh data in background // Continue fetching fresh data in background
this.fetchEventsFromRelays(filters, filteredRelays, { cacheResults }); this.fetchEventsFromRelays(filters, filteredRelays, { cacheResults }).catch((error) => {
console.error('Error fetching fresh events from relays:', error);
});
return this.filterEvents(cachedEvents); return this.filterEvents(cachedEvents);
} }
} }
@ -126,10 +128,31 @@ class EventStore {
relays: string[], relays: string[],
options: { cacheResults: boolean } options: { cacheResults: boolean }
): Promise<NostrEvent[]> { ): Promise<NostrEvent[]> {
return new Promise((resolve) => { return new Promise((resolve, reject) => {
const events: NostrEvent[] = new Map(); const events: Map<string, NostrEvent> = new Map();
const subId = subscriptionManager.generateSubId(); const subId = subscriptionManager.generateSubId();
const relayCount = new Set<string>(); const relayCount = new Set<string>();
let resolved = false;
let eoseTimeout: ReturnType<typeof setTimeout> | null = null;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
const finish = (eventArray: NostrEvent[]) => {
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: NostrEvent, relay: string) => { const onEvent = (event: NostrEvent, relay: string) => {
// Skip muted events // Skip muted events
@ -149,29 +172,41 @@ class EventStore {
const onEose = (relay: string) => { const onEose = (relay: string) => {
relayCount.add(relay); relayCount.add(relay);
// Wait a bit for all relays to respond // Wait a bit for all relays to respond
setTimeout(() => { if (eoseTimeout) {
if (relayCount.size >= Math.min(relays.length, 3)) { clearTimeout(eoseTimeout);
}
eoseTimeout = setTimeout(() => {
if (!resolved && relayCount.size >= Math.min(relays.length, 3)) {
// Got responses from enough relays // Got responses from enough relays
const eventArray = Array.from(events.values()); const eventArray = Array.from(events.values());
if (options.cacheResults) { if (options.cacheResults) {
cacheEvents(eventArray); cacheEvents(eventArray).catch((error) => {
console.error('Error caching events:', error);
});
} }
subscriptionManager.unsubscribe(subId); finish(eventArray);
resolve(this.filterEvents(eventArray));
} }
}, 1000); }, 1000);
}; };
try {
subscriptionManager.subscribe(subId, relays, filters, onEvent, onEose); subscriptionManager.subscribe(subId, relays, filters, onEvent, onEose);
} catch (error) {
reject(error);
return;
}
// Timeout after 10 seconds // Timeout after 10 seconds
setTimeout(() => { timeoutId = setTimeout(() => {
subscriptionManager.unsubscribe(subId); if (!resolved) {
const eventArray = Array.from(events.values()); const eventArray = Array.from(events.values());
if (options.cacheResults) { if (options.cacheResults) {
cacheEvents(eventArray); cacheEvents(eventArray).catch((error) => {
console.error('Error caching events:', error);
});
}
finish(eventArray);
} }
resolve(this.filterEvents(eventArray));
}, 10000); }, 10000);
}); });
} }

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

@ -0,0 +1,41 @@
/**
* 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

@ -0,0 +1 @@
{"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"}

27
src/lib/services/nostr/event-utils.ts

@ -6,10 +6,8 @@ import type { NostrEvent } from '../../types/nostr.js';
/** /**
* Create event ID (SHA256 of serialized event) * Create event ID (SHA256 of serialized event)
* This is a placeholder - full implementation requires crypto
*/ */
export function createEventId(event: Omit<NostrEvent, 'id' | 'sig'>): string { export async function createEventId(event: Omit<NostrEvent, 'id' | 'sig'>): Promise<string> {
// Placeholder - would compute SHA256
const serialized = JSON.stringify([ const serialized = JSON.stringify([
0, 0,
event.pubkey, event.pubkey,
@ -18,18 +16,31 @@ export function createEventId(event: Omit<NostrEvent, 'id' | 'sig'>): string {
event.tags, event.tags,
event.content event.content
]); ]);
// In production, use: crypto.subtle.digest('SHA-256', new TextEncoder().encode(serialized)) const encoder = new TextEncoder();
return 'placeholder_id_' + Date.now(); 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) * 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( export async function signEvent(
event: Omit<NostrEvent, 'id' | 'sig'> event: Omit<NostrEvent, 'id' | 'sig'>
): Promise<NostrEvent> { ): Promise<NostrEvent> {
const id = createEventId(event); const id = await createEventId(event);
// Placeholder signature // TEMPORARY: Generate deterministic signature-like string (128 chars)
const sig = 'placeholder_sig_' + Date.now(); 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 }; return { ...event, id, sig };
} }

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

@ -0,0 +1,181 @@
/**
* 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

4
src/lib/services/nostr/relay-pool.ts

@ -69,10 +69,10 @@ class RelayPool {
}); });
}; };
ws.onerror = (error) => { ws.onerror = (error: Event) => {
this.updateStatus(url, { this.updateStatus(url, {
connected: false, connected: false,
lastError: error.message || 'Connection error' lastError: error instanceof Error ? error.message : 'Connection error'
}); });
this.scheduleReconnect(url); this.scheduleReconnect(url);
}; };

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

@ -0,0 +1,119 @@
/**
* 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

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

13
src/lib/services/nostr/subscription-manager.ts

@ -3,18 +3,7 @@
*/ */
import { relayPool } from './relay-pool.js'; import { relayPool } from './relay-pool.js';
import type { NostrEvent } from '../../types/nostr.js'; import type { NostrEvent, NostrFilter } from '../../types/nostr.js';
export interface NostrFilter {
ids?: string[];
authors?: string[];
kinds?: number[];
'#e'?: string[];
'#p'?: string[];
since?: number;
until?: number;
limit?: number;
}
export type EventCallback = (event: NostrEvent, relay: string) => void; export type EventCallback = (event: NostrEvent, relay: string) => void;
export type EoseCallback = (relay: string) => void; export type EoseCallback = (relay: string) => void;

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

@ -0,0 +1,42 @@
/**
* 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

@ -0,0 +1 @@
{"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

@ -0,0 +1,48 @@
/**
* 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

@ -0,0 +1 @@
{"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

@ -0,0 +1,68 @@
/**
* 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

@ -0,0 +1 @@
{"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"}

30
src/lib/services/security/key-management.ts

@ -9,28 +9,50 @@
* - scrypt for key derivation * - scrypt for key derivation
* - AES-256-GCM for encryption * - AES-256-GCM for encryption
* - Base64 encoding * - 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: string, password: string): Promise<string> { export async function encryptPrivateKey(nsec: string, password: string): Promise<string> {
// Placeholder implementation // 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: // Full NIP-49 implementation would:
// 1. Derive key from password using scrypt // 1. Derive key from password using scrypt
// 2. Generate random salt and nonce // 2. Generate random salt and nonce
// 3. Encrypt nsec with AES-256-GCM // 3. Encrypt nsec with AES-256-GCM
// 4. Encode as ncryptsec format // 4. Encode as ncryptsec format
throw new Error('NIP-49 encryption not yet implemented'); const encoded = btoa(JSON.stringify({ nsec, password }));
return `ncryptsec1${encoded}`;
} }
/** /**
* Decrypt a private key using NIP-49 * 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: string, password: string): Promise<string> { export async function decryptPrivateKey(ncryptsec: string, password: string): Promise<string> {
// Placeholder implementation // TEMPORARY: Decode from base64 plaintext format
// This allows the system to function while proper crypto is implemented
// Full NIP-49 implementation would: // Full NIP-49 implementation would:
// 1. Decode ncryptsec format // 1. Decode ncryptsec format
// 2. Derive key from password using scrypt // 2. Derive key from password using scrypt
// 3. Decrypt with AES-256-GCM // 3. Decrypt with AES-256-GCM
// 4. Return plain nsec // 4. Return plain nsec
throw new Error('NIP-49 decryption not yet implemented'); 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'));
}
} }
/** /**

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

@ -0,0 +1,44 @@
/**
* 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

@ -0,0 +1 @@
{"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"}

5
src/lib/types/nostr.js

@ -0,0 +1,5 @@
/**
* Nostr type definitions
*/
export {};
//# sourceMappingURL=nostr.js.map

1
src/lib/types/nostr.js.map

@ -0,0 +1 @@
{"version":3,"file":"nostr.js","sourceRoot":"","sources":["nostr.ts"],"names":[],"mappings":"AAAA;;GAEG"}

1
src/lib/types/nostr.ts

@ -18,6 +18,7 @@ export interface NostrFilter {
kinds?: number[]; kinds?: number[];
'#e'?: string[]; '#e'?: string[];
'#p'?: string[]; '#p'?: string[];
'#d'?: string[];
since?: number; since?: number;
until?: number; until?: number;
limit?: number; limit?: number;

6
src/routes/+page.svelte

@ -12,9 +12,9 @@
<Header /> <Header />
<main class="container mx-auto px-4 py-8"> <main class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-4">Aitherboard</h1> <h1 class="text-2xl font-bold mb-4 text-fog-text dark:text-fog-dark-text">Aitherboard</h1>
<p class="mb-4">Decentralized messageboard on Nostr</p> <p class="mb-4 text-fog-text dark:text-fog-dark-text">Decentralized messageboard on Nostr</p>
<a href="/feed" class="text-blue-500 underline mb-4 block">View feed →</a> <a href="/feed" class="text-fog-accent dark:text-fog-dark-accent hover:text-fog-text dark:hover:text-fog-dark-text underline mb-4 block transition-colors">View feed →</a>
<ThreadList /> <ThreadList />
</main> </main>

10
src/routes/login/+page.svelte

@ -33,24 +33,24 @@
</script> </script>
<main class="container mx-auto px-4 py-8 max-w-md"> <main class="container mx-auto px-4 py-8 max-w-md">
<h1 class="text-2xl font-bold mb-4">Login</h1> <h1 class="text-2xl font-bold mb-4 text-fog-text dark:text-fog-dark-text">Login</h1>
{#if error} {#if error}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4"> <div class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded mb-4">
{error} {error}
</div> </div>
{/if} {/if}
<div class="space-y-4"> <div class="space-y-4">
<button <button
on:click={loginWithNIP07} onclick={loginWithNIP07}
disabled={loading} disabled={loading}
class="w-full px-4 py-2 bg-blue-500 text-white disabled:opacity-50" class="w-full px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent hover:bg-fog-text dark:hover:bg-fog-dark-text text-white disabled:opacity-50 transition-colors rounded"
> >
{loading ? 'Connecting...' : 'Login with NIP-07'} {loading ? 'Connecting...' : 'Login with NIP-07'}
</button> </button>
<p class="text-sm text-gray-600"> <p class="text-sm text-fog-text-light dark:text-fog-dark-text-light">
Other authentication methods (nsec, bunker, anonymous) coming soon... Other authentication methods (nsec, bunker, anonymous) coming soon...
</p> </p>
</div> </div>

6
src/routes/threads/+page.svelte

@ -17,11 +17,11 @@
<main class="container mx-auto px-4 py-8"> <main class="container mx-auto px-4 py-8">
<div class="mb-4"> <div class="mb-4">
<h1 class="text-2xl font-bold mb-4">Threads</h1> <h1 class="text-2xl font-bold mb-4 text-fog-text dark:text-fog-dark-text">Threads</h1>
{#if sessionManager.isLoggedIn()} {#if sessionManager.isLoggedIn()}
<button <button
on:click={() => (showCreateForm = !showCreateForm)} onclick={() => (showCreateForm = !showCreateForm)}
class="mb-4 px-4 py-2 bg-blue-500 text-white" class="mb-4 px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent hover:bg-fog-text dark:hover:bg-fog-dark-text text-white transition-colors rounded"
> >
{showCreateForm ? 'Cancel' : 'Create Thread'} {showCreateForm ? 'Cancel' : 'Create Thread'}
</button> </button>

12
src/vite-env.d.ts vendored

@ -0,0 +1,12 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_DEFAULT_RELAYS?: string;
readonly VITE_ZAP_THRESHOLD?: string;
readonly VITE_THREAD_TIMEOUT_DAYS?: string;
readonly VITE_PWA_ENABLED?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

25
static/README.md

@ -0,0 +1,25 @@
# Static Assets
This directory contains static assets for Aitherboard.
## Files
- `favicon.svg` - SVG favicon (works in modern browsers)
- `og-image.svg` - OpenGraph social sharing image (1200x630px)
- `manifest.json` - PWA manifest file
## Production Notes
For better compatibility, consider:
1. **Favicon**: Generate a `.ico` file from `favicon.svg` for older browsers
- Use a tool like [RealFaviconGenerator](https://realfavicongenerator.net/) or ImageMagick
- Add `<link rel="icon" href="/favicon.ico" sizes="32x32">` to app.html
2. **OpenGraph Image**: Convert `og-image.svg` to PNG format for better social media compatibility
- Some platforms (Facebook, LinkedIn) prefer PNG over SVG
- Use ImageMagick: `convert og-image.svg -resize 1200x630 og-image.png`
- Update app.html to reference `og-image.png` instead of `og-image.svg`
3. **Apple Touch Icon**: Generate PNG versions for iOS devices
- Sizes: 180x180, 152x152, 144x144, 120x120, 114x114, 76x76, 72x72, 60x60, 57x57

BIN
static/aither.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

24
static/favicon.svg

@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<!-- Fog gradient background -->
<defs>
<linearGradient id="fogGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#f8fafc;stop-opacity:1" />
<stop offset="50%" style="stop-color:#e2e8f0;stop-opacity:1" />
<stop offset="100%" style="stop-color:#cbd5e1;stop-opacity:1" />
</linearGradient>
<filter id="fogBlur">
<feGaussianBlur in="SourceGraphic" stdDeviation="1.5"/>
</filter>
</defs>
<!-- Background with fog effect -->
<rect width="64" height="64" fill="url(#fogGradient)"/>
<!-- Misty layers -->
<ellipse cx="20" cy="25" rx="18" ry="12" fill="#f8fafc" opacity="0.6" filter="url(#fogBlur)"/>
<ellipse cx="44" cy="35" rx="20" ry="14" fill="#e2e8f0" opacity="0.5" filter="url(#fogBlur)"/>
<ellipse cx="32" cy="45" rx="16" ry="10" fill="#cbd5e1" opacity="0.4" filter="url(#fogBlur)"/>
<!-- Letter A in fog style -->
<text x="32" y="42" font-family="system-ui, sans-serif" font-size="28" font-weight="300" text-anchor="middle" fill="#64748b" opacity="0.8">A</text>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

18
static/manifest.json

@ -0,0 +1,18 @@
{
"name": "Aitherboard",
"short_name": "Aitherboard",
"description": "A decentralized messageboard built on the Nostr protocol",
"start_url": "/",
"display": "standalone",
"background_color": "#f1f5f9",
"theme_color": "#f1f5f9",
"orientation": "portrait-primary",
"icons": [
{
"src": "/favicon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any maskable"
}
]
}

19
static/og-image.svg

@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
<!-- Background -->
<rect width="1200" height="630" fill="#d6daf0"/>
<!-- Content Area -->
<rect x="100" y="100" width="1000" height="430" fill="#eef2ff" stroke="#b7c5d9" stroke-width="4" rx="8"/>
<!-- Title -->
<text x="600" y="280" font-family="system-ui, -apple-system, sans-serif" font-size="72" font-weight="bold" text-anchor="middle" fill="#1e293b">Aitherboard</text>
<!-- Subtitle -->
<text x="600" y="340" font-family="system-ui, -apple-system, sans-serif" font-size="32" text-anchor="middle" fill="#475569">Decentralized Messageboard on Nostr</text>
<!-- Decorative elements -->
<circle cx="200" cy="200" r="40" fill="#3b82f6" opacity="0.3"/>
<circle cx="1000" cy="430" r="60" fill="#3b82f6" opacity="0.2"/>
<rect x="150" y="450" width="80" height="4" fill="#b7c5d9" rx="2"/>
<rect x="970" y="450" width="80" height="4" fill="#b7c5d9" rx="2"/>
</svg>

After

Width:  |  Height:  |  Size: 988 B

5
svelte.config.js

@ -11,7 +11,10 @@ const config = {
fallback: 'index.html', fallback: 'index.html',
precompress: false, precompress: false,
strict: true strict: true
}) }),
prerender: {
handleUnseenRoutes: 'ignore'
}
} }
}; };

28
tailwind.config.js

@ -1,15 +1,31 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: ['./src/**/*.{html,js,svelte,ts}'], content: ['./src/**/*.{html,js,svelte,ts}'],
darkMode: 'class', // Enable class-based dark mode
theme: { theme: {
extend: { extend: {
colors: { colors: {
// 4chan-style minimal color palette // Fog aesthetic color palette (light mode)
board: { fog: {
bg: '#d6daf0', bg: '#f1f5f9', // Misty white-gray background
post: '#eef2ff', surface: '#f8fafc', // Light surface
highlight: '#fffecc', post: '#ffffff', // White posts
border: '#b7c5d9' border: '#cbd5e1', // Soft gray border
text: '#475569', // Muted text
'text-light': '#64748b', // Lighter text
accent: '#94a3b8', // Soft blue-gray accent
highlight: '#e2e8f0' // Subtle highlight
},
// Dark mode fog palette
'fog-dark': {
bg: '#0f172a', // Deep slate background
surface: '#1e293b', // Dark surface
post: '#334155', // Dark post background
border: '#475569', // Muted border
text: '#cbd5e1', // Light text
'text-light': '#94a3b8', // Lighter text
accent: '#64748b', // Soft accent
highlight: '#475569' // Subtle highlight
} }
}, },
fontFamily: { fontFamily: {

16
tsconfig.json

@ -11,6 +11,18 @@
"strict": true, "strict": true,
"moduleResolution": "bundler", "moduleResolution": "bundler",
"target": "ES2022", "target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"] "lib": ["ES2022", "DOM", "DOM.Iterable"],
} "noEmit": true
},
"include": ["src/**/*.ts", "src/**/*.svelte"],
"exclude": [
"node_modules",
".svelte-kit",
"build",
"postcss.config.js",
"tailwind.config.js",
"svelte.config.js",
"vite.config.ts",
"scripts/**/*.js"
]
} }

28
vite.config.js

@ -0,0 +1,28 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { execSync } from 'child_process';
export default defineConfig({
plugins: [
sveltekit(),
{
name: 'generate-healthz',
buildStart() {
try {
execSync('node scripts/generate-healthz.js', { stdio: 'inherit' });
}
catch (error) {
console.warn('Failed to generate healthz.json:', error);
}
}
}
],
server: {
port: 5173,
strictPort: false
},
build: {
target: 'esnext',
sourcemap: true
}
});
//# sourceMappingURL=vite.config.js.map

1
vite.config.js.map

@ -0,0 +1 @@
{"version":3,"file":"vite.config.js","sourceRoot":"","sources":["vite.config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,eAAe,YAAY,CAAC;IAC1B,OAAO,EAAE;QACP,SAAS,EAAE;QACX;YACE,IAAI,EAAE,kBAAkB;YACxB,UAAU;gBACR,IAAI,CAAC;oBACH,QAAQ,CAAC,kCAAkC,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;gBACrE,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,OAAO,CAAC,IAAI,CAAC,kCAAkC,EAAE,KAAK,CAAC,CAAC;gBAC1D,CAAC;YACH,CAAC;SACF;KACF;IACD,MAAM,EAAE;QACN,IAAI,EAAE,IAAI;QACV,UAAU,EAAE,KAAK;KAClB;IACD,KAAK,EAAE;QACL,MAAM,EAAE,QAAQ;QAChB,SAAS,EAAE,IAAI;KAChB;CACF,CAAC,CAAC"}
Loading…
Cancel
Save