eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1// Provisioning: turn an admin invite into a persisted agent session.
2//
3// This is web/static/join.js run headlessly. Mint a fresh-person invite in the
4// admin console (ideally the "This is an agent" one so it gets a bot badge) and
5// pass its #fragment as LCHAT_INVITE. The fragment never reaches the server: we
6// derive the invite id from it, generate our own keypair, claim a device token,
7// and register the public key in the roster.
8//
9// Exposed as provision(T, {name, path}) so the runtime (agent.mjs) can
10// self-provision each bot identity on first boot in a container; also runnable
11// directly: `node provision.mjs` (provisions the primary bot).
12import { writeFileSync } from "node:fs";
13import { deriveInvite, generateUserKeypair } from "../web/static/crypto.js";
14import { BASE, AGENT_NAME, SESSION_PATH, requireBase, loadSession } from "./config.mjs";
15
16// provision claims the invite secret T, generates a keypair, registers the
17// pubkey, and persists + returns the session. Throws on any failure.
18export async function provision(T, { name = AGENT_NAME, path = SESSION_PATH } = {}) {
19 const post = (p, body) =>
20 fetch(`${BASE}${p}`, {
21 method: "POST",
22 headers: { "content-type": "application/json" },
23 body: JSON.stringify(body),
24 }).then((r) => r.json());
25
26 const { id, claimSecret } = await deriveInvite(T);
27 const info = await (await fetch(`${BASE}/api/key-invite/${encodeURIComponent(id)}`)).json();
28 if (!info.ok) throw new Error("invite is used or invalid — mint a fresh one");
29 if (info.wrapped) throw new Error("that's an add-device link; use a fresh-person invite");
30
31 const kp = await generateUserKeypair();
32 // The claim must prove knowledge of the invite secret (claim_secret is the
33 // preimage of the stored claim_hash). The screen name is never sent — an agent's
34 // label was set on the server when the admin minted the invite. (Mirrors join.js.)
35 const claim = await post(`/api/key-invite/${encodeURIComponent(id)}/claim`, { claim_secret: claimSecret });
36 if (!claim.ok || !claim.token) throw new Error("couldn't claim the invite (it may have just been used)");
37 await post("/api/pubkey", { token: claim.token, public_key: kp.publicKey });
38
39 const session = { token: claim.token, name, userPub: kp.publicKey, userPriv: kp.privateKey };
40 writeFileSync(path, JSON.stringify(session, null, 2));
41 return session;
42}
43
44// CLI: `node provision.mjs` with LCHAT_INVITE in the environment.
45if (import.meta.url === `file://${process.argv[1]}`) {
46 requireBase();
47 if (loadSession()) {
48 console.error(`Already provisioned (${SESSION_PATH}). Delete it to re-provision.`);
49 process.exit(1);
50 }
51 const T = process.env.LCHAT_INVITE;
52 if (!T) {
53 console.error("Set LCHAT_INVITE to the #fragment of a /join/... link from the admin console.");
54 process.exit(1);
55 }
56 try {
57 await provision(T);
58 console.log(`Provisioned "${AGENT_NAME}". Session written to ${SESSION_PATH}.`);
59 console.log("Now add the agent to a group, or start a DM with it from the app, then run: npm start");
60 } catch (e) {
61 console.error(e.message);
62 process.exit(1);
63 }
64}
65