| … | … | @@ -1,16 +1,12 @@ |
| 1 | 1 | # favela-peer |
| 2 | 2 | |
| 3 | | **A social feed you actually hold.** Posts are markdown files in an append-only log that lives on |
| 4 | | your machine. There is no server, no account, and nobody to ask. |
| 3 | The favela daemon. Feeds, rooms and namespaces of markdown records over Hypercore. |
| 4 | Holds the data, replicates it, and answers RPC on a unix socket. |
| 5 | 5 | |
| 6 | | This is the daemon. It keeps your data, talks to other peers, and answers questions over a unix |
| 7 | | socket. You read it through `favela-surface` in a browser, or the `favela` CLI below. |
| 6 | No atproto, no ActivityPub — those are downstream projections that read this. |
| 8 | 7 | |
| 9 | 8 | ## Install |
| 10 | 9 | |
| 11 | | Deploy is publish then install — there is no hand build, and the release carries prebuilds for |
| 12 | | every platform: |
| 13 | | |
| 14 | 10 | ```sh |
| 15 | 11 | pgh publish . |
| 16 | 12 | pgh app install pgh://<registry-key>/favela-peer --name favela-peer |
| … | … | @@ -23,87 +19,124 @@ favela-peer # the pgh-installed launcher |
| 23 | 19 | bare index.js # or straight from a checkout |
| 24 | 20 | ``` |
| 25 | 21 | |
| 26 | | Storage is `~/.favela`, socket `~/.favela/favela.sock` — `FAVELA_STORAGE` and `FAVELA_SOCK` move |
| 27 | | them. Set `FAVELA_DEBUG=1` for JSON logs. Only one daemon may hold a socket; a second refuses to |
| 28 | | start rather than leaving the first unreachable. |
| 29 | | |
| 30 | | ## Using it |
| 31 | | |
| 32 | | `favela` is the CLI. Run it bare for the full list; these are the ones you need first: |
| 33 | | |
| 34 | | ``` |
| 35 | | favela whoami # this node's feed key |
| 36 | | favela link # attest this device from your identity vault |
| 37 | | favela claim alice # take a handle — you get e.g. alice0 |
| 38 | | favela post "hello" # write a post |
| 39 | | favela follow <feedKey> # record a follow AND start holding the feed |
| 40 | | favela timeline # posts from every feed you hold |
| 41 | | favela invite # a one-time token to sign a browser in |
| 42 | | ``` |
| 43 | | |
| 44 | | It is a testing surface rather than the product surface — `favela-surface` is what a person uses — |
| 45 | | but everything the daemon can do is reachable from it, and it is the fastest way to see whether |
| 46 | | something works. |
| 47 | | |
| 48 | | ## The ideas you need |
| 49 | | |
| 50 | | **Following is a record, not a subscription.** To follow someone you write a record in *your own* |
| 51 | | feed naming theirs. Nothing is written to theirs. They need not be online, need not consent, and |
| 52 | | need never know. Likes and replies work the same way. This is why a feed's writer set never grows |
| 53 | | past its owner's devices no matter how popular it is — and why counts are per-observer rather than |
| 54 | | summed. Your "12 likes" and mine are both honest and may differ. |
| 55 | | |
| 56 | | **The timeline is local, and it is a projection.** There is no relay and no aggregator. Your |
| 57 | | timeline is computed from the feeds this node happens to be holding, so two people with identical |
| 58 | | follow lists can legitimately see different timelines. A feed you follow but have not replicated |
| 59 | | contributes nothing — which is correct, and is why `favela missing` exists. A gap you can name is a |
| 60 | | different thing from a gap that is silently empty. |
| 61 | | |
| 62 | | **A post is a markdown file** — JSON frontmatter, then a body. It is mutable and edited in place, |
| 63 | | so the content hash changes on every edit and the name is the stable identity, never the hash. |
| 64 | | The bytes live content-addressed in hyper-overlay; the log holds a pointer. See `favela-markdown`. |
| 65 | | |
| 66 | | ## The three primitives |
| 67 | | |
| 68 | | **Feed** — one identity's log. Its writers are that identity's devices, and its roster is flat — |
| 69 | | the only question is whether a writer is on it. You have exactly one. |
| 70 | | |
| 71 | | **Room** — many members, each with their own devices, and membership is the audience. Two levels |
| 72 | | rather than one, which matters more than it sounds: a role belongs to a person, not to a laptop. |
| 73 | | The vocabulary is keet's — admin, moderator, peer, read-only — because there was no reason to |
| 74 | | invent another. A DM is a room with one other member in it. |
| 75 | | |
| 76 | | The engine underneath is autobee, not autobase. Autobase needs `floor(N/2)+1` indexers, so at |
| 77 | | N=2 one member going offline freezes the view while it keeps growing, and it cannot heal by |
| 78 | | dropping to one. A person's two devices make N=2 the ordinary case, not an edge case. Weights |
| 79 | | retire that whole class of problem. |
| 80 | | |
| 81 | | **Namespace** — human names that resolve to feeds. It is a subscription, not an authority: two |
| 82 | | people subscribed to different namespaces resolve the same string to different feeds, and that is |
| 83 | | correct rather than a conflict. We give up global uniqueness and get, in exchange, a name that is |
| 84 | | always qualified by a namespace you can point at. Resolution runs handle → self-label → key, and |
| 85 | | the key is always shown. |
| 86 | | |
| 87 | | ## The boundary |
| 88 | | |
| 89 | | ⛔ **No `atproto-*` or `ap-*` imports. No DAG-CBOR, no MST, no DID, no signed commit.** |
| 90 | | |
| 91 | | This is a clean break from atproto and ActivityPub. Bridges come *after* the MVP and they are |
| 92 | | downstream — they read this, this never reads them. The moment the daemon imports the projection, |
| 93 | | the dependency has inverted and the recentring has failed. |
| 94 | | |
| 95 | | That is an acceptance criterion rather than an aspiration (`hyper-social` #61 §10), so |
| 96 | | `test/boundary.test.js` enforces it over every file in `lib/` and over the dependency list. |
| 22 | Refuses to start if another daemon already holds the socket. |
| 23 | |
| 24 | ## Config |
| 25 | |
| 26 | | var | default | | |
| 27 | |---|---|---| |
| 28 | | `FAVELA_STORAGE` | `~/.favela` | corestore + overlay + working tree | |
| 29 | | `FAVELA_SOCK` | `<storage>/favela.sock` | RPC socket | |
| 30 | | `FAVELA_DEBUG` | unset | set to any value for JSON logs on stdout | |
| 31 | |
| 32 | ## CLI |
| 33 | |
| 34 | `favela` speaks to the daemon over `FAVELA_SOCK`. It holds no state. |
| 35 | |
| 36 | ### identity and devices |
| 37 | |
| 38 | | | | |
| 39 | |---|---| |
| 40 | | `whoami` | this node's feed key | |
| 41 | | `link [--vault <path>]` | attest THIS device from your identity vault | |
| 42 | | `devices` | admitted devices, and who owns this feed | |
| 43 | | `unlink <deviceKey>` | revoke a device, and everything it attested | |
| 44 | | `pair` | offer this device's credential to another machine | |
| 45 | | `pair --accept <invite>` | take an invite and link this machine | |
| 46 | | `pair --cancel` | withdraw an outstanding invite | |
| 47 | |
| 48 | ### handles and namespaces |
| 49 | |
| 50 | | | | |
| 51 | |---|---| |
| 52 | | `namespace` | which namespace this node resolves handles in | |
| 53 | | `namespace --join <key>` | resolve handles in a different one, from now on | |
| 54 | | `namespace --invite` | mint a single-use invite (admins) | |
| 55 | | `namespace --request [--token T]` | ask to carry this namespace's log | |
| 56 | | `namespace --withdraw <hash>` | withdraw an unspent invite | |
| 57 | | `namespace --admit <writerKey>` | answer an ask (admins) | |
| 58 | | `namespace --grant <rootKey>` | make an identity an admin here | |
| 59 | | `namespace --revoke <deviceKey>` | retire a device, and what it carries | |
| 60 | | `claim <name>` | claim a handle — you get e.g. `alice0` | |
| 61 | | `rename <oldHandle> <newName>` | give one up and take another | |
| 62 | | `release <handle>` | | |
| 63 | | `handles [query]` | every handle in the namespace, or those matching | |
| 64 | | `resolve <handle>` | handle → feed, and whether the feed agrees | |
| 65 | |
| 66 | ### profile and browsers |
| 67 | |
| 68 | | | | |
| 69 | |---|---| |
| 70 | | `profile` | display name and picture | |
| 71 | | `set-profile [--name X] [--bio "…"] [--avatar <file>]` | `--bio ""` removes | |
| 72 | | `invite [--ttl <ms>]` | one-time invite for a browser | |
| 73 | | `browsers` | browsers you have admitted | |
| 74 | | `revoke <publicKey>` | retire a browser | |
| 75 | |
| 76 | ### posts and social |
| 77 | |
| 78 | | | | |
| 79 | |---|---| |
| 80 | | `post <text>` | write a post | |
| 81 | | `timeline [--limit N]` | posts from every feed you hold | |
| 82 | | `follow <feedKey>` | record a follow AND hold the feed | |
| 83 | | `unfollow <feedKey>` | | |
| 84 | | `following` | who you follow | |
| 85 | | `missing` | followed feeds you cannot read | |
| 86 | | `unresolved` | records you hold pointers for and cannot read | |
| 87 | | `like`/`unlike`/`likes` `<feed> <collection> <name>` | | |
| 88 | | `reply <feed> <collection> <name> <text>` | | |
| 89 | | `replies <feed> <collection> <name>` | | |
| 90 | | `search <query> [--limit N]` | over what you already hold | |
| 91 | | `knock <feedKey>` | ring someone's doorbell | |
| 92 | | `knocks` | who rang yours | |
| 93 | | `admit <feedKey>` | accept a knock — this follows them | |
| 94 | | `dismiss <feedKey>` | | |
| 95 | | `directory` | listings across registries you hold | |
| 96 | |
| 97 | `lib/rpc.js` answers 92 methods; the CLI covers the ones a human drives. |
| 98 | |
| 99 | ## Primitives |
| 100 | |
| 101 | | | roster | engine | |
| 102 | |---|---|---| |
| 103 | | **feed** | device-flat — one identity, writers are its devices | autobase | |
| 104 | | **room** | members, each with devices; membership is the audience | autobee | |
| 105 | | **namespace** | subscription; not an authority | autobee | |
| 106 | |
| 107 | - One feed per identity. |
| 108 | - Room roles are keet's: admin, moderator, peer, read-only. A role belongs to a person, not a device. |
| 109 | - A DM is a room with one other member. |
| 110 | - autobee, not autobase, for rooms: autobase needs `floor(N/2)+1` indexers and N=2 is the ordinary |
| 111 | case for one person's two devices. |
| 112 | - Handle resolution order: handle → self-label → key. The key is always shown. |
| 113 | - Two namespaces resolve the same string to different feeds. That is by design. |
| 114 | |
| 115 | ## Records |
| 116 | |
| 117 | - A record is a markdown file: JSON frontmatter, then a body. See `favela-markdown`. |
| 118 | - Mutable, edited in place — the name is the stable identity, the hash is not. |
| 119 | - Bytes are content-addressed in hyper-overlay; the log holds a pointer. |
| 120 | |
| 121 | ## Interactions |
| 122 | |
| 123 | - A like, reply or follow is a record in **your own** feed naming a target. |
| 124 | - Nothing is ever written to someone else's feed, so a writer set never grows past its owner's devices. |
| 125 | - Counts are per-observer, never summed. |
| 126 | - The timeline is local: computed from feeds this node holds. No relay, no aggregator. |
| 127 | - Identical follow lists can yield different timelines. `favela missing` names the gap. |
| 128 | |
| 129 | ## Boundary |
| 130 | |
| 131 | ⛔ No `atproto-*` or `ap-*` imports. No DAG-CBOR, no MST, no DID, no signed commit. |
| 132 | |
| 133 | `test/boundary.test.js` enforces this over every file in `lib/` and over the dependency list. |
| 97 | 134 | |
| 98 | 135 | ## Status |
| 99 | 136 | |
| 100 | | Working, and in daily use on tabertron alongside `favela-surface` and `favela-web`. |
| 101 | | |
| 102 | | `npm test` runs 342 tests over feeds, rooms, namespaces, handles, device pairing and attestation, |
| 103 | | browser sessions, files and avatars, the mirror, the follow graph, search, notifications and |
| 104 | | replication. All passing. |
| 137 | Working. 0.0.48, running on tabertron, ashatron and stortron. |
| 105 | 138 | |
| 106 | | Build order and the two-peer acceptance tests are in `hyper-social` #61 §10 and §11.2. |
| 139 | `npm test` — 342 tests, 813 asserts, passing. |
| 107 | 140 | |
| 108 | 141 | ## License |
| 109 | 142 | |