eleven / messenger Public

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

Plain git — no account needed to clone.

Download

Download this file

1#!/usr/bin/env node
2// lchat-dev-mac — seed the NATIVE Mac app with the dev server's space, for
3// click-testing a branch build against tools/lchat-dev-up.mjs.
4//
5// Why this exists: the Mac app's only product entry is "Sign in with Eleven"
6// (the home-server vault), which can't know about a local dev instance — and
7// the join page's "Open in the app" link is deliberately unhandled (joining
8// lives on the web; a native join flow comes later). So for dev builds this
9// script does what the vault would: it mints + claims a fresh add-device
10// token for the seeded "you" account and writes a SavedInstance the app can
11// load.
12//
13// It writes ~/Library/Application Support/lchat/instances.json — the store's
14// legacy READ fallback (InstancesStore in ios/LChatCore Keychain.swift). That
15// file holds the dev identity's PRIVATE KEY in the clear, which is exactly why
16// the app never WRITES it anymore — acceptable here only because this is a
17// throwaway localhost identity; never run this against a real space. The app
18// deletes the file on its first successful keychain save (migration), so
19// re-run the script if the space vanishes after the keychain adopts it.
20//
21// Usage: node tools/lchat-dev-mac.mjs (after tools/lchat-dev-up.mjs; quit
22// the Mac app first — it reads the stores at launch)
23import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
24import { homedir } from "node:os";
25import path from "node:path";
26import { fileURLToPath } from "node:url";
27import { randomSecret, deriveInvite } from "../web/static/crypto.js";
28
29const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
30const cfgPath = path.join(root, ".lchat-dev.json");
31if (!existsSync(cfgPath)) {
32 console.error("No .lchat-dev.json — run tools/lchat-dev-up.mjs first.");
33 process.exit(1);
34}
35const cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
36
37// 1) Mint + claim a fresh add-device invite for the seeded "you" account.
38// wrapped is empty — we already hold the private key locally (dev config);
39// the claim only needs to yield a device token.
40const T = randomSecret();
41const inv = await deriveInvite(T);
42let r = await fetch(cfg.base + "/api/admin/key-invites", {
43 method: "POST",
44 headers: { "Content-Type": "application/json", Authorization: "Bearer " + cfg.adminToken },
45 body: JSON.stringify({ label: "mac-dev", user_id: cfg.you.userId, id: inv.id, wrapped: "", claim_hash: inv.claimHash }),
46});
47if (!r.ok) {
48 console.error("minting the device invite failed:", r.status, await r.text());
49 process.exit(1);
50}
51r = await fetch(`${cfg.base}/api/key-invite/${encodeURIComponent(inv.id)}/claim`, {
52 method: "POST",
53 headers: { "Content-Type": "application/json" },
54 body: JSON.stringify({ claim_secret: inv.claimSecret }),
55});
56if (!r.ok) {
57 console.error("claiming the device invite failed:", r.status, await r.text());
58 process.exit(1);
59}
60const { token } = await r.json();
61
62// 2) Write the SavedInstance (Codable field names — ios/LChatCore Models.swift)
63// and a matching rail entry (SavedChat — ios/LChatMac MacInstance.swift),
64// merging with whatever is already there.
65const dir = path.join(homedir(), "Library/Application Support/lchat");
66mkdirSync(dir, { recursive: true });
67const instPath = path.join(dir, "instances.json");
68const serversPath = path.join(dir, "servers.json");
69
70const instance = {
71 origin: cfg.base,
72 deviceToken: token,
73 screenName: "You",
74 userPub: cfg.you.publicKey,
75 userPriv: cfg.you.privateKey,
76 myUserId: cfg.you.userId,
77 groupName: cfg.groupName || "Dev chat",
78};
79const readJSON = (p) => {
80 try {
81 return JSON.parse(readFileSync(p, "utf8"));
82 } catch {
83 return [];
84 }
85};
86const instances = readJSON(instPath).filter((i) => i && i.origin !== instance.origin);
87instances.push(instance);
88writeFileSync(instPath, JSON.stringify(instances, null, 2) + "\n");
89
90const servers = readJSON(serversPath);
91if (!servers.some((s) => s && typeof s.url === "string" && s.url.startsWith(cfg.base))) {
92 servers.push({ url: cfg.base, name: "You", lastSeenId: 0 });
93 writeFileSync(serversPath, JSON.stringify(servers, null, 2) + "\n");
94}
95
96console.log("Seeded the Mac app with the dev space:");
97console.log(" origin: " + cfg.base);
98console.log(" token: " + token.slice(0, 8) + "… (fresh device token)");
99console.log(" stores: " + instPath);
100console.log(" " + serversPath);
101console.log("Launch the app — the space should be in the rail, chats fully native.");
102