@dtudury/streamo 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {}
@@ -4,7 +4,11 @@
4
4
  "Bash(git add *)",
5
5
  "Bash(git commit -m ' *)",
6
6
  "Bash(git push *)",
7
- "Bash(gh repo *)"
7
+ "Bash(gh repo *)",
8
+ "Bash(git rm *)",
9
+ "Bash(node --test)",
10
+ "Bash(node --input-type=module --eval \"import './bin/streamo.js'\" --help)",
11
+ "Bash(node bin/streamo.js --help)"
8
12
  ]
9
13
  }
10
14
  }
package/.env.dev ADDED
@@ -0,0 +1,4 @@
1
+ STREAMO_NAME=streamo-dev
2
+ STREAMO_USERNAME=dev
3
+ STREAMO_PASSWORD=dev
4
+ STREAMO_KEY_ITERATIONS=1
package/CLAUDE.md ADDED
@@ -0,0 +1,73 @@
1
+ # streamo — Claude context
2
+
3
+ ## what this project is
4
+
5
+ `@dtudury/streamo` is a peer-to-peer sync library with a reactive UI layer. The central
6
+ promise: no server holds authority over your data or your identity. Keys are derived
7
+ deterministically from credentials (no files to manage), every write is signed and
8
+ append-only, and the server is a relay — not a gatekeeper.
9
+
10
+ ## the face of the project
11
+
12
+ These files are how people discover and understand streamo. Keep them in sync
13
+ after any meaningful change — not as an afterthought, but as part of the work:
14
+
15
+ - **`README.md`** — npm/GitHub landing page; imports, framing, and examples must reflect
16
+ the current package name (`@dtudury/streamo`) and current capabilities
17
+ - **`public/index.html`** — browser homepage; feature list and app cards should match
18
+ the README's framing
19
+ - **`package.json`** — version, name (`@dtudury/streamo`), description, and keywords;
20
+ version bumps immediately after the user says they've published
21
+ - **`ROADMAP.md`** — public on GitHub; mark items done when they ship, update "start
22
+ here" to the next priority, keep the "toward 1.0" list current
23
+
24
+ Stale public-facing docs erode trust faster than bugs do.
25
+
26
+ ## publish rhythm
27
+
28
+ The user publishes to npm manually and notifies Claude when done. The moment they say
29
+ they've published, bump the patch version in `package.json`, commit, and push — before
30
+ starting any other work. This ritual matters to them.
31
+
32
+ ## language and framing
33
+
34
+ Use this framing consistently across all public-facing text:
35
+
36
+ 1. **No server holds authority** — the server is a relay, not a gatekeeper
37
+ 2. **Your identity travels with you** — same credentials, same keypair, everywhere; no
38
+ key files, no seed phrases, no backup ritual
39
+ 3. **Every write is provably yours** — signed commits, append-only, permanent
40
+ 4. **Content-addressed** — data identified by what it is, not where it lives
41
+
42
+ "Content-addressed" is technically important but not the lead. Start with ownership.
43
+
44
+ ## architecture notes
45
+
46
+ - `Streamo` — content-addressed, append-only byte store with self-describing codec
47
+ - `Repo` — wraps Streamo; every `set()` is a signed commit (message, date, address, parent)
48
+ - `Signer` — deterministic secp256k1 keypairs via PBKDF2 from username + password
49
+ - `Recaller` — fine-grained reactive dependency tracker; `watch(name, f)` / `unwatch(f)`
50
+ - `h` — tagged template literal HTML parser → HElement / HText virtual tree
51
+ - `mount` — reactive DOM renderer; slots are reactive cells; elements recycled by
52
+ `data-key` then tag on re-render; removed nodes cleaned up via `recaller.unwatch()`;
53
+ exports `dismount(root, recaller)` for custom element cleanup
54
+ - `StreamoComponent` — base class for hot-reloadable custom elements; `componentKey`
55
+ generates address-based names; `defineComponent` registers render functions; function
56
+ components `(props) => nodes` work directly as tags in `h` with no class needed
57
+ - `registrySync` — bidirectional multi-repo sync over a single WebSocket; works in Node
58
+ and browser; content-driven discovery via `follow`
59
+ - CLI `--chat-room` flag — when combined with `--web`, auto-accepts member announcements
60
+ and stores the member list in the server's own repo (backed by `archiveSync`);
61
+ the server's public key is the room key; `chat-server.js` is retired
62
+
63
+ ## what's next (toward 1.0)
64
+
65
+ 1. Chat signing — wire `repo.sign()` so messages are cryptographically verified
66
+ 2. Rebuild the browser app with `h` / `mount`
67
+
68
+ ## commit style
69
+
70
+ Commit and push at the end of every response that makes a change. Over-commit rather
71
+ than over-think. Co-author line on every commit:
72
+
73
+ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
package/README.md CHANGED
@@ -2,25 +2,25 @@
2
2
 
