@goodea/echolet 0.1.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 (33) hide show
  1. package/dist/cli.js +39712 -0
  2. package/dist/cli.red-24474.js +37801 -0
  3. package/dist/tui.js +1478 -0
  4. package/package.json +46 -0
  5. package/web-dist/client/assets/concepts/amber_terminal_ui_1789858657697.jpg +0 -0
  6. package/web-dist/client/assets/concepts/avionics_cockpit_ui_1789858538190.jpg +0 -0
  7. package/web-dist/client/assets/concepts/bathyscaphe_sonar_ui_1789858725632.jpg +0 -0
  8. package/web-dist/client/assets/concepts/polar_expedition_ui_1789858484680.jpg +0 -0
  9. package/web-dist/client/assets/concepts/spy_briefcase_ui_1789858596025.jpg +0 -0
  10. package/web-dist/client/assets/skins/audiophile-hifi/chassis.jpg +0 -0
  11. package/web-dist/client/assets/skins/military-r250/chassis.jpg +0 -0
  12. package/web-dist/client/assets/skins/nixie-tube/chassis.jpg +0 -0
  13. package/web-dist/client/assets/skins/nordic-op1/chassis.png +0 -0
  14. package/web-dist/client/assets/skins/oscilloscope-crt/chassis.jpg +0 -0
  15. package/web-dist/client/assets/skins/vintage-radiola/chassis.jpg +0 -0
  16. package/web-dist/client/audiophile_tube_ui_1789836391373.jpg +0 -0
  17. package/web-dist/client/bundle.js +66 -0
  18. package/web-dist/client/cyberdeck_spectrum_ui_1789835241900.jpg +0 -0
  19. package/web-dist/client/echolet_obsidian_clean_1789834739350.jpg +0 -0
  20. package/web-dist/client/echolet_split_horizon_1789834765974.jpg +0 -0
  21. package/web-dist/client/echolet_tactical_terminal_1789834714340.jpg +0 -0
  22. package/web-dist/client/gallery.html +541 -0
  23. package/web-dist/client/glassmorphic_radio_ui_1789835283668.jpg +0 -0
  24. package/web-dist/client/index.html +17 -0
  25. package/web-dist/client/nixie_tube_messenger_1789836184230.jpg +0 -0
  26. package/web-dist/client/op1_minimal_skin_1789836795427.jpg +0 -0
  27. package/web-dist/client/soviet_tube_radio_1789836251332.jpg +0 -0
  28. package/web-dist/client/stealth_zerotrace_ui_1789835261306.jpg +0 -0
  29. package/web-dist/client/steampunk_brass_ui_1789836339337.jpg +0 -0
  30. package/web-dist/client/styles.css +3725 -0
  31. package/web-dist/client/tube_oscilloscope_ui_1789836294852.jpg +0 -0
  32. package/web-dist/client/vintage_radiola_ui_1789836216660.jpg +0 -0
  33. package/web-dist/server.js +460 -0
