aboutsummaryrefslogtreecommitdiffstats
path: root/tools/concord-genesis.js
blob: 797902d877ac2cb8e84bd299d562f8b3ae893ad9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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);