aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-02 10:09:42 -0700
committerAdam Soltys <[email protected]>2026-08-02 10:09:42 -0700
commit3205acf157b7f1cfb857a383f374ef4d739d82cc (patch)
treec61dd9e51b230106bc3134a22eef774dc1decd8c /tools
parent6ebae6bce794c8f2db8337cb189b9095975e36ad (diff)
downloadcoinosv3-3205acf157b7f1cfb857a383f374ef4d739d82cc.tar.gz
coinosv3-3205acf157b7f1cfb857a383f374ef4d739d82cc.zip
Chat: Concord encrypted community messaging, with a default coinos room
Implements the Concord protocol (CORD-01..04) — private streams, the control-plane fold, guestbook membership, owner-rooted authority — and ships a built-in "coinos" community founded by Adam's npub (owner/admin). Messages sign with the nostr login identity when a signer is live, else the wallet's NIP-06 key; all encryption rides derived stream keys so remote signers only ever sign seals. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
Diffstat (limited to 'tools')
-rw-r--r--tools/concord-genesis.js85
1 files changed, 85 insertions, 0 deletions
diff --git a/tools/concord-genesis.js b/tools/concord-genesis.js
new file mode 100644
index 0000000..797902d
--- /dev/null
+++ b/tools/concord-genesis.js
@@ -0,0 +1,85 @@
+// One-time genesis for a Concord community (CORD-02 §1): publishes the two
+// owner-signed editions (community metadata + #general) and the owner's Join,
+// then prints the join material to embed in the app.
+//
+// bun run tools/concord-genesis.js <owner-privkey-hex> <name> <relay> [relay...]
+
+import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
+import { getPublicKey } from "nostr-tools/pure";
+import { Relay, useWebSocketImplementation } from "nostr-tools/relay";
+import WebSocket from "ws";
+import {
+ communityId,
+ controlKey,
+ guestbookKey,
+ makeEdition,
+ wrapRumor,
+ msTags,
+} from "../src/concord.js";
+
+try { useWebSocketImplementation(WebSocket); } catch {}
+
+let [skHex, name, ...relays] = process.argv.slice(2);
+if (!skHex || !name || !relays.length) {
+ console.error("usage: bun run tools/concord-genesis.js <owner-privkey-hex> <name> <relay...>");
+ process.exit(1);
+}
+
+let ownerSk = hexToBytes(skHex);
+let owner = getPublicKey(ownerSk);
+let ownerSalt = bytesToHex(crypto.getRandomValues(new Uint8Array(32)));
+let communityRoot = crypto.getRandomValues(new Uint8Array(32));
+let cid = communityId(owner, ownerSalt);
+let generalId = bytesToHex(crypto.getRandomValues(new Uint8Array(32)));
+
+let control = controlKey(communityRoot, cid, 0);
+let guestbook = guestbookKey(communityRoot, cid, 0);
+
+let now = Date.now();
+let metadata = makeEdition(
+ { vsk: 0, eid: cid, version: 1, content: JSON.stringify({ name, relays }) },
+ now
+);
+let general = makeEdition(
+ { vsk: 2, eid: generalId, version: 1, content: JSON.stringify({ name: "general", private: false }) },
+ now + 1
+);
+let { created_at, ms } = msTags(now + 2);
+let join = { kind: 3306, content: "join", tags: [ms], created_at };
+
+let events = [
+ await wrapRumor({ ...metadata, pubkey: owner }, ownerSk, control, { plaintext: true }),
+ await wrapRumor({ ...general, pubkey: owner }, ownerSk, control, { plaintext: true }),
+ await wrapRumor({ ...join, pubkey: owner }, ownerSk, guestbook),
+];
+
+for (let url of relays) {
+ try {
+ let r = await Relay.connect(url);
+ for (let e of events) await r.publish(e);
+ r.close();
+ console.error(`published ${events.length} events to ${url}`);
+ } catch (e) {
+ console.error(`FAILED ${url}: ${e.message ?? e}`);
+ }
+}
+
+// Join material (CORD-02 §8) — the bundle's membership subset. Contains the
+// community_root: embedding it in the app makes every app user a member.
+console.log(
+ JSON.stringify(
+ {
+ community_id: cid,
+ owner,
+ owner_salt: ownerSalt,
+ community_root: bytesToHex(communityRoot),
+ root_epoch: 0,
+ channels: [{ id: generalId, name: "general" }],
+ relays,
+ name,
+ },
+ null,
+ 2
+ )
+);
+process.exit(0);