@inetafrica/open-claudia 2.6.12 → 2.6.13
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.
- package/core/handlers.js +32 -0
- package/core/web-otp.js +44 -0
- package/package.json +1 -1
- package/web.js +16 -0
package/core/handlers.js
CHANGED
|
@@ -302,6 +302,38 @@ register({
|
|
|
302
302
|
},
|
|
303
303
|
});
|
|
304
304
|
|
|
305
|
+
// Where the magic link should point. Explicit WEB_PUBLIC_URL wins; else the
|
|
306
|
+
// machine's first non-internal IPv4 (so a phone on the same wifi can reach
|
|
307
|
+
// it); else localhost.
|
|
308
|
+
function dashboardBaseUrl() {
|
|
309
|
+
const explicit = (process.env.WEB_PUBLIC_URL || "").trim().replace(/\/+$/, "");
|
|
310
|
+
if (explicit) return explicit;
|
|
311
|
+
const port = process.env.WEB_PORT || "8080";
|
|
312
|
+
const ifaces = require("os").networkInterfaces();
|
|
313
|
+
for (const name of Object.keys(ifaces)) {
|
|
314
|
+
for (const ni of ifaces[name] || []) {
|
|
315
|
+
if (ni.family === "IPv4" && !ni.internal) return `http://${ni.address}:${port}`;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return `http://localhost:${port}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
register({
|
|
322
|
+
name: "dashboard", aliases: ["weblink"], description: "Get a one-time login link to the web dashboard", ownerOnly: true,
|
|
323
|
+
handler: async (env) => {
|
|
324
|
+
if (!ownerEnv(env)) return;
|
|
325
|
+
const { mint, TTL_MS } = require("./web-otp");
|
|
326
|
+
const { token } = mint();
|
|
327
|
+
const url = `${dashboardBaseUrl()}/otp/${token}`;
|
|
328
|
+
const mins = Math.round(TTL_MS / 60000);
|
|
329
|
+
const lines = [`Web dashboard login (valid ${mins} min, single use):`, url];
|
|
330
|
+
if (process.env.WEB_UI !== "true") {
|
|
331
|
+
lines.push("", "Note: the web UI isn't enabled yet — set WEB_UI=true in ~/.open-claudia/.env and /restart, then this link will work.");
|
|
332
|
+
}
|
|
333
|
+
send(lines.join("\n"));
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
|
|
305
337
|
async function requestAgentSpaceUpgrade() {
|
|
306
338
|
const apiUrl = process.env.AGENTSPACE_API_URL;
|
|
307
339
|
const token = process.env.AGENTSPACE_POD_TOKEN;
|
package/core/web-otp.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// One-time login tokens for the web dashboard. The bot mints a token via
|
|
2
|
+
// the /dashboard command and hands back a magic link; web.js burns the
|
|
3
|
+
// token on first use and sets the session cookie. File-backed so it
|
|
4
|
+
// survives a restart and works whether or not web + bot share a process.
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const crypto = require("crypto");
|
|
9
|
+
const CONFIG_DIR = require("../config-dir");
|
|
10
|
+
|
|
11
|
+
const OTP_FILE = path.join(CONFIG_DIR, ".web-otp.json");
|
|
12
|
+
const TTL_MS = 10 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
function _load() {
|
|
15
|
+
try { return JSON.parse(fs.readFileSync(OTP_FILE, "utf-8")); } catch (e) { return []; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function _save(list) {
|
|
19
|
+
try { fs.writeFileSync(OTP_FILE, JSON.stringify(list), { mode: 0o600 }); } catch (e) {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function mint(ttlMs = TTL_MS) {
|
|
23
|
+
const token = crypto.randomBytes(32).toString("hex");
|
|
24
|
+
const expiresAt = Date.now() + ttlMs;
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
const list = _load().filter((e) => e && e.expiresAt > now);
|
|
27
|
+
list.push({ token, expiresAt });
|
|
28
|
+
_save(list);
|
|
29
|
+
return { token, expiresAt };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Validate and burn (single use). Returns true only if the token was
|
|
33
|
+
// present and unexpired; prunes expired entries either way.
|
|
34
|
+
function consume(token) {
|
|
35
|
+
if (!token || typeof token !== "string") return false;
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
const list = _load();
|
|
38
|
+
const idx = list.findIndex((e) => e && e.token === token && e.expiresAt > now);
|
|
39
|
+
const remaining = list.filter((e, i) => e && e.expiresAt > now && i !== idx);
|
|
40
|
+
if (remaining.length !== list.length) _save(remaining);
|
|
41
|
+
return idx !== -1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { mint, consume, OTP_FILE, TTL_MS };
|
package/package.json
CHANGED
package/web.js
CHANGED
|
@@ -895,6 +895,22 @@ function startWebServer() {
|
|
|
895
895
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
896
896
|
if (req.method === "OPTIONS") { res.writeHead(204); return res.end(); }
|
|
897
897
|
|
|
898
|
+
// One-time magic-link login: /otp/<token> from the bot's /dashboard
|
|
899
|
+
// command. Burns the token, sets the session cookie, redirects to /.
|
|
900
|
+
if (req.method === "GET" && req.url.startsWith("/otp/")) {
|
|
901
|
+
const token = decodeURIComponent(req.url.slice(5).split("?")[0].split("/")[0]);
|
|
902
|
+
const { consume } = require("./core/web-otp");
|
|
903
|
+
if (consume(token)) {
|
|
904
|
+
res.writeHead(302, {
|
|
905
|
+
"Location": "/",
|
|
906
|
+
"Set-Cookie": `oc_session=${authToken()}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400`,
|
|
907
|
+
});
|
|
908
|
+
return res.end();
|
|
909
|
+
}
|
|
910
|
+
res.writeHead(401, { "Content-Type": "text/html" });
|
|
911
|
+
return res.end('<!DOCTYPE html><html><head><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>body{font-family:-apple-system,sans-serif;background:#0f0f0f;color:#e0e0e0;display:flex;align-items:center;justify-content:center;min-height:100vh;text-align:center;padding:20px}</style></head><body><div><h2>Link expired or already used</h2><p style="color:#888">Send /dashboard in the bot for a fresh link.</p></div></body></html>');
|
|
912
|
+
}
|
|
913
|
+
|
|
898
914
|
if (req.url.startsWith("/api/")) {
|
|
899
915
|
let body = "";
|
|
900
916
|
req.on("data", (d) => { body += d; });
|