Skip to content

Commit 4688679

Browse files
Initial implementation of browser-based Nostr Git Browser
0 parents  commit 4688679

2 files changed

Lines changed: 1161 additions & 0 deletions

File tree

DESIGN.md

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
# Nostr Git Browser - Design Document
2+
3+
A browser-based git repository viewer and sync client using Nostr for real-time updates.
4+
5+
## Status: ✅ Implemented
6+
7+
Working prototype in `index.html` (~750 lines, single file, no build step).
8+
9+
**Features working:**
10+
- Clone repositories to IndexedDB
11+
- Browse files with expandable tree
12+
- View files (raw text or HTML preview)
13+
- HTML preview with local JSON injection (fetch interception)
14+
- Auto-sync on Nostr 30617 events from trusted pubkeys
15+
- Multi-relay WebSocket connections with status indicators
16+
- Add/remove repositories via modal
17+
- Configuration persisted to localStorage
18+
19+
## Overview
20+
21+
This project brings the functionality of [nostr-git-sync](https://github.com/JavaScriptSolidServer/nostr-git-sync) to the browser, allowing users to:
22+
23+
1. Subscribe to multiple git repositories via Nostr (NIP-34)
24+
2. Clone/sync repositories to browser IndexedDB
25+
3. Browse repository files with a visual UI
26+
4. Auto-update when new commits are published
27+
28+
```
29+
┌─────────────────┐ WebSocket ┌─────────────────┐
30+
│ │◄──────────────────►│ Nostr Relays │
31+
│ Browser │ └─────────────────┘
32+
│ │ HTTP/fetch ┌─────────────────┐
33+
│ IndexedDB │◄──────────────────►│ Git Server │
34+
│ /sync/repo1/ │ (clone/pull) │ (JSS, GitHub) │
35+
│ /sync/repo2/ │ └─────────────────┘
36+
└─────────────────┘
37+
```
38+
39+
## Core Concepts
40+
41+
### NIP-34 Events
42+
43+
**Kind 30617** - Repository Announcement
44+
```json
45+
{
46+
"kind": 30617,
47+
"tags": [
48+
["d", "repo-id"],
49+
["name", "repo-name"],
50+
["clone", "https://server.com/repo"],
51+
["refs/heads/main", "abc123..."]
52+
],
53+
"content": "Latest commit: message",
54+
"pubkey": "publisher-pubkey"
55+
}
56+
```
57+
58+
When a trusted pubkey publishes a 30617 event for a tracked repo, the browser syncs.
59+
60+
### Storage Structure
61+
62+
Uses IndexedDB via LightningFS (isomorphic-git's filesystem):
63+
64+
```
65+
IndexedDB: nostr-git-browser
66+
├── /sync/
67+
│ ├── /20260202/
68+
│ │ ├── .git/
69+
│ │ ├── index.html
70+
│ │ ├── gitmark.html
71+
│ │ ├── webledgers.json
72+
│ │ └── .well-known/txo/txo.json
73+
│ └── /another-repo/
74+
│ └── ...
75+
```
76+
77+
### Configuration
78+
79+
Stored in localStorage as `nostr-git-config`:
80+
81+
```json
82+
{
83+
"relays": [
84+
"wss://relay.damus.io",
85+
"wss://nos.lol",
86+
"wss://melvin.me/relay"
87+
],
88+
"repos": {
89+
"20260202": {
90+
"cloneUrl": "https://melvin.me/public/git/sync/20260202",
91+
"branch": "gh-pages",
92+
"trusted": ["d769d2b81c051d2f2c0b437d0ffe39e00ff0f7161b520f0bd30811f4c057795f"]
93+
}
94+
}
95+
}
96+
```
97+
98+
## Architecture
99+
100+
### Single-File Implementation
101+
102+
The entire application is contained in `index.html` with inline CSS and JavaScript:
103+
104+
```
105+
index.html (~750 lines)
106+
├── <style> # CSS (layout, components, modals)
107+
├── <script type="module">
108+
│ ├── Imports # Preact, htm, isomorphic-git, LightningFS
109+
│ ├── Config # localStorage load/save
110+
│ ├── Git Ops # clone, pull, listFiles, readFile
111+
│ ├── Nostr # WebSocket relay connections, subscriptions
112+
│ ├── Components # Preact functional components
113+
│ │ ├── Header # App header with relay status
114+
│ │ ├── Sidebar # Repository list
115+
│ │ ├── FileTree # Expandable file browser
116+
│ │ ├── FileView # File content viewer with preview
117+
│ │ ├── AddRepoModal
118+
│ │ └── Toast # Notifications
119+
│ └── App # Main component, state management
120+
```
121+
122+
### Dependencies
123+
124+
All loaded via CDN (esm.sh) - **no build step required**:
125+
126+
```javascript
127+
import { h, render } from 'https://esm.sh/preact@10.19.3';
128+
import { useState, useEffect, useCallback, useMemo } from 'https://esm.sh/preact@10.19.3/hooks';
129+
import htm from 'https://esm.sh/htm@3.1.1';
130+
import git from 'https://esm.sh/isomorphic-git@1.25.3';
131+
import http from 'https://esm.sh/isomorphic-git@1.25.3/http/web';
132+
import LightningFS from 'https://esm.sh/@isomorphic-git/lightning-fs@4.6.0';
133+
```
134+
135+
**Why Preact + htm:**
136+
- No build tools (Webpack, Vite, etc.)
137+
- Single HTML file
138+
- JSX-like syntax via tagged templates
139+
- Tiny footprint (3kb vs React's 40kb+)
140+
- Hooks support
141+
142+
## Key Features
143+
144+
### 1. Git Operations
145+
146+
```javascript
147+
// Clone repository
148+
await git.clone({
149+
fs, http, dir,
150+
url: cloneUrl,
151+
ref: branch,
152+
singleBranch: true,
153+
depth: 1
154+
});
155+
156+
// Pull updates
157+
await git.pull({
158+
fs, http, dir,
159+
ref: branch,
160+
singleBranch: true,
161+
author: { name: 'browser', email: 'browser@local' }
162+
});
163+
```
164+
165+
### 2. Nostr Relay Connections
166+
167+
```javascript
168+
function connectRelays(relays, repoIds, trusted, onEvent) {
169+
for (const url of relays) {
170+
const ws = new WebSocket(url);
171+
ws.onopen = () => {
172+
// Subscribe to kind 30617 for tracked repos
173+
ws.send(JSON.stringify([
174+
"REQ", "sync",
175+
{ kinds: [30617], "#d": repoIds, authors: trusted }
176+
]));
177+
};
178+
ws.onmessage = (msg) => {
179+
const [type, , event] = JSON.parse(msg.data);
180+
if (type === 'EVENT') onEvent(event);
181+
};
182+
}
183+
}
184+
```
185+
186+
### 3. HTML Preview with Fetch Interception
187+
188+
When viewing HTML files, the app injects a script that intercepts `fetch()` calls for local JSON files:
189+
190+
```javascript
191+
// Scan HTML for fetch() patterns
192+
const fetchMatches = content.matchAll(/fetch\s*\(\s*['"]([^'"?]+)/g);
193+
for (const match of fetchMatches) {
194+
const fetchPath = match[1];
195+
const json = await readFile(repoId, fetchPath);
196+
localFiles[fetchPath] = JSON.parse(json);
197+
}
198+
199+
// Inject fetch wrapper into HTML before rendering in iframe
200+
const fetchWrapper = '<script>' +
201+
'window.__localFiles = ' + JSON.stringify(localFiles) + ';' +
202+
'const _fetch = window.fetch;' +
203+
'window.fetch = (url, ...args) => {' +
204+
' const name = String(url).split("?")[0];' +
205+
' if (window.__localFiles[name] !== undefined) {' +
206+
' return Promise.resolve(new Response(JSON.stringify(window.__localFiles[name])));' +
207+
' }' +
208+
' return _fetch(url, ...args);' +
209+
'};' +
210+
'<' + '/script>';
211+
```
212+
213+
This allows HTML files like `index.html` (WebLedger) and `gitmark.html` to display real data from their companion JSON files stored in IndexedDB.
214+
215+
### 4. File Tree with Lazy Loading
216+
217+
```javascript
218+
function FileTree({ files, selected, expanded, onSelect, onToggle, depth = 0 }) {
219+
return html`
220+
${files.map(entry => html`
221+
<div key=${entry.path}>
222+
<div
223+
class="file-entry ${selected === entry.path ? 'selected' : ''}"
224+
style="padding-left: ${12 + depth * 16}px"
225+
onClick=${() => entry.type === 'dir' ? onToggle(entry.path) : onSelect(entry.path)}
226+
>
227+
<span>${entry.type === 'dir' ? (expanded.has(entry.path) ? '📂' : '📁') : '📄'}</span>
228+
<span>${entry.name}</span>
229+
</div>
230+
${entry.type === 'dir' && expanded.has(entry.path) && entry.children && html`
231+
<${FileTree} files=${entry.children} ... depth=${depth + 1} />
232+
`}
233+
</div>
234+
`)}
235+
`;
236+
}
237+
```
238+
239+
## User Interface
240+
241+
### Layout
242+
243+
```
244+
┌─────────────────────────────────────────────────────────────────┐
245+
│ 🔗 Nostr Git Browser ● nos.lol ● damus ● melvin │
246+
├────────────────────┬────────────────────────────────────────────┤
247+
│ │ │
248+
│ REPOSITORIES │ index.html [Raw] │
249+
│ ─────────────── │ ───────────────────────────────────────── │
250+
│ │ │
251+
│ 📁 20260202 │ ┌─────────────────────────────────────┐ │
252+
│ 📂 .well-known │ │ WebLedger │ │
253+
│ 📂 txo │ │ 2026-02-02 | Updated: 4:53 PM │ │
254+
│ txo.json │ │ │ │
255+
│ gitmark.html │ │ ENTRIES │ │
256+
│ index.html ◄ │ │ did:nostr:de7ecd... 1,242,035 │ │
257+
│ mark.sh │ │ │ │
258+
│ sync.sh │ │ TOTAL: 1,242,035 sats │ │
259+
│ SYSTEM.md │ └─────────────────────────────────────┘ │
260+
│ webledgers.json│ │
261+
│ │ │
262+
│ [+ Add Repository]│ │
263+
└────────────────────┴────────────────────────────────────────────┘
264+
```
265+
266+
### Status Indicators
267+
268+
- 🟢 Connected to relay (green dot in header)
269+
- 🟡 Syncing (spinner on repo)
270+
- Toast notifications for sync events
271+
272+
### Interactions
273+
274+
1. **Add Repo**: Click [+ Add Repository] → Modal → Enter details → Clone starts
275+
2. **Browse Files**: Click folder to expand, click file to view
276+
3. **View HTML**: Toggle Raw/Preview button for rendered view
277+
4. **Auto-sync**: Nostr events from trusted pubkeys trigger pull
278+
279+
## Technical Considerations
280+
281+
### CORS
282+
283+
Git servers must allow CORS for browser fetch:
284+
- JSS has CORS enabled by default
285+
- GitHub raw URLs work
286+
- May need CORS proxy for some servers
287+
288+
### Storage Limits
289+
290+
IndexedDB limits vary by browser:
291+
- Chrome: ~80% of available disk
292+
- Firefox: ~50% of available disk
293+
- Safari: ~1GB default
294+
295+
### Branch Handling
296+
297+
The app uses the configured branch for both clone and pull operations:
298+
```javascript
299+
await git.clone({ ..., ref: branch, singleBranch: true });
300+
await git.pull({ ..., ref: branch, singleBranch: true });
301+
```
302+
303+
### Security
304+
305+
- Only sync from trusted pubkeys (configurable per repo)
306+
- HTML previews run in sandboxed iframe with `sandbox="allow-scripts"`
307+
- Fetch interception only provides read access to IndexedDB files
308+
309+
## Running
310+
311+
Just open `index.html` in a browser:
312+
313+
```bash
314+
cd nostr-git-browser
315+
python -m http.server 3006
316+
# or
317+
npx serve .
318+
```
319+
320+
Then visit `http://localhost:3006`
321+
322+
No build step, no npm install, no configuration files needed.
323+
324+
## Example: Add a Repository
325+
326+
1. Click **"+ Add Repository"**
327+
2. Enter:
328+
- **ID**: `20260202`
329+
- **URL**: `https://melvin.me/public/git/sync/20260202`
330+
- **Branch**: `gh-pages`
331+
- **Trusted**: `d769d2b81c051d2f2c0b437d0ffe39e00ff0f7161b520f0bd30811f4c057795f`
332+
3. Click **Add**
333+
4. Repository clones to IndexedDB
334+
5. Browse files, view index.html with live WebLedger data
335+
336+
## Future Improvements
337+
338+
- [ ] Delete repository button
339+
- [ ] Settings panel for relay configuration
340+
- [ ] Syntax highlighting for code files
341+
- [ ] Commit history viewer
342+
- [ ] Diff viewer
343+
- [ ] Export/import configuration
344+
- [ ] Service worker for offline support
345+
- [ ] Storage usage indicator
346+
347+
## References
348+
349+
- [isomorphic-git](https://isomorphic-git.org/) - Git implementation in JavaScript
350+
- [LightningFS](https://github.com/isomorphic-git/lightning-fs) - IndexedDB filesystem
351+
- [NIP-34](https://github.com/nostr-protocol/nips/blob/master/34.md) - Git over Nostr
352+
- [nostr-git-sync](https://github.com/JavaScriptSolidServer/nostr-git-sync) - Server-side equivalent
353+
- [Preact](https://preactjs.com/) - Fast 3kB React alternative
354+
- [htm](https://github.com/developit/htm) - JSX-like syntax with template literals
355+
356+
## License
357+
358+
MIT

0 commit comments

Comments
 (0)