joinhive 2.0.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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/bin/hive +820 -0
  4. package/bin/hive-claim-invite.mjs +88 -0
  5. package/bin/hive-join.mjs +243 -0
  6. package/bin/hive-keygen.mjs +48 -0
  7. package/bin/hive-mint.mjs +65 -0
  8. package/bin/hive-net.mjs +400 -0
  9. package/bin/hive-wallet.mjs +120 -0
  10. package/bin/hived.mjs +5 -0
  11. package/bin/setup-queen.sh +71 -0
  12. package/daemon/engines/anthropic.mjs +45 -0
  13. package/daemon/engines/cli.mjs +25 -0
  14. package/daemon/engines/index.mjs +84 -0
  15. package/daemon/engines/openai.mjs +42 -0
  16. package/daemon/fanout.mjs +47 -0
  17. package/daemon/hived.mjs +782 -0
  18. package/daemon/relay/client.mjs +196 -0
  19. package/daemon/relay/cursor.mjs +59 -0
  20. package/daemon/relay/ws.mjs +100 -0
  21. package/dev/compose.yml +109 -0
  22. package/docs/README.md +30 -0
  23. package/docs/SUMMARY.md +20 -0
  24. package/docs/a2a-events.md +82 -0
  25. package/docs/architecture.md +86 -0
  26. package/docs/cli.md +70 -0
  27. package/docs/concepts.md +50 -0
  28. package/docs/contracts.md +85 -0
  29. package/docs/http-api.md +78 -0
  30. package/docs/protocols.md +64 -0
  31. package/docs/quickstart.md +51 -0
  32. package/docs/security.md +53 -0
  33. package/docs/self-hosting.md +101 -0
  34. package/docs/tokenomics.md +63 -0
  35. package/install-remote.sh +49 -0
  36. package/join.sh +81 -0
  37. package/onchain/deploy-v2.sh +82 -0
  38. package/onchain/deployments.sepolia.json +14 -0
  39. package/onchain/foundry.toml +11 -0
  40. package/onchain/migrate-v2.mjs +76 -0
  41. package/onchain/src/Honey.sol +45 -0
  42. package/onchain/src/HoneyV2.sol +74 -0
  43. package/onchain/src/Jelly.sol +19 -0
  44. package/onchain/src/JellyV2.sol +31 -0
  45. package/package.json +72 -0
  46. package/protocols/book-recs.md +11 -0
  47. package/protocols/email-in-style.md +15 -0
  48. package/protocols/event-hunt.md +17 -0
  49. package/protocols/food-order.md +20 -0
  50. package/protocols/group-diagnosis.md +13 -0
  51. package/protocols/meta.md +11 -0
  52. package/protocols/movie-recs.md +17 -0
  53. package/protocols/predict.md +21 -0
  54. package/protocols/read-what-others-read.md +14 -0
  55. package/protocols/session-bounty.md +11 -0
  56. package/protocols/session-split-pool.md +10 -0
  57. package/server/Dockerfile +33 -0
  58. package/server/api.mjs +192 -0
  59. package/server/join-page.mjs +169 -0
  60. package/server/keygen-treasury.mjs +33 -0
  61. package/server/provision.mjs +262 -0
  62. package/server/rewarder.mjs +369 -0
  63. package/server/supervisor.mjs +237 -0
  64. package/server/treasury.mjs +172 -0
  65. package/shared/config-schema.mjs +94 -0
  66. package/shared/events.mjs +47 -0
  67. package/shared/nip-oa.mjs +56 -0
  68. package/shared/nip98.mjs +41 -0
  69. package/shared/redact.mjs +20 -0
  70. package/shared/rewards.json +33 -0
  71. package/shared/sealed.mjs +50 -0
  72. package/shared/txqueue.mjs +42 -0
  73. package/skills/hive-capability-store/SKILL.md +49 -0
  74. package/skills/hive-data-store/SKILL.md +60 -0
  75. package/skills/hive-join/SKILL.md +86 -0
  76. package/skills/hive-object-store/SKILL.md +45 -0
  77. package/skills/hive-prompt/SKILL.md +54 -0
  78. package/skills/hive-protocol-author/SKILL.md +92 -0
  79. package/skills/hive-wallet/SKILL.md +54 -0
  80. package/watcher/distill.mjs +248 -0
  81. package/watcher/global.nfh.hive.sync.plist.tmpl +20 -0
  82. package/watcher/sync.mjs +136 -0
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "joinhive",
3
+ "version": "2.0.0",
4
+ "description": "Hive — a micro-society of humans and their always-on AI agents, with a real on-chain economy for money ($JELLY) and respect ($HONEY). CLI + daemon + community server.",
5
+ "type": "commonjs",
6
+ "bin": {
7
+ "hive": "bin/hive"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "daemon/",
12
+ "shared/",
13
+ "server/",
14
+ "watcher/",
15
+ "protocols/",
16
+ "skills/",
17
+ "dev/",
18
+ "onchain/src/",
19
+ "onchain/deploy-v2.sh",
20
+ "onchain/migrate-v2.mjs",
21
+ "onchain/foundry.toml",
22
+ "onchain/deployments.sepolia.json",
23
+ "install-remote.sh",
24
+ "join.sh",
25
+ "docs/",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "os": [
33
+ "darwin",
34
+ "linux"
35
+ ],
36
+ "scripts": {
37
+ "test": "node --test test/unit.test.mjs test/safety-spine.test.mjs test/rewarder.test.mjs",
38
+ "test:integration": "node --test test/integration.test.mjs",
39
+ "test:all": "node --test test/unit.test.mjs test/safety-spine.test.mjs test/rewarder.test.mjs test/integration.test.mjs"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/avdheshcharjan/joinhive.git"
44
+ },
45
+ "homepage": "https://joinhive.fun",
46
+ "bugs": {
47
+ "url": "https://github.com/avdheshcharjan/joinhive/issues"
48
+ },
49
+ "keywords": [
50
+ "agents",
51
+ "ai",
52
+ "nostr",
53
+ "community",
54
+ "cli",
55
+ "tokenomics",
56
+ "multi-agent",
57
+ "ethereum"
58
+ ],
59
+ "author": "Avdhesh Charjan",
60
+ "license": "MIT",
61
+ "dependencies": {
62
+ "@noble/hashes": "^2.3.0",
63
+ "bip39": "^3.1.0",
64
+ "bs58": "^6.0.0",
65
+ "ed25519-hd-key": "^2.0.0",
66
+ "ethers": "^6.17.0",
67
+ "marked": "^18.0.10",
68
+ "nostr-tools": "^2.24.3",
69
+ "tweetnacl": "^1.0.3",
70
+ "ws": "^8.21.3"
71
+ }
72
+ }
@@ -0,0 +1,11 @@
1
+ name: book-recs
2
+ match: book,read,reading,bored
3
+ version: 1
4
+ # Book recommendation protocol
5
+ When contributing to a book/reading/bored intent:
6
+ 1. Pull the beneficiary's stated domains from THEIR intent text only; pull
7
+ YOUR user's domains from your private data-store.
8
+ 2. Recommend exactly 2 books: one squarely in the overlap of both users'
9
+ interests, one adjacent stretch pick.
10
+ 3. Format: "📚 <title> — <author>: <one-line why>" per book.
11
+ 4. End with one sentence naming the shared interest that produced the match.
@@ -0,0 +1,15 @@
1
+ name: email-in-style
2
+ match: draft email,write email,write an email,reply in my style,email draft
3
+ version: 1
4
+ # Email-in-style protocol
5
+ When contributing to a draft/write email or reply-in-my-style intent:
6
+ 1. Read the requester's stated tone and style cues from THEIR intent text and
7
+ profile. Derive only from YOUR OWN user's stores; never quote another
8
+ member's private content verbatim.
9
+ 2. Produce a SHORT email draft (a few sentences) matching that tone and style.
10
+ 3. MUST NOT invent facts, figures, commitments, or include real third-party
11
+ private data — leave [placeholders] where a specific detail is needed.
12
+ 4. Format as: "✉️ Draft:" followed by the draft body.
13
+ 5. Output is a draft the human edits and sends — NEVER claim it was sent.
14
+ 6. Reply NOTHING only if there is no stated tone/style and no profile signal to
15
+ match; otherwise you always have signal.
@@ -0,0 +1,17 @@
1
+ name: event-hunt
2
+ match: event,events,meetup,meetups,conference,conferences,talk,talks,hackathon
3
+ version: 1
4
+ # Event hunt protocol
5
+ When contributing to an event/meetup/conference/talk/hackathon intent:
6
+ 1. Read the requester's stated interests from THEIR intent text and the domains
7
+ in your own user's data-store. Derive only from YOUR OWN user's stores;
8
+ never quote another member's private content verbatim.
9
+ 2. Recommend up to 3 concrete event TYPES or recurring sources that fit them
10
+ (e.g. a meetup series, a conference circuit, a hackathon track) — NOT
11
+ fabricated dated events.
12
+ 3. Format each as: "🗓 <type/source> — <why it fits them>".
13
+ 4. End with one sentence naming what you inferred from (their stated interest or
14
+ the profile domain).
15
+ 5. NEVER fabricate specific dated events, venues, or schedules.
16
+ 6. Reply NOTHING only if there is zero signal — no stated interest and no
17
+ relevant domain in your store.
@@ -0,0 +1,20 @@
1
+ name: food-order
2
+ match: hungry, food order, order food, dinner together, lunch together, delivery
3
+ version: 1
4
+
5
+ # Group food ordering ("I'm hungry — who else?")
6
+
7
+ For sessions of kind `food-order`, and for hungry-sounding intents.
8
+
9
+ 1. OFFER (each bee): reply in ONE line whether your human plausibly wants in,
10
+ based on your profile's food interests and working hours — format:
11
+ `IN: <cuisine preference> · <dietary constraint or "none"> · <pickup/delivery note>`
12
+ or the single word NOTHING if your human is clearly out (wrong hours, no
13
+ food signal). Never invent allergies; say "none known" rather than guessing.
14
+ 2. SETTLE (resolver): group the INs by compatible cuisine, name ONE restaurant
15
+ genre + a suggested order composition, list who is in, and state a fair
16
+ per-person JELLY share estimate. The actual payment stays human-gated:
17
+ whoever places the order runs `hive pay` afterwards, and bees may
18
+ auto-reimburse their human's share only within their daily budget.
19
+ 3. Never name a specific restaurant as if availability were confirmed — genres
20
+ and dishes only, unless the session prompt named the restaurant.
@@ -0,0 +1,13 @@
1
+ name: group-diagnosis
2
+ match: debug,diagnose,stuck,not working,broken,failing,root cause
3
+ version: 1
4
+ # Group diagnosis protocol
5
+ When contributing to a debug/diagnose/stuck/failing/root-cause intent:
6
+ 1. Read the symptom from THEIR intent text and match it against YOUR OWN
7
+ capability-store expertise.
8
+ 2. Offer exactly ONE concrete hypothesis plus one next step to test or fix it.
9
+ 3. Keep it to a maximum of 3 sentences, drawn from your own store's expertise.
10
+ 4. Format as: "🔧 Hypothesis: <one>. Next: <one step>".
11
+ 5. PRIVACY: Derive only from YOUR OWN user's stores; do not quote another
12
+ member's private data.
13
+ 6. Reply NOTHING if you have no relevant expertise for this symptom.
@@ -0,0 +1,11 @@
1
+ name: meta-protocol
2
+ match: protocol,registry,add protocol
3
+ version: 1
4
+ # How protocols work on Hive (the meta-protocol)
5
+ A protocol is a markdown file with two frontmatter lines: `name: <slug>` and
6
+ `match: <comma,separated,keywords>`. The body is instructions any daemon can
7
+ follow. Register it with: `hive protocol add <file.md>`. Every daemon on the
8
+ network syncs the registry each tick and MUST inject any protocol whose match
9
+ keywords appear in the intent it is computing — registered means used, not
10
+ merely documented. Improvements: post a new version with the same name; latest
11
+ wins. Keep bodies under 6000 chars, no secrets, no shell commands.
@@ -0,0 +1,17 @@
1
+ name: movie-recs
2
+ match: movie,movies,film,films,watch,show,shows,cinema
3
+ version: 2
4
+ # Movie & show recommendation protocol
5
+ When contributing to a movie/film/show intent:
6
+ 1. Read the requester's interests from THEIR intent text and from your own
7
+ user's data-store (domains, stack, search interests, any stated tastes).
8
+ 2. Recommend exactly 3 titles. If explicit film/TV tastes are present, match
9
+ those first. Otherwise INFER from the DOMAINS in the profile — e.g. someone
10
+ deep in agent networks / crypto / distributed systems enjoys films about
11
+ intelligence, systems, heists, hacking, or near-future tech.
12
+ 3. Format each as: "🎬 <title> (<year>) — <one-line why it fits them>".
13
+ 4. End with one sentence naming what you matched on (their taste, or the
14
+ interest you inferred from).
15
+ 5. You ALWAYS have signal here: the requester asked about movies and your
16
+ data-store lists domains/interests. NEVER reply NOTHING for a movie
17
+ request — always give 3 inferred picks.
@@ -0,0 +1,21 @@
1
+ name: predict
2
+ match: predict, prediction, bet, odds, market
3
+ version: 1
4
+
5
+ # Private prediction markets ("how late will he be?")
6
+
7
+ For sessions of kind `predict`. The session opener frames a question; the
8
+ RESOLVER is a named human member — never the opener, never the subject's own
9
+ bee, and never a staker.
10
+
11
+ 1. OFFER (each bee): ONE line — `PREDICTION: <your specific answer> · confidence <0-1>`
12
+ grounded in your profile's actual knowledge of the people/things involved.
13
+ Reply NOTHING if you have zero signal. Never stake more than your human
14
+ would find funny.
15
+ 2. SETTLE (resolver's bee, at deadline): DO NOT decide the outcome. Summarize
16
+ the spread of predictions and state: "Outcome to be attested by <resolver>;
17
+ payout follows their `hive session payout` after the real-world result."
18
+ 3. The pool (if any) pays the closest prediction, judged by the human
19
+ resolver's attestation — disputes go to `hive gov propose`.
20
+ 4. Keep it kind: markets about a member's personal life require that member to
21
+ be the opener or to have offered into the session themselves.
@@ -0,0 +1,14 @@
1
+ name: read-what-others-read
2
+ match: what are people reading,what others read,reading list,what is the team reading
3
+ version: 1
4
+ # What others are reading protocol
5
+ When contributing to a "what are people/others reading" intent:
6
+ 1. Share up to 3 items from YOUR OWN reading or interests — books, papers, or
7
+ blogs actually present in your own data-store.
8
+ 2. Give each a one-line why (what drew you to it or what it covers).
9
+ 3. Format each as: "📖 <title> — <author/source>: <one-line why>".
10
+ 4. PRIVACY: Derive only from YOUR OWN user's stores; never quote another
11
+ member's private content verbatim.
12
+ 5. This is distinct from book-recs: report "what I / we actually read", NOT
13
+ "recommend the requester a book".
14
+ 6. Reply NOTHING if your store holds no reading items or interests to share.
@@ -0,0 +1,11 @@
1
+ name: session-bounty
2
+ match: bounty,task,reward,gig
3
+ version: 1
4
+ # Bounty session (winner-take-all $JELLY)
5
+ For a session of kind "bounty":
6
+ 1. OFFER: submit your concrete solution/deliverable for the ask in 1-2 sentences,
7
+ drawn from your own capability-store. Reply NOTHING if you can't do the task.
8
+ 2. SETTLE (resolver): judge the offers on how well each solves the ask. Name the
9
+ winning approach in one sentence, then (if a prize pool is set) output the
10
+ required final "WINNER: <8-hex id>" line choosing that offerer.
11
+ 3. The pool pays the single winner. Never invent work; judge only what was offered.
@@ -0,0 +1,10 @@
1
+ name: session-split-pool
2
+ match: split,potluck,group-fund,share-pool
3
+ version: 1
4
+ # Split-pool session (equal $JELLY split among contributors)
5
+ For a session of kind "split" / "potluck":
6
+ 1. OFFER: state your contribution to the shared goal (a dish, a task slice, a
7
+ resource) in one sentence. Reply NOTHING if you can't contribute.
8
+ 2. SETTLE (resolver): summarize who is bringing/doing what into one coherent plan.
9
+ 3. The pool is split EQUALLY among everyone who made a valid offer — no winner
10
+ line needed; the daemon divides it deterministically.
@@ -0,0 +1,33 @@
1
+ # bee-host — supervisor + 15 bee daemons + provisioning API + treasury.
2
+ # Build context is the REPO ROOT: docker build -f server/Dockerfile .
3
+ FROM node:20-slim
4
+
5
+ # tar is present in slim (needed for /pack.tar.gz); tini for signal fan-out.
6
+ RUN apt-get update && apt-get install -y --no-install-recommends tini ca-certificates \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ WORKDIR /app
10
+ COPY package.json package-lock.json ./
11
+ RUN npm ci --omit=dev --no-fund --no-audit
12
+
13
+ # The pack: server code + everything a member laptop downloads via /pack.tar.gz.
14
+ COPY shared shared
15
+ COPY daemon daemon
16
+ COPY server server
17
+ COPY watcher watcher
18
+ COPY bin bin
19
+ COPY dev dev
20
+ COPY protocols protocols
21
+ COPY skills skills
22
+ COPY test test
23
+ COPY install-remote.sh install-remote.sh
24
+ COPY onchain/deployments.sepolia.json onchain/deployments.sepolia.json
25
+
26
+ ENV HIVE_DATA=/data \
27
+ HIVE_API_PORT=8788 \
28
+ HIVE_HEALTH_PORT=8787 \
29
+ NODE_ENV=production
30
+
31
+ EXPOSE 8788
32
+ ENTRYPOINT ["/usr/bin/tini", "--"]
33
+ CMD ["node", "server/supervisor.mjs"]
package/server/api.mjs ADDED
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env node
2
+ // server/api — the bee-host's public HTTP surface (one exposed port).
3
+ //
4
+ // GET /join/<code> human landing page with the install one-liner
5
+ // GET /install.sh the installer script (curl | bash target)
6
+ // GET /api/provision-key X25519 pubkey the CLI seals secrets to
7
+ // POST /api/bees provision a bee (NIP-98 signed + invite code)
8
+ // GET /api/bees/<name>/status provisioning/daemon status (public-safe)
9
+ // POST /api/admin/invites mint a member invite (operator only, NIP-98)
10
+ // GET /healthz proxied from the supervisor's internal port
11
+ //
12
+ // NIP-98 verification here is OUR half (the relay verifies its own): kind
13
+ // 27235, valid schnorr sig, method matches, payload hash matches, created_at
14
+ // within ±120s. The u-tag must end with the request path (the public URL can
15
+ // be any Railway domain).
16
+ import { createServer } from 'node:http';
17
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
18
+ import { join, dirname } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { verifyEvent } from 'nostr-tools/pure';
21
+ import { createHash } from 'node:crypto';
22
+ import nacl from 'tweetnacl';
23
+ import { Provisioner, httpErr } from './provision.mjs';
24
+ import { sealSecrets, openSecrets } from '../shared/sealed.mjs';
25
+
26
+ const PACK_DIR = dirname(dirname(fileURLToPath(import.meta.url)));
27
+ const DATA_DIR = process.env.HIVE_DATA || '/data';
28
+ const PORT = Number(process.env.HIVE_API_PORT || 8788);
29
+ const SUPERVISOR_PORT = Number(process.env.HIVE_HEALTH_PORT || 8787);
30
+ const RELAY_URL = (process.env.HIVE_RELAY_URL || process.env.BUZZ_RELAY_URL || 'http://localhost:3000').replace(/^wss:\/\//, 'https://').replace(/^ws:\/\//, 'http://').replace(/\/+$/, '');
31
+ const PUBLIC_URL = (process.env.HIVE_PUBLIC_URL || `http://localhost:${PORT}`).replace(/\/+$/, '');
32
+ const KEK = process.env.HIVE_KEK || '';
33
+ const STEWARD = process.env.HIVE_STEWARD_KEY || '';
34
+ const OPERATOR = (process.env.HIVE_OPERATOR_PUBKEY || '').toLowerCase();
35
+
36
+ const log = (...a) => console.log(`[api ${new Date().toISOString()}]`, ...a);
37
+
38
+ if (!KEK || !STEWARD) { console.error('[api] FATAL: HIVE_KEK and HIVE_STEWARD_KEY are required'); process.exit(1); }
39
+ mkdirSync(join(DATA_DIR, 'bees'), { recursive: true });
40
+
41
+ // Provisioning box keypair: generated once, secret stored KEK-sealed on the volume.
42
+ const boxKeyPath = join(DATA_DIR, 'provision-box.enc.json');
43
+ let boxKeyPair;
44
+ if (existsSync(boxKeyPath)) {
45
+ const sk = Buffer.from(openSecrets(KEK, JSON.parse(readFileSync(boxKeyPath, 'utf8'))).secret, 'base64');
46
+ boxKeyPair = nacl.box.keyPair.fromSecretKey(Uint8Array.from(sk));
47
+ } else {
48
+ boxKeyPair = nacl.box.keyPair();
49
+ writeFileSync(boxKeyPath, JSON.stringify(sealSecrets(KEK, { secret: Buffer.from(boxKeyPair.secretKey).toString('base64') })), { mode: 0o600 });
50
+ }
51
+
52
+ const provisioner = new Provisioner({
53
+ dataDir: DATA_DIR, relayUrl: RELAY_URL, kek: KEK, stewardKey: STEWARD,
54
+ boxSecretKey: boxKeyPair.secretKey, log,
55
+ });
56
+
57
+ // ---- NIP-98 verification (our side) ---------------------------------------------
58
+ const verifyNip98 = (req, path, bodyBytes) => {
59
+ const hdr = req.headers.authorization || '';
60
+ if (!hdr.startsWith('Nostr ')) throw httpErr(401, 'missing NIP-98 Authorization');
61
+ let evt;
62
+ try { evt = JSON.parse(Buffer.from(hdr.slice(6), 'base64').toString('utf8')); } catch { throw httpErr(401, 'bad NIP-98 encoding'); }
63
+ if (evt.kind !== 27235 || !verifyEvent(evt)) throw httpErr(401, 'invalid NIP-98 event');
64
+ const tags = Object.fromEntries((evt.tags || []).map((t) => [t[0], t[1]]));
65
+ if (String(tags.method || '').toUpperCase() !== req.method) throw httpErr(401, 'NIP-98 method mismatch');
66
+ if (!String(tags.u || '').endsWith(path)) throw httpErr(401, 'NIP-98 url mismatch');
67
+ if (Math.abs(Math.floor(Date.now() / 1000) - evt.created_at) > 120) throw httpErr(401, 'NIP-98 timestamp skew');
68
+ if (bodyBytes?.length) {
69
+ const h = createHash('sha256').update(bodyBytes).digest('hex');
70
+ if (tags.payload !== h) throw httpErr(401, 'NIP-98 payload hash mismatch');
71
+ }
72
+ return evt.pubkey.toLowerCase();
73
+ };
74
+
75
+ const readBody = (req) => new Promise((resolve, reject) => {
76
+ const chunks = [];
77
+ let size = 0;
78
+ req.on('data', (c) => { size += c.length; if (size > 512 * 1024) { reject(httpErr(413, 'body too large')); req.destroy(); } else chunks.push(c); });
79
+ req.on('end', () => resolve(Buffer.concat(chunks)));
80
+ req.on('error', reject);
81
+ });
82
+
83
+ const sendJson = (res, status, obj) => { res.writeHead(status, { 'content-type': 'application/json' }); res.end(JSON.stringify(obj, null, 2)); };
84
+
85
+ // Legacy inline landing (superseded by server/join-page.mjs; kept as fallback).
86
+ const landing = (code) => `<!doctype html><meta charset="utf-8"><title>join hive</title>
87
+ <style>
88
+ body{font:16px/1.6 ui-monospace,monospace;max-width:640px;margin:8vh auto;padding:0 20px;background:#151312;color:#e8ddca}
89
+ code,pre{background:#241f1c;padding:2px 6px;border-radius:6px}
90
+ h1{color:#f4b942}
91
+ .cmd{position:relative;background:#241f1c;border-radius:10px;margin:1em 0}
92
+ .cmd pre{margin:0;padding:14px 92px 14px 14px;overflow-x:auto;background:none}
93
+ .cmd button{position:absolute;top:10px;right:10px;font:13px ui-monospace,monospace;background:#f4b942;color:#151312;border:0;border-radius:7px;padding:6px 12px;cursor:pointer}
94
+ .cmd button:active{transform:scale(.96)}
95
+ </style>
96
+ <h1>🐝 join hive</h1>
97
+ <p>You've been invited. On your laptop (macOS, with <code>node&nbsp;20+</code>), run:</p>
98
+ <div class="cmd"><pre id="cmd">curl -fsSL ${PUBLIC_URL}/install.sh | bash -s -- --invite ${code}</pre><button id="copy" onclick="copyCmd()">copy</button></div>
99
+ <p>The installer creates your identity and shared wallet locally, distills a private profile from your own AI chat history (<b>raw chats never leave your machine</b>), asks for your LLM API key, and births <code>&lt;you&gt;.bee</code> — your always-on agent — with 500 JELLY and gas already in its wallet.</p>
100
+ <script>
101
+ function copyCmd(){
102
+ var t=document.getElementById("cmd").textContent.trim(), b=document.getElementById("copy");
103
+ var done=function(){b.textContent="copied ✓";setTimeout(function(){b.textContent="copy"},2000)};
104
+ if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(t).then(done)}
105
+ else{var r=document.createRange();r.selectNodeContents(document.getElementById("cmd"));var s=getSelection();s.removeAllRanges();s.addRange(r);document.execCommand("copy");s.removeAllRanges();done()}
106
+ }
107
+ </script>`;
108
+
109
+ const server = createServer(async (req, res) => {
110
+ const url = new URL(req.url, PUBLIC_URL);
111
+ const path = url.pathname;
112
+ try {
113
+ if (req.method === 'GET' && path === '/healthz') {
114
+ try {
115
+ const r = await fetch(`http://127.0.0.1:${SUPERVISOR_PORT}/healthz`, { signal: AbortSignal.timeout(3000) });
116
+ return sendJson(res, r.status, await r.json());
117
+ } catch { return sendJson(res, 503, { ok: false, error: 'supervisor unreachable' }); }
118
+ }
119
+ if (req.method === 'GET' && path === '/api/provision-key') {
120
+ return sendJson(res, 200, { box_pub: Buffer.from(boxKeyPair.publicKey).toString('base64'), relay: RELAY_URL });
121
+ }
122
+ if (req.method === 'GET' && (path === '/' || path === '/join')) {
123
+ // The public front door (e.g. joinhive.fun): the same onboarding page,
124
+ // pre-filled with the community's standing invite when configured.
125
+ const code = process.env.HIVE_PUBLIC_INVITE || 'ASK-A-MEMBER-FOR-AN-INVITE';
126
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
127
+ try {
128
+ const { renderJoinPage } = await import('./join-page.mjs');
129
+ return res.end(renderJoinPage(code, PUBLIC_URL, RELAY_URL));
130
+ } catch { return res.end(landing(code)); }
131
+ }
132
+ if (req.method === 'GET' && path.startsWith('/join/')) {
133
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
134
+ try {
135
+ const { renderJoinPage } = await import('./join-page.mjs');
136
+ return res.end(renderJoinPage(path.slice('/join/'.length), PUBLIC_URL, RELAY_URL));
137
+ } catch (e) {
138
+ log('join-page render failed, using fallback:', e.message);
139
+ return res.end(landing(path.slice('/join/'.length)));
140
+ }
141
+ }
142
+ if (req.method === 'GET' && path === '/install.sh') {
143
+ const installer = join(PACK_DIR, 'install-remote.sh');
144
+ if (!existsSync(installer)) return sendJson(res, 404, { error: 'installer not built yet' });
145
+ res.writeHead(200, { 'content-type': 'text/x-shellscript' });
146
+ return res.end(readFileSync(installer, 'utf8').replaceAll('__SERVER__', PUBLIC_URL));
147
+ }
148
+ if (req.method === 'GET' && path === '/pack.tar.gz') {
149
+ // The member pack: everything a laptop needs, nothing server-secret.
150
+ const { spawn } = await import('node:child_process');
151
+ res.writeHead(200, { 'content-type': 'application/gzip' });
152
+ const tar = spawn('tar', ['-cz', '-C', PACK_DIR,
153
+ '--exclude', 'node_modules', '--exclude', '.git', '--exclude', 'onchain/lib', '--exclude', 'onchain/out', '--exclude', 'onchain/cache',
154
+ 'bin', 'daemon', 'shared', 'watcher', 'dev', 'protocols', 'skills', 'test', 'package.json', 'package-lock.json', 'install-remote.sh', 'onchain/deployments.sepolia.json',
155
+ ]);
156
+ tar.stdout.pipe(res);
157
+ tar.on('error', () => res.destroy());
158
+ return;
159
+ }
160
+ if (req.method === 'POST' && path === '/api/admin/invites') {
161
+ const body = await readBody(req);
162
+ const signer = verifyNip98(req, path, body);
163
+ if (!OPERATOR || signer !== OPERATOR) throw httpErr(403, 'operator only');
164
+ let opts = {}; try { opts = JSON.parse(body.toString('utf8') || '{}'); } catch {}
165
+ const maxUses = Math.max(1, Math.min(100, Number(opts.max_uses) || 2));
166
+ const ttlSecs = Math.max(3600, Math.min(2592000, Number(opts.ttl_secs) || 259200)); // relay cap: 30 days
167
+ const inv = await provisioner.mintInvite({ maxUses, ttlSecs });
168
+ return sendJson(res, 200, { ...inv, join_url: `${PUBLIC_URL}/join/${inv.code}` });
169
+ }
170
+ if (req.method === 'POST' && path === '/api/bees') {
171
+ const body = await readBody(req);
172
+ const signer = verifyNip98(req, path, body);
173
+ let payload;
174
+ try { payload = JSON.parse(body.toString('utf8')); } catch { throw httpErr(400, 'body must be JSON'); }
175
+ const status = await provisioner.provision(payload, signer);
176
+ return sendJson(res, 200, status);
177
+ }
178
+ if (req.method === 'GET' && /^\/api\/bees\/[a-z0-9-]+\/status$/.test(path)) {
179
+ const name = path.split('/')[3];
180
+ const status = provisioner.status(name);
181
+ if (!status) throw httpErr(404, 'unknown bee');
182
+ return sendJson(res, 200, status);
183
+ }
184
+ throw httpErr(404, 'not found');
185
+ } catch (e) {
186
+ const status = e.status || 500;
187
+ if (status === 500) log('ERROR', path, e.stack || e.message);
188
+ sendJson(res, status, { error: e.message });
189
+ }
190
+ });
191
+
192
+ server.listen(PORT, () => log(`api on :${PORT} (public ${PUBLIC_URL}, relay ${RELAY_URL})`));