@@ -0,0 +1,460 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ const require = createRequire(import.meta.url);
4
+
5
+ // ../web/src/server/server.ts
6
+ import http from "node:http";
7
+ import { readFile, writeFile } from "node:fs/promises";
8
+ import { existsSync as existsSync2 } from "node:fs";
9
+ import { resolve as resolve2, extname, dirname as dirname2 } from "node:path";
10
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
11
+
12
+ // ../web/src/server/cliBridge.ts
13
+ import { spawn } from "node:child_process";
14
+ import { existsSync } from "node:fs";
15
+ import { resolve, dirname } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ var HERE = dirname(fileURLToPath(import.meta.url));
18
+ function resolveCliPath(overridePath) {
19
+ if (overridePath && existsSync(overridePath)) {
20
+ return overridePath;
21
+ }
22
+ if (process.env.ECHOLET_CLI_PATH && existsSync(process.env.ECHOLET_CLI_PATH)) {
23
+ return process.env.ECHOLET_CLI_PATH;
24
+ }
25
+ const flattenedNpmPath = resolve(HERE, "./cli.js");
26
+ if (existsSync(flattenedNpmPath)) {
27
+ return flattenedNpmPath;
28
+ }
29
+ const siblingDistPath = resolve(HERE, "../cli/dist/cli.js");
30
+ if (existsSync(siblingDistPath)) {
31
+ return siblingDistPath;
32
+ }
33
+ return resolve(HERE, "../../cli/dist/cli.js");
34
+ }
35
+ var DEFAULT_CLI_PATH = resolveCliPath();
36
+ var CliBridge = class {
37
+ constructor(options) {
38
+ this.options = options;
39
+ }
40
+ async execute(argv, stdinInput) {
41
+ return new Promise((res) => {
42
+ const child = spawn(process.execPath, [this.options.cliPath, ...argv], {
43
+ env: process.env,
44
+ stdio: ["pipe", "pipe", "pipe"]
45
+ });
46
+ let stdout = "";
47
+ let stderr = "";
48
+ child.stdout.on("data", (chunk) => {
49
+ stdout += chunk.toString("utf8");
50
+ });
51
+ child.stderr.on("data", (chunk) => {
52
+ stderr += chunk.toString("utf8");
53
+ });
54
+ child.on("error", (err) => {
55
+ res({
56
+ ok: false,
57
+ code: "SPAWN_ERROR",
58
+ exitCode: 1,
59
+ data: { error: err.message }
60
+ });
61
+ });
62
+ child.on("close", (exitCode) => {
63
+ const code = exitCode ?? 0;
64
+ try {
65
+ const trimmed = stdout.trim();
66
+ if (trimmed.startsWith("{")) {
67
+ const parsed = JSON.parse(trimmed);
68
+ res({
69
+ ok: parsed.ok ?? code === 0,
70
+ code: parsed.error?.code ?? (code === 0 ? "ok" : "ERROR"),
71
+ exitCode: code,
72
+ data: parsed.data ?? parsed
73
+ });
74
+ return;
75
+ }
76
+ } catch {
77
+ }
78
+ res({
79
+ ok: code === 0,
80
+ code: code === 0 ? "ok" : "NON_ZERO_EXIT",
81
+ exitCode: code,
82
+ data: { stdout, stderr }
83
+ });
84
+ });
85
+ if (stdinInput !== void 0) {
86
+ child.stdin.end(stdinInput, "utf8");
87
+ } else {
88
+ child.stdin.end();
89
+ }
90
+ });
91
+ }
92
+ async doctor() {
93
+ return this.execute(["doctor", "--profile", this.options.profileDir, "--json"]);
94
+ }
95
+ async poll() {
96
+ return this.execute(["poll", "--profile", this.options.profileDir, "--json"]);
97
+ }
98
+ async history(contactIdentityId) {
99
+ return this.execute([
100
+ "history",
101
+ "--with",
102
+ contactIdentityId,
103
+ "--profile",
104
+ this.options.profileDir,
105
+ "--json"
106
+ ]);
107
+ }
108
+ async send(toIdentityId, text) {
109
+ return this.execute(
110
+ ["send", "--to", toIdentityId, "--profile", this.options.profileDir, "--json"],
111
+ text
112
+ );
113
+ }
114
+ async publish() {
115
+ return this.execute(["relay", "publish", "--profile", this.options.profileDir, "--json"]);
116
+ }
117
+ async exportContact(outPath) {
118
+ return this.execute([
119
+ "contact",
120
+ "export",
121
+ "--out",
122
+ outPath,
123
+ "--profile",
124
+ this.options.profileDir,
125
+ "--json"
126
+ ]);
127
+ }
128
+ async importContact(cardPath) {
129
+ return this.execute([
130
+ "contact",
131
+ "import",
132
+ "--from",
133
+ cardPath,
134
+ "--yes",
135
+ "--profile",
136
+ this.options.profileDir,
137
+ "--json"
138
+ ]);
139
+ }
140
+ };
141
+
142
+ // ../web/src/server/server.ts
143
+ var HERE2 = dirname2(fileURLToPath2(import.meta.url));
144
+ var CLIENT_DIST = resolve2(HERE2, "client");
145
+ function parseArgv() {
146
+ const argv = process.argv.slice(2);
147
+ let port = 3e3;
148
+ let profileDir = resolve2(process.cwd(), ".tmp/demo-data/alice");
149
+ let label = "Operator";
150
+ let relayUrl = "https://depr.tail5a88fb.ts.net:8443";
151
+ let storeKeyEnv = "ECHOLET_STORE_KEY";
152
+ let cliPath = DEFAULT_CLI_PATH;
153
+ for (let i = 0; i < argv.length; i++) {
154
+ const arg = argv[i];
155
+ const next = argv[i + 1];
156
+ if (arg === "--port" && next) {
157
+ port = parseInt(next, 10);
158
+ i++;
159
+ } else if (arg === "--profile" && next) {
160
+ profileDir = resolve2(next);
161
+ i++;
162
+ } else if (arg === "--label" && next) {
163
+ label = next;
164
+ i++;
165
+ } else if (arg === "--relay-url" && next) {
166
+ relayUrl = next;
167
+ i++;
168
+ } else if (arg === "--store-key-env" && next) {
169
+ storeKeyEnv = next;
170
+ i++;
171
+ } else if (arg === "--cli" && next) {
172
+ cliPath = resolve2(next);
173
+ i++;
174
+ }
175
+ }
176
+ return { port, profileDir, label, relayUrl, storeKeyEnv, cliPath };
177
+ }
178
+ var config = parseArgv();
179
+ var bridge = new CliBridge({
180
+ cliPath: config.cliPath,
181
+ profileDir: config.profileDir,
182
+ storeKeyEnv: config.storeKeyEnv,
183
+ relayUrl: config.relayUrl
184
+ });
185
+ var telemetryLogs = [];
186
+ function logTelemetry(type, message) {
187
+ const now = /* @__PURE__ */ new Date();
188
+ const time = now.toTimeString().split(" ")[0] + "." + String(now.getMilliseconds()).padStart(3, "0");
189
+ const item = {
190
+ id: Math.random().toString(36).substring(2, 9),
191
+ time,
192
+ type,
193
+ message
194
+ };
195
+ telemetryLogs.push(item);
196
+ if (telemetryLogs.length > 200) telemetryLogs.shift();
197
+ broadcastSSE("telemetry", item);
198
+ }
199
+ var sseClients = /* @__PURE__ */ new Set();
200
+ function broadcastSSE(event, data) {
201
+ const payload = `event: ${event}
202
+ data: ${JSON.stringify(data)}
203
+
204
+ `;
205
+ for (const client of sseClients) {
206
+ client.write(payload);
207
+ }
208
+ }
209
+ var cachedProfile = null;
210
+ var lastPingMs = null;
211
+ async function checkRelayPing() {
212
+ const start = Date.now();
213
+ try {
214
+ const controller = new AbortController();
215
+ const timeout = setTimeout(() => controller.abort(), 3e3);
216
+ const res = await fetch(`${config.relayUrl}/health`, { signal: controller.signal });
217
+ clearTimeout(timeout);
218
+ if (res.ok) {
219
+ const ping = Date.now() - start;
220
+ lastPingMs = ping;
221
+ return ping;
222
+ }
223
+ } catch {
224
+ lastPingMs = null;
225
+ }
226
+ return null;
227
+ }
228
+ var polling = false;
229
+ async function pollLoop() {
230
+ if (polling) return;
231
+ polling = true;
232
+ try {
233
+ const outcome = await bridge.poll();
234
+ if (outcome.ok && outcome.data) {
235
+ const received = outcome.data.received ?? 0;
236
+ if (received > 0) {
237
+ logTelemetry("success", `[Inbound] ${received} new encrypted message(s) downloaded from relay`);
238
+ broadcastSSE("new_message", { received });
239
+ }
240
+ } else if (!outcome.ok) {
241
+ logTelemetry("warn", `[Poll Warning] ${outcome.code}`);
242
+ }
243
+ } catch (err) {
244
+ logTelemetry("error", `[Poll Error] ${err.message}`);
245
+ } finally {
246
+ polling = false;
247
+ }
248
+ }
249
+ setInterval(pollLoop, 2500);
250
+ setInterval(async () => {
251
+ const ping = await checkRelayPing();
252
+ if (ping !== null) {
253
+ broadcastSSE("ping", { ping, status: "healthy" });
254
+ } else {
255
+ broadcastSSE("ping", { ping: null, status: "unreachable" });
256
+ }
257
+ }, 5e3);
258
+ (async () => {
259
+ logTelemetry("crypto", `Initializing Echolet Web Node for [${config.label}]`);
260
+ logTelemetry("info", `Profile store: ${config.profileDir}`);
261
+ logTelemetry("info", `Target relay: ${config.relayUrl}`);
262
+ const ping = await checkRelayPing();
263
+ if (ping !== null) {
264
+ logTelemetry("success", `Connected to Relay: ${ping}ms latency`);
265
+ } else {
266
+ logTelemetry("warn", `Relay unreachable or checking...`);
267
+ }
268
+ const doc = await bridge.doctor();
269
+ if (doc.ok) {
270
+ cachedProfile = doc.data;
271
+ logTelemetry("crypto", `Identity verified: ${doc.data.identity_id?.substring(0, 16)}...`);
272
+ logTelemetry("info", `Pinned contacts count: ${doc.data.contact_count ?? 0}`);
273
+ }
274
+ })();
275
+ var MIME_TYPES = {
276
+ ".html": "text/html",
277
+ ".js": "application/javascript",
278
+ ".css": "text/css",
279
+ ".json": "application/json",
280
+ ".svg": "image/svg+xml",
281
+ ".png": "image/png",
282
+ ".jpg": "image/jpeg"
283
+ };
284
+ async function readBody(req) {
285
+ return new Promise((res, rej) => {
286
+ let body = "";
287
+ req.on("data", (chunk) => body += chunk);
288
+ req.on("end", () => {
289
+ try {
290
+ res(body ? JSON.parse(body) : {});
291
+ } catch (err) {
292
+ rej(err);
293
+ }
294
+ });
295
+ req.on("error", rej);
296
+ });
297
+ }
298
+ var server = http.createServer(async (req, res) => {
299
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
300
+ const pathname = url.pathname;
301
+ res.setHeader("Access-Control-Allow-Origin", "*");
302
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
303
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
304
+ if (req.method === "OPTIONS") {
305
+ res.writeHead(204);
306
+ res.end();
307
+ return;
308
+ }
309
+ if (pathname === "/api/events") {
310
+ res.writeHead(200, {
311
+ "Content-Type": "text/event-stream",
312
+ "Cache-Control": "no-cache",
313
+ Connection: "keep-alive"
314
+ });
315
+ res.write("\n");
316
+ sseClients.add(res);
317
+ req.on("close", () => sseClients.delete(res));
318
+ return;
319
+ }
320
+ if (pathname === "/api/status" && req.method === "GET") {
321
+ if (!cachedProfile) {
322
+ const doc = await bridge.doctor();
323
+ if (doc.ok) cachedProfile = doc.data;
324
+ }
325
+ res.writeHead(200, { "Content-Type": "application/json" });
326
+ res.end(JSON.stringify({
327
+ ok: true,
328
+ label: config.label,
329
+ profile: cachedProfile,
330
+ relayUrl: config.relayUrl,
331
+ pingMs: lastPingMs,
332
+ telemetry: telemetryLogs.slice(-50)
333
+ }));
334
+ return;
335
+ }
336
+ if (pathname === "/api/history" && req.method === "GET") {
337
+ const withId = url.searchParams.get("with");
338
+ if (!withId) {
339
+ res.writeHead(400, { "Content-Type": "application/json" });
340
+ res.end(JSON.stringify({ ok: false, error: "Missing 'with' parameter" }));
341
+ return;
342
+ }
343
+ const outcome = await bridge.history(withId);
344
+ res.writeHead(outcome.ok ? 200 : 500, { "Content-Type": "application/json" });
345
+ res.end(JSON.stringify(outcome));
346
+ return;
347
+ }
348
+ if (pathname === "/api/send" && req.method === "POST") {
349
+ try {
350
+ const body = await readBody(req);
351
+ if (!body.to || !body.text) {
352
+ res.writeHead(400, { "Content-Type": "application/json" });
353
+ res.end(JSON.stringify({ ok: false, error: "Missing 'to' or 'text'" }));
354
+ return;
355
+ }
356
+ logTelemetry("crypto", `Encrypting message via Double Ratchet for recipient ${body.to.substring(0, 12)}...`);
357
+ const outcome = await bridge.send(body.to, body.text);
358
+ if (outcome.ok) {
359
+ logTelemetry("success", `[Outbound] Envelope delivered to relay (status: ${outcome.data?.status})`);
360
+ broadcastSSE("outbound_sent", outcome.data);
361
+ } else {
362
+ logTelemetry("error", `Send failure: ${outcome.code}`);
363
+ }
364
+ res.writeHead(outcome.ok ? 200 : 500, { "Content-Type": "application/json" });
365
+ res.end(JSON.stringify(outcome));
366
+ } catch (err) {
367
+ res.writeHead(500, { "Content-Type": "application/json" });
368
+ res.end(JSON.stringify({ ok: false, error: err.message }));
369
+ }
370
+ return;
371
+ }
372
+ if (pathname === "/api/publish" && req.method === "POST") {
373
+ logTelemetry("crypto", `Minting and publishing Signal PreKey Bundles...`);
374
+ const outcome = await bridge.publish();
375
+ if (outcome.ok) {
376
+ logTelemetry("success", `PreKey pool updated: target 20, claimable 20`);
377
+ } else {
378
+ logTelemetry("error", `Publish failed: ${outcome.code}`);
379
+ }
380
+ res.writeHead(outcome.ok ? 200 : 500, { "Content-Type": "application/json" });
381
+ res.end(JSON.stringify(outcome));
382
+ return;
383
+ }
384
+ if (pathname === "/api/contacts/export" && req.method === "GET") {
385
+ const tmpOut = resolve2(config.profileDir, "../export-temp.json");
386
+ const outcome = await bridge.exportContact(tmpOut);
387
+ if (outcome.ok && existsSync2(tmpOut)) {
388
+ const cardJson = await readFile(tmpOut, "utf8");
389
+ res.writeHead(200, { "Content-Type": "application/json" });
390
+ res.end(cardJson);
391
+ return;
392
+ }
393
+ res.writeHead(500, { "Content-Type": "application/json" });
394
+ res.end(JSON.stringify(outcome));
395
+ return;
396
+ }
397
+ if (pathname === "/api/contacts/import" && req.method === "POST") {
398
+ try {
399
+ const body = await readBody(req);
400
+ let cardPath = body.cardPath;
401
+ if (body.cardJson) {
402
+ cardPath = resolve2(config.profileDir, "../import-temp.json");
403
+ await writeFile(cardPath, typeof body.cardJson === "string" ? body.cardJson : JSON.stringify(body.cardJson), "utf8");
404
+ }
405
+ if (!cardPath) {
406
+ res.writeHead(400, { "Content-Type": "application/json" });
407
+ res.end(JSON.stringify({ ok: false, error: "Missing cardPath or cardJson" }));
408
+ return;
409
+ }
410
+ logTelemetry("crypto", `Verifying contact card cryptographic signatures...`);
411
+ const outcome = await bridge.importContact(cardPath);
412
+ if (outcome.ok) {
413
+ logTelemetry("success", `Contact trusted & added to secure address book`);
414
+ const doc = await bridge.doctor();
415
+ if (doc.ok) cachedProfile = doc.data;
416
+ broadcastSSE("contact_added", {});
417
+ } else {
418
+ logTelemetry("error", `Contact import failed: ${outcome.code}`);
419
+ }
420
+ res.writeHead(outcome.ok ? 200 : 500, { "Content-Type": "application/json" });
421
+ res.end(JSON.stringify(outcome));
422
+ } catch (err) {
423
+ res.writeHead(500, { "Content-Type": "application/json" });
424
+ res.end(JSON.stringify({ ok: false, error: err.message }));
425
+ }
426
+ return;
427
+ }
428
+ let filePath = pathname === "/" ? "index.html" : pathname.replace(/^\//, "");
429
+ let target = resolve2(CLIENT_DIST, filePath);
430
+ if (!existsSync2(target)) {
431
+ target = resolve2(CLIENT_DIST, "index.html");
432
+ }
433
+ if (existsSync2(target)) {
434
+ const ext = extname(target).toLowerCase();
435
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
436
+ try {
437
+ const content = await readFile(target);
438
+ res.writeHead(200, { "Content-Type": contentType });
439
+ res.end(content);
440
+ return;
441
+ } catch {
442
+ res.writeHead(500);
443
+ res.end("Error loading file");
444
+ return;
445
+ }
446
+ }
447
+ res.writeHead(404);
448
+ res.end("Not Found");
449
+ });
450
+ server.listen(config.port, () => {
451
+ console.log(`
452
+ =====================================================`);
453
+ console.log(` ECHOLET WEB CLIENT \u2014 ${config.label}`);
454
+ console.log(`=====================================================`);
455
+ console.log(` Local URL: http://localhost:${config.port}`);
456
+ console.log(` Profile: ${config.profileDir}`);
457
+ console.log(` Relay URL: ${config.relayUrl}`);
458
+ console.log(`=====================================================
459
+ `);
460
+ });