diff options
| author | Adam Soltys <[email protected]> | 2026-09-14 19:03:06 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-14 19:03:06 -0700 |
| commit | 718d7f2bf077ea27ced17c94f643592003e6bacc (patch) | |
| tree | a4bae2678468de7d4f6a23ae16a869bea3fe7811 /src/concord.js | |
| parent | 3bf5557d8ea3d45779f6643d40c18629362d78e3 (diff) | |
| download | coinosv3-718d7f2bf077ea27ced17c94f643592003e6bacc.tar.gz coinosv3-718d7f2bf077ea27ced17c94f643592003e6bacc.zip | |
A community that has been refounded reads and writes at its current epoch.
Vector communities (Nostr Talk, FOSS Apps) sit at root epoch 1 after a
refounding. coinos derived every channel stream at epoch 0, so it read
and posted to an address nobody else used: five messages from coinos
users where Vector showed months of history. Public channels now follow
the base epoch (CORD-03), private ones their own key and epoch from the
join material; the epoch tag on every rumor matches the stream that
carried it.
The Control Plane is read at the invite's control_pk (CORD-02 §5 split)
and decrypted under the root-derived key, so metadata, icon and channel
definitions fold for a post-split community; the legacy derivation is
kept for the built-in coinos community, which still writes through it.
Guestbook snapshots (3312) from the owner seed members after a
refounding, so the count matches Vector's. Join material carries
control_pk and private channel keys through the Community List and
direct invites, and the list merge heals an entry that predates them.
A pasted invite previews under the Communities heading where the paste
box was, instead of at the top of a page the user had scrolled past.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'src/concord.js')
| -rw-r--r-- | src/concord.js | 22 |
1 files changed, 20 insertions, 2 deletions
diff --git a/src/concord.js b/src/concord.js index 74c1313..63a83b9 100644 --- a/src/concord.js +++ b/src/concord.js @@ -77,6 +77,15 @@ export const guestbookKey = (root, cid, epoch = 0) => groupKey("concord/guestbook", root, hexToBytes(cid), epoch); export const channelKey = (secret, channelId, epoch = 0) => groupKey("concord/channel", secret, hexToBytes(channelId), epoch); + +// CORD-03: a Public Channel derives from the community_root at the BASE +// epoch (it rotates with every Refounding); a Private one carries its own +// key and epoch in the join material. A channel entry with no key, or whose +// key is the root itself, is public. +export const channelIsPrivate = (jm, ch) => !!(ch && ch.key && ch.key !== jm.community_root); +export const channelEpoch = (jm, ch) => (channelIsPrivate(jm, ch) ? ch.epoch || 0 : jm.root_epoch || 0); +export const channelStream = (jm, ch) => + channelKey(hexToBytes(channelIsPrivate(jm, ch) ? ch.key : jm.community_root), ch.id, channelEpoch(jm, ch)); export const grantEid = (cid, memberHex) => bytesToHex(hkdf32(hexToBytes(cid), "concord/grant", hexToBytes(memberHex))); export const banlistEid = (cid) => bytesToHex(hkdf32(hexToBytes(cid), "concord/banlist", ZERO32)); @@ -218,7 +227,7 @@ export const foldControl = (entries, { ownerHex, cid }) => { // ---- CORD-02 §5 guestbook fold ---- // entries: [{ rumor, author }], returns Map(npub -> { state: 'join'|'leave'|'kick', t }) -export const foldGuestbook = (entries, { nowMs, banned = new Set() } = {}) => { +export const foldGuestbook = (entries, { nowMs, banned = new Set(), ownerHex } = {}) => { let members = new Map(); let consider = (npub, state, t, id) => { let cur = members.get(npub); @@ -229,7 +238,16 @@ export const foldGuestbook = (entries, { nowMs, banned = new Set() } = {}) => { if (t === null || (nowMs && t > nowMs + 3600_000)) continue; if (rumor.kind === 3306 && (rumor.content === "join" || rumor.content === "leave")) consider(author, rumor.content, t, rumor.id); - // kicks (3309) and snapshots (3312): honored later with roster ranks; skipped for now + // A snapshot (3312) seeds the new epoch's Guestbook after a Refounding: + // the refounder's secondhand list of present members, dated at the + // snapshot, so any self-signed entry newer than it still wins. Honored + // from the owner only (the sole authority coinos folds today). + else if (rumor.kind === 3312 && ownerHex && author === ownerHex) { + let list; try { list = JSON.parse(rumor.content); } catch { continue; } + if (!Array.isArray(list)) continue; + for (let npub of list.slice(0, 400)) if (/^[0-9a-f]{64}$/.test(npub)) consider(npub, "join", t, rumor.id); + } + // kicks (3309): honored later with roster ranks; skipped for now } for (let b of banned) members.delete(b); return members; |