3
3
  > every device is an equal author
4
4
 
5
- Streamo is a content-addressed, cryptographically signed, peer-to-peer sync library. There is no central server your keypair is your identity, your commit log is the source of truth, and every connected peer sees the same history.
5
+ Streamo is a peer-to-peer sync library built around a simple promise: **no server holds authority over your data or your identity.** The server is a relay, not a gatekeeper. Your keypair is your identity — derived from your credentials, not stored in a file. Your commit log is the source of truth, and every connected peer sees the same history.
6
6
 
7
7
  ## core ideas
8
8
 
9
- - **Content-addressed** — data is identified by what it is, not where it lives. The same value always lands at the same address; deduplication and diffing are free.
10
- - **Signed** every write is authenticated with a secp256k1 keypair derived from your username and password. Peers reject unsigned or mis-signed data.
11
- - **Append-only** — history is never rewritten. Every commit is permanent and verifiable.
12
- - **P2P sync** — repos replicate over WebSocket. The server is just another peer; disconnect it and the data is still yours.
9
+ - **No server holds authority** — the server is a relay; your data lives on your devices and can't be seized or censored. Disconnect the server and everything is still yours.
10
+ - **Your identity travels with you** keys are derived with PBKDF2 from your username and password. Same credentials, same keypair, everywhere — no key files, no seed phrases, no backup ritual.
11
+ - **Every write is provably yours** — commits are signed with your keypair and append-only. History is permanent and can't be forged; peers reject unsigned or mis-signed data.
12
+ - **Content-addressed** — data is identified by what it is, not where it lives. The same value always lands at the same address; deduplication and diffing are structural.
13
13
 
14
14
  ## install
15
15
 
16
16
  ```bash
17
- npm install streamo
17
+ npm install @dtudury/streamo
18
18
  ```
19
19
 
20
20
  Or run the CLI directly:
21
21
 
22
22
  ```bash
23
- npx streamo --help
23
+ npx @dtudury/streamo --help
24
24
  ```
25
25
 
26
26
  ## cli
@@ -56,8 +56,8 @@ streamo --env-file .env
56
56
  ### Streamo — reactive append-only store
57
57
 
58
58
  ```js
59
- import { Streamo } from 'streamo/public/streamo/Streamo.js'
60
- import { Recaller } from 'streamo/public/streamo/utils/Recaller.js'
59
+ import { Streamo } from '@dtudury/streamo/public/streamo/Streamo.js'
60
+ import { Recaller } from '@dtudury/streamo/public/streamo/utils/Recaller.js'
61
61
 
62
62
  const store = new Streamo()
63
63
  store.set({ name: 'alice', score: 42 })
@@ -72,20 +72,23 @@ Values are encoded with a self-describing codec (strings, numbers, dates, boolea
72
72
  `Repo` wraps a `Streamo` so every `set()` becomes a commit — message, date, data address, and parent pointer. The raw commit log is what syncs over the wire.
73
73
 
74
74
  ```js
75
- import { Repo } from 'streamo/public/streamo/Repo.js'
75
+ import { Repo } from '@dtudury/streamo/public/streamo/Repo.js'
76
76
 
77
77
  const repo = new Repo()
78
+ repo.attachSigner(signer, 'my-dataset') // auto-sign every commit
78
79
  repo.set({ name: 'alice', messages: [] })
79
80
  repo.get('name') // 'alice'
80
81
  repo.lastCommit // { message: '', date: Date, dataAddress: n, parent: n|undefined }
81
82
  [...repo.history()] // newest-first iterator over commits
82
83
  ```
83
84
 
85
+ Signature chunks travel in the byte stream automatically — peers running `registrySync` or `originSync` verify every signature on receipt and reject data that doesn't match the repo's public key.
86
+
84
87
  ### Signer — deterministic identity
85
88
 
86
89
  ```js
87
- import { Signer } from 'streamo/public/streamo/Signer.js'
88
- import { bytesToHex } from 'streamo/public/streamo/utils.js'
90
+ import { Signer } from '@dtudury/streamo/public/streamo/Signer.js'
91
+ import { bytesToHex } from '@dtudury/streamo/public/streamo/utils.js'
89
92
 
90
93
  const signer = new Signer('alice', 'my-password')
91
94
  const { publicKey } = await signer.keysFor('my-dataset')
@@ -97,8 +100,8 @@ Keys are derived with PBKDF2 so the same username + password always produces the
97
100
  ### RepoRegistry — multi-repo store
98
101
 
99
102
  ```js
100
- import { RepoRegistry } from 'streamo/public/streamo/RepoRegistry.js'
101
- import { archiveSync } from 'streamo/public/streamo/archiveSync.js'
103
+ import { RepoRegistry } from '@dtudury/streamo/public/streamo/RepoRegistry.js'
104
+ import { archiveSync } from '@dtudury/streamo/public/streamo/archiveSync.js'
102
105
 
103
106
  const registry = new RepoRegistry(async key => {
104
107
  const repo = new Repo()
@@ -112,7 +115,7 @@ const repo = await registry.open(publicKeyHex)
112
115
  ### registrySync — peer sync over WebSocket
113
116
 
114
117
  ```js
115
- import { registrySync } from 'streamo/public/streamo/registrySync.js'
118
+ import { registrySync } from '@dtudury/streamo/public/streamo/registrySync.js'
116
119
 
117
120
  const session = await registrySync(registry, 'localhost', 8080, {
118
121
  // only sync repos you care about
@@ -134,9 +137,9 @@ session.announce(myKey, rootKey) // tell interested peers about your repo
134
137
  ### h + mount — reactive UI
135
138
 
136
139
  ```js
137
- import { h } from 'streamo/public/streamo/h.js'
138
- import { mount } from 'streamo/public/streamo/mount.js'
139
- import { Recaller } from 'streamo/public/streamo/utils/Recaller.js'
140
+ import { h } from '@dtudury/streamo/public/streamo/h.js'
141
+ import { mount } from '@dtudury/streamo/public/streamo/mount.js'
142
+ import { Recaller } from '@dtudury/streamo/public/streamo/utils/Recaller.js'
140
143
 
141
144
  const recaller = new Recaller('app')
142
145
 
@@ -148,7 +151,21 @@ mount(h`
148
151
  `, document.body, recaller)
149
152
  ```
150
153
 
151
- Functions interpolated as `${() => ...}` are reactive cells — they re-run automatically whenever the data they read changes. No virtual DOM diffing; only the exact DOM nodes bound to changed data update.
154
+ Functions interpolated as `${() => ...}` are reactive cells — they re-run automatically whenever the data they read changes. No virtual DOM diffing; only the exact DOM nodes bound to changed data update. Elements are recycled across re-renders by `data-key` (or tag as a fallback), so user input and focus survive list reorders. SVG namespaces propagate automatically — `` h`<svg><path d="..."/></svg>` `` works without any extra wiring. `class` accepts an array (`['btn', isActive && 'active']`) or an object (`{btn: true, active: false}`); falsy entries are filtered out.
155
+
156
+ Any function can be used directly as a tag — it receives `{ ...attrs, children }` and returns virtual nodes:
157
+
158
+ ```js
159
+ import { StreamoComponent, componentKey, defineComponent } from '@dtudury/streamo/public/streamo/StreamoComponent.js'
160
+
161
+ function Card ({ title, children }) {
162
+ return h`<div class="card"><h2>${title}</h2>${children}</div>`
163
+ }
164
+
165
+ mount(h`<${Card} title="Hello"><p>hi</p></${Card}>`, document.body, recaller)
166
+ ```
167
+
168
+ For hot-reloading, `componentKey(prefix, address)` and `defineComponent(name, fn)` pair a content address to a unique custom element name. A new file version gets a new name; stale elements are naturally orphaned and cleaned up without any explicit bookkeeping.
152
169
 
153
170
  ## sync backends
154
171
 
@@ -165,17 +182,18 @@ Functions interpolated as `${() => ...}` are reactive cells — they re-run auto
165
182
  ## chat example
166
183
 
167
184
  ```bash
168
- # start the server
169
- node public/streamo/chat-server.js 8080
185
+ # start the server — its public key becomes the room key
186
+ STREAMO_NAME=my-chat STREAMO_USERNAME=relay STREAMO_PASSWORD=secret \
187
+ node public/apps/chat/server.js
170
188
 
171
189
  # join from the browser
172
- open http://localhost:8080
190
+ open http://localhost:8080/apps/chat/
173
191
 
174
192
  # join from the terminal
175
193
  node public/streamo/chat-cli.js alice secret localhost 8080
176
194
  ```
177
195
 
178
- Each participant owns their own message stream. The server holds only a root repo listing members; it has no special authority over anyone's data.
196
+ Each participant owns their own message stream. The server is just another streamo node — it holds the member list in its own repo and auto-accepts anyone who announces to it. Its public key is the room address. No special authority, no hidden state.
179
197
 
180
198
  ## tests
181
199
 
@@ -189,6 +207,10 @@ node --test public/streamo/Repo.test.js # single file
189
207
  See [ROADMAP.md](./ROADMAP.md) for what's been built, what's next, and what we're
190
208
  aiming at for 1.0.
191
209
 
210
+ ## collaboration
211
+
212
+ Built with significant AI collaboration via [Claude Code](https://claude.ai/code). Human-directed; Claude is a co-author and contributor, not an autonomous builder.
213
+
192
214
  ## license
193
215
 
194
216
  AGPL-3.0-only
package/ROADMAP.md CHANGED
@@ -5,7 +5,7 @@ picture of where the project is and where it's headed.
5
5
 
6
6
  ---
7
7
 
8
- ## where we are (0.1.0)
8
+ ## where we are (0.2.0)
9
9
 
10
10
  The foundation is solid and working. Here's what's in:
11
11
 
@@ -14,7 +14,8 @@ The foundation is solid and working. Here's what's in:
14
14
  self-describing codec. Same value always encodes to the same bytes; dedup and
15
15
  diffing are free.
16
16
  - `Repo` — every write is a signed commit. Message, date, data address, parent.
17
- The full history is always there.
17
+ The full history is always there. `attachSigner(signer, name)` enables
18
+ automatic signing after every commit; concurrent commits are batched safely.
18
19
  - `Signer` — deterministic secp256k1 keypairs from username + password via PBKDF2.
19
20
  No key files to manage; same credentials always produce the same identity.
20
21
  - `Recaller` — fine-grained reactive dependency tracker. Watchers re-run only when
@@ -32,51 +33,46 @@ The foundation is solid and working. Here's what's in:
32
33
  any persistence.
33
34
 
34
35
  **UI layer**
35
- - `h` — tagged template literal parser. Turns `h\`<div class=${cls}>...\`` into a
36
+ - `h` — tagged template literal parser. Turns `` h`<div class=${cls}>...` `` into a
36
37
  virtual tree of `HElement` / `HText` / slot nodes.
37
38
  - `mount` — reactive DOM renderer. Slots that are functions re-run automatically
38
39
  when the data they read changes. No virtual DOM diffing — only the exact nodes
39
- bound to mutated paths update.
40
+ bound to mutated paths update. Watcher cleanup is precise: removed nodes are
41
+ unwatched before removal so watchers never accumulate. Elements are recycled
42
+ across re-renders by `data-key` (exact) then tag (positional fallback), so user
43
+ input and focus survive list reorders. SVG namespaces propagate automatically —
44
+ `` h`<svg><path/></svg>` `` just works. `class` accepts an array
45
+ (`['btn', isActive && 'active']`) or an object (`{btn: true, active: false}`).
46
+ - `StreamoComponent` — base class for hot-reloadable custom element components.
47
+ Function components (`(props) => nodes`) work directly as tags in `h`. For
48
+ hot-reloading, `componentKey(prefix, address)` and `defineComponent(name, fn)`
49
+ pair a content address to a unique custom element name — a new file version gets
50
+ a new name, stale elements are naturally orphaned and cleaned up.
40
51
 
41
52
  **Apps**
42
- - Chat — full p2p messaging app. Each participant owns their own message stream;
43
- the server is just a relay and holds no special authority. Runs in the browser
44
- and from the terminal (`chat-cli.js`).
53
+ - Chat — full p2p messaging app. Each participant owns their own signed message
54
+ stream. `public/apps/chat/server.js` is the standalone server its public key
55
+ is the room address, its member list is in its own repo, and it has no special
56
+ authority over anyone's data. Runs in the browser and from the terminal
57
+ (`chat-cli.js`).
45
58
  - Homepage at `public/index.html`.
46
- - `npm run serve` — static file server for `public/`.
59
+ - `StreamoServer` — reusable class that wraps signer, registry, and all sync
60
+ methods behind a clean API. `bin/streamo.js` is now a thin CLI parser on top
61
+ of it; `public/apps/chat/server.js` is a standalone chat server using the
62
+ same class.
63
+ - `npm run serve` — starts a streamo node (with REPL) using `.env.dev`
64
+ credentials. The dev server is a real peer, not a bare static file server.
47
65
 
48
66
  ---
49
67
 
50
68
  ## what's next
51
69
 
52
- ### fix watcher leaks in `mount` ← start here
53
- When a reactive slot re-renders, child watchers registered inside it are never
54
- cleaned up. Every re-render accumulates more. This is a correctness bug and the
55
- thing most worth fixing before building more UI on top of `mount`.
56
-
57
- ### component support in `h`
58
- Functions as tags: `h\`<${Card} title="hi"/>\``. This is the difference between
59
- a templating tool and a UI framework. Once watcher cleanup exists, components
60
- become straightforward — a component is just a function that returns nodes and
61
- cleans up after itself.
62
-
63
- ### SVG namespace
64
- `mount` hardcodes the XHTML namespace. `h\`<svg><path/></svg>\`` won't render
65
- correctly until `mount` auto-detects SVG elements and switches namespaces.
66
-
67
- ### `class` as array or object
68
- `class=${['btn', isActive && 'active']}` is such a common pattern that not
69
- supporting it is a daily papercut. Easy win.
70
-
71
- ### chat persistence
72
- Right now the chat server is in-memory — restart it and history is gone. Wiring
73
- `archiveSync` into `chat-server.js` is a small change with a big quality-of-life
74
- improvement.
75
-
76
- ### chat signing
77
- Messages aren't cryptographically verified yet. Anyone who knows a participant's
78
- public key hex could theoretically spoof them. Wiring `repo.sign()` after each
79
- `set()` closes this.
70
+ ### chat persistence ← start here
71
+ The chat server (`public/apps/chat/server.js`) uses `StreamoServer` and wires
72
+ `archiveSync` so the member list survives restarts automatically. Individual
73
+ message history lives in each participant's own repo; persistence there depends
74
+ on participants running with `--data-dir` set. The remaining work is ensuring the
75
+ browser chat client also persists across page reloads.
80
76
 
81
77
  ### presence indicators
82
78
  Who's currently online? The `interest` / `announce` layer is ephemeral by design,
@@ -88,24 +84,63 @@ The old repository-browser app was left behind during the migration because its
88
84
  imports broke. Rebuilding it with `h` / `mount` would be the first substantial
89
85
  real-world test of the UI layer.
90
86
 
91
- ### fix dead links on the homepage
92
- `public/index.html` links to the browser and components apps that no longer exist.
93
- Either rebuild them or remove the links.
87
+ ---
88
+
89
+ ## toward 1.0
90
+
91
+ One thing blocking a stable `1.0` claim:
94
92
 
95
- ### CLAUDE.md
96
- Add a `CLAUDE.md` for the streamo repo so future Claude sessions have the full
97
- context.
93
+ 1. **Chat persistence** — a chat app that loses history on restart isn't production-ready
94
+
95
+ Chat signing is done. Components, keyed list reconciliation, SVG namespaces,
96
+ `class` arrays/objects, and the CLI server unification are all done.
97
+ Persistence is the last mile.
98
98
 
99
99
  ---
100
100
 
101
- ## toward 1.0
101
+ ## beyond 1.0
102
+
103
+ Ideas that follow naturally from the architecture but aren't blocking anything.
104
+
105
+ ### Claude scratchpad repos
106
+
107
+ Every streamo node already has a signed, append-only repo. A Claude session
108
+ could write observations, notes, and work products to that repo during a
109
+ conversation — and the owner could watch them appear live in a browser via
110
+ `mount`. Between sessions, Claude reads the repo to reconstruct context
111
+ instead of relying on static memory files. The work is persistent and
112
+ provably Claude's, with the same integrity guarantees as any other streamo data.
113
+
114
+ ### Claude-to-Claude networks
115
+
116
+ If each person's Claude has a scratchpad repo, those repos can sync the same
117
+ way any other repos do. The `follow` callback in `registrySync` already handles
118
+ content-driven discovery — subscribe to a member list, auto-follow everyone on
119
+ it. A Claude could watch its person's friends' scratchpads, surface what's
120
+ relevant, and filter what isn't.
121
+
122
+ The interesting architectural difference from a traditional social network: there
123
+ is no central moderator. Each Claude is an advocate for its person, not a
124
+ reporter to a platform. Judgment about what to surface or filter lives at the
125
+ edge, anchored to a real signed identity. Conflicts between Claudes are just
126
+ their people having different values — which is honest in a way platform
127
+ moderation usually isn't.
128
+
129
+ A natural extension: if a Claude scratchpad includes a `StreamoComponent` for
130
+ how its notes render, other people see those notes in Claude's own layout. The
131
+ presentation travels with the content — no server controls the framing.
102
132
 
103
- The three things blocking a stable `1.0` claim:
133
+ ### StreamoComponent demos shared components as content
104
134
 
105
- 1. **Watcher leak fixed** `mount` must be correct before anyone builds on it
106
- 2. **Components** without them, `h` / `mount` is too limited for real apps
107
- 3. **Chat signing** the whole point of the project is cryptographic authorship;
108
- the flagship app should demonstrate it
135
+ `StreamoComponent` makes most sense as a post-1.0 story, after chat signing
136
+ gives the trust foundation that running someone else's component requires.
137
+ The right first demo is a **tarot deck**: each card is a `StreamoComponent`
138
+ from its designer, stored in their signed repo at a content address.
139
+ `componentKey` generates a stable element name from that address. A reading
140
+ is a snapshot — cards freeze at the version they were drawn, which is a
141
+ feature, not a bug. The designer's signed key is provenance.
109
142
 
110
- Keyed list reconciliation and refs are quality-of-life improvements that can come
111
- after 1.0.
143
+ Other directions once the pattern is established: publisher-controlled article
144
+ cards that travel with syndicated content (the layout is the author's, not
145
+ the platform's); collaborative maps where each participant's marker is their
146
+ own component; shared instrument components in a live music session.
package/bin/streamo.js CHANGED
@@ -6,16 +6,14 @@ import { Option, program } from 'commander'
6
6
  import { config } from 'dotenv'
7
7
  import { question, questionNewPassword } from 'readline-sync'
8
8
  import { start as startRepl } from 'repl'
9
- import { Signer } from '../public/streamo/Signer.js'
9
+ import { StreamoServer } from '../public/streamo/StreamoServer.js'
10
10
  import { Repo } from '../public/streamo/Repo.js'
11
11
  import { RepoRegistry } from '../public/streamo/RepoRegistry.js'
12
12
  import { archiveSync } from '../public/streamo/archiveSync.js'
13
13
  import { fileSync } from '../public/streamo/fileSync.js'
14
14
  import { outletSync } from '../public/streamo/outletSync.js'
15
15
  import { originSync } from '../public/streamo/originSync.js'
16
- import { webSync } from '../public/streamo/webSync.js'
17
16
  import { s3Sync } from '../public/streamo/s3Sync.js'
18
- import { stateFileSync } from '../public/streamo/stateFileSync.js'
19
17
 
20
18
  const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url)))
21
19
 
@@ -112,29 +110,32 @@ if (options.envFile) {
112
110
  Object.assign(options, program.opts())
113
111
  }
114
112
 
115
- options.name ||= question('Name: ')
113
+ options.name ||= question('Name: ')
116
114
  options.username ||= question('Username: ')
117
115
  const password = options.password || questionNewPassword('Password [ATTENTION!: Backspace won\'t work here]: ', { min: 4, max: 999 })
118
116
 
119
- const signer = new Signer(options.username, password, options.keyIterations)
120
- const { publicKey } = await signer.keysFor(options.name)
121
- const publicKeyHex = Array.from(publicKey).map(b => b.toString(16).padStart(2, '0')).join('')
117
+ const server = await StreamoServer.create({
118
+ name: options.name,
119
+ username: options.username,
120
+ password,
121
+ dataDir: options.dataDir,
122
+ keyIterations: options.keyIterations,
123
+ })
124
+
125
+ const { name, username, publicKeyHex, signer, streamo, registry } = server
122
126
 
123
- const name = options.name
124
- const username = options.username
125
- const appPath = options.envFile
126
- ? '/' + dirname(options.envFile).replace(/^public\//, '') + '/'
127
- : '/'
128
- const webUrl = options.web ? `http://localhost:${+options.web}${appPath}` : null
127
+ const envDir = options.envFile ? dirname(options.envFile).replace(/^public\//, '') : null
128
+ const appPath = (envDir && envDir !== '.') ? `/${envDir}/` : '/'
129
+ const webUrl = options.web ? `http://localhost:${+options.web}${appPath}` : null
129
130
  const rows = [
130
- ['NAME', name],
131
- ['USERNAME', username],
131
+ ['NAME', name],
132
+ ['USERNAME', username],
132
133
  ['PUBLIC KEY', publicKeyHex],
133
134
  ...(webUrl ? [['URL', webUrl]] : []),
134
135
  ]
135
136
  const maxLength = Math.max(...rows.map(([, v]) => v.length))
136
- const pad = (v) => v + ' '.repeat(maxLength - v.length)
137
- const div = '─'.repeat(maxLength)
137
+ const pad = (v) => v + ' '.repeat(maxLength - v.length)
138
+ const div = '─'.repeat(maxLength)
138
139
  const label = (l) => l.padStart(16)
139
140
  console.log(`\x1b[35m
140
141
  ╭${'─'.repeat(maxLength + 23)}╮
@@ -145,49 +146,40 @@ ${rows.map(([l, v], i) => [
145
146
  ].filter(Boolean).join('\n')).join('\n')}
146
147
  ╰──────────────────┴──${'━'.repeat(maxLength)}──╯\x1b[0m`)
147
148
 
148
- const dataDir = options.dataDir
149
- const registry = new RepoRegistry(async key => {
150
- const repo = new Repo()
151
- await archiveSync(repo, dataDir, key)
152
- return repo
153
- })
154
- const streamo = await registry.open(publicKeyHex)
155
-
156
149
  if (options.files) {
157
150
  const folder = typeof options.files === 'string' ? options.files : '.'
158
- await fileSync(streamo, folder, options.dataDir)
151
+ await server.files(folder)
159
152
  console.log(`\x1b[32mmirroring files: ${folder}\x1b[0m`)
160
153
  }
161
154
 
162
155
  if (options.stateFile) {
163
- stateFileSync(streamo, options.stateFile)
156
+ server.stateFile(options.stateFile)
164
157
  console.log(`\x1b[32mstate file: ${options.stateFile}\x1b[0m`)
165
158
  }
166
159
 
167
160
  if (options.s3Bucket) {
168
- await s3Sync(streamo, publicKeyHex, {
169
- bucket: options.s3Bucket,
170
- endpoint: options.s3Endpoint,
171
- region: options.s3Region,
172
- accessKeyId: options.s3AccessKeyId,
173
- secretAccessKey: options.s3SecretAccessKey
161
+ await server.s3({
162
+ bucket: options.s3Bucket,
163
+ endpoint: options.s3Endpoint,
164
+ region: options.s3Region,
165
+ accessKeyId: options.s3AccessKeyId,
166
+ secretAccessKey: options.s3SecretAccessKey,
174
167
  })
175
168
  console.log(`\x1b[32ms3: syncing to bucket ${options.s3Bucket}\x1b[0m`)
176
169
  }
177
170
 
178
171
  if (options.web) {
179
- await webSync(registry, publicKeyHex, +options.web, name, options.keyIterations)
172
+ await server.web(+options.web)
180
173
  }
181
174
 
182
175
  if (options.outlet) {
183
176
  const port = +options.outlet
184
- outletSync(registry, port)
177
+ server.outlet(port)
185
178
  console.log(`\x1b[32moutlet: listening on port ${port}\x1b[0m`)
186
179
  }
187
180
 
188
181
  if (options.origin) {
189
- const [host, port] = options.origin.split(':')
190
- await originSync(streamo, publicKeyHex, host, +port)
182
+ await server.connect(options.origin)
191
183
  console.log(`\x1b[32morigin: connected to ${options.origin}\x1b[0m`)
192
184
  }
193
185
 
@@ -197,13 +189,10 @@ if (options.verbose) {
197
189
  }
198
190
 
199
191
  if (options.interactive) {
200
- const get = (...args) => streamo.get(...args)
201
- const set = (...args) => streamo.set(...args)
202
- const ls = () => [...registry].map(([k, s]) => ({ key: k.slice(0, 8) + '…', bytes: s.byteLength }))
203
- const connect = (hostPort) => {
204
- const [host, port] = hostPort.split(':')
205
- return originSync(streamo, publicKeyHex, host, +port)
206
- }
192
+ const get = (...args) => streamo.get(...args)
193
+ const set = (...args) => streamo.set(...args)
194
+ const ls = () => [...registry].map(([k, s]) => ({ key: k.slice(0, 8) + '…', bytes: s.byteLength }))
195
+ const connect = (hostPort) => server.connect(hostPort)
207
196
 
208
197
  Object.assign(globalThis, {
209
198
  // identity
package/jsconfig.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ES2022",
4
- "module": "ES2022",
5
- "moduleResolution": "node",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "node16",
6
+ "moduleResolution": "node16",
6
7
  "checkJs": false
7
8
  },
8
9
  "exclude": ["node_modules"]
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@dtudury/streamo",
3
- "version": "0.1.1",
4
- "description": "content-addressed p2p sync",
3
+ "version": "0.2.0",
4
+ "description": "peer-to-peer sync where your data and identity belong to you, not the server",
5
+ "keywords": ["p2p", "peer-to-peer", "sync", "reactive", "content-addressed", "websocket", "signed", "append-only", "offline-first", "cryptographic", "identity"],
5
6
  "repository": "git@github.com:dtudury/streamo.git",
6
7
  "author": "David Tudury <david.tudury@gmail.com>",
7
8
  "license": "AGPL-3.0-only",
@@ -21,6 +22,6 @@
21
22
  "ws": "^8.18.3"
22
23
  },
23
24
  "scripts": {
24
- "serve": "node scripts/serve.js"
25
+ "serve": "node bin/streamo.js --env-file .env.dev --web 3000 --interactive"
25
26
  }
26
27
  }