meldivo 1.1.0 → 1.1.1
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/dist/server/access.js +45 -36
- package/dist/server/access.js.map +1 -1
- package/package.json +1 -1
package/dist/server/access.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
2
|
// Everything the hub serves, including the page itself, requires the secret. A browser proves
|
|
3
3
|
// it either with the X-Meldivo-Token header (API calls) or with an HttpOnly cookie set by
|
|
4
|
-
// POST /api/unlock.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// POST /api/unlock. An unauthenticated visitor gets a plain "404 Not Found" for every request,
|
|
5
|
+
// so the address gives no hint that anything is there. That page quietly reads the key from
|
|
6
|
+
// the link's #fragment (never sent to servers, so it stays out of proxy logs), exchanges it
|
|
7
|
+
// for the cookie, and reloads; tapping it five times reveals a key field for devices that only
|
|
8
|
+
// have the bare address (e.g. a Home Screen app that lost its cookie).
|
|
7
9
|
const COOKIE_NAME = "meldivo_session";
|
|
8
10
|
const COOKIE_MAX_AGE_S = 365 * 24 * 60 * 60;
|
|
9
11
|
export function createAccessGate(secret) {
|
|
@@ -24,23 +26,24 @@ export function createAccessGate(secret) {
|
|
|
24
26
|
res.setHeader("Cache-Control", "private, no-cache");
|
|
25
27
|
return next();
|
|
26
28
|
}
|
|
27
|
-
|
|
28
|
-
if (req.path === "/api" || req.path.startsWith("/api/"))
|
|
29
|
-
return res.status(404).json({ error: "Not found" });
|
|
30
|
-
if (req.method !== "GET" && req.method !== "HEAD")
|
|
31
|
-
return res.status(404).end();
|
|
32
|
-
res.status(401).type("html").send(LOCK_PAGE);
|
|
29
|
+
notFound(req, res);
|
|
33
30
|
};
|
|
34
31
|
const unlock = (req, res) => {
|
|
35
32
|
const token = typeof req.body?.token === "string" ? req.body.token.trim() : "";
|
|
36
|
-
res.setHeader("Cache-Control", "no-store");
|
|
37
33
|
if (!token || !safeEqual(secret, token))
|
|
38
|
-
return
|
|
34
|
+
return notFound(req, res);
|
|
35
|
+
res.setHeader("Cache-Control", "no-store");
|
|
39
36
|
setSessionCookie(req, res, cookieValue);
|
|
40
37
|
res.status(204).end();
|
|
41
38
|
};
|
|
42
39
|
return { middleware, unlock, isAuthorized };
|
|
43
40
|
}
|
|
41
|
+
// The same bare 404 for every unauthenticated request, page or API, right key path or not.
|
|
42
|
+
function notFound(req, res) {
|
|
43
|
+
res.removeHeader("X-Request-Id");
|
|
44
|
+
res.status(404).set({ "Cache-Control": "no-store", "Content-Type": "text/html" });
|
|
45
|
+
res.end(req.method === "HEAD" ? undefined : NOT_FOUND_PAGE);
|
|
46
|
+
}
|
|
44
47
|
function setSessionCookie(req, res, value) {
|
|
45
48
|
// Secure everywhere except plain-http localhost, whatever a (spoofable) X-Forwarded-Proto says.
|
|
46
49
|
const secure = req.secure || !isLoopbackHost(req.hostname);
|
|
@@ -68,38 +71,44 @@ function safeEqual(expected, candidate) {
|
|
|
68
71
|
const b = Buffer.from(candidate);
|
|
69
72
|
return a.length === b.length && timingSafeEqual(a, b);
|
|
70
73
|
}
|
|
71
|
-
//
|
|
72
|
-
const
|
|
73
|
-
<
|
|
74
|
-
<
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
button{padding:9px 13px;border:0;border-radius:8px;background:#233;color:#dde}</style></head>
|
|
78
|
-
<body><form id="f"><input id="k" type="password" autocomplete="off" aria-label="Key"><button>Open</button></form>
|
|
74
|
+
// Looks like a web server's stock 404. The script only acts on a #token= link or five taps.
|
|
75
|
+
const NOT_FOUND_PAGE = `<html>
|
|
76
|
+
<head><title>404 Not Found</title><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow"></head>
|
|
77
|
+
<body>
|
|
78
|
+
<center><h1>404 Not Found</h1></center>
|
|
79
|
+
<hr>
|
|
79
80
|
<script>
|
|
80
81
|
(function () {
|
|
81
|
-
function keyFrom(value) {
|
|
82
|
-
var match = /(?:^|[#&?])token=([^&]+)/.exec(value);
|
|
83
|
-
return decodeURIComponent(match ? match[1] : value).trim();
|
|
84
|
-
}
|
|
85
82
|
function unlock(key) {
|
|
86
83
|
return fetch("/api/unlock", { method: "POST", headers: { "Content-Type": "application/json" },
|
|
87
84
|
credentials: "same-origin", body: JSON.stringify({ token: key }) }).then(function (r) { return r.ok; });
|
|
88
85
|
}
|
|
89
|
-
var
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
86
|
+
var match = /[#&]token=([^&]+)/.exec(location.hash);
|
|
87
|
+
if (match) unlock(decodeURIComponent(match[1]).trim()).then(function (ok) { if (ok) location.reload(); });
|
|
88
|
+
var taps = 0, first = 0;
|
|
89
|
+
document.addEventListener("click", function () {
|
|
90
|
+
var now = Date.now();
|
|
91
|
+
if (now - first > 3000) { first = now; taps = 0; }
|
|
92
|
+
if (++taps < 5 || document.querySelector("input")) return;
|
|
93
|
+
var input = document.createElement("input");
|
|
94
|
+
input.type = "password";
|
|
95
|
+
input.autocomplete = "off";
|
|
96
|
+
input.addEventListener("keydown", function (event) {
|
|
97
|
+
if (event.key !== "Enter") return;
|
|
98
|
+
var key = input.value.replace(/^.*[#&]token=/, "").trim();
|
|
99
|
+
try { key = decodeURIComponent(key); } catch (e) {}
|
|
100
|
+
unlock(key).then(function (ok) {
|
|
101
|
+
if (!ok) { input.value = ""; return; }
|
|
102
|
+
history.replaceState(null, "", location.pathname + location.search + "#token=" + encodeURIComponent(key));
|
|
103
|
+
location.reload();
|
|
104
|
+
});
|
|
99
105
|
});
|
|
106
|
+
document.body.appendChild(input);
|
|
107
|
+
input.focus();
|
|
100
108
|
});
|
|
101
|
-
if (hashKey && location.hash) unlock(hashKey).then(function (ok) { ok ? location.reload() : showForm(); }, showForm);
|
|
102
|
-
else showForm();
|
|
103
109
|
})();
|
|
104
|
-
</script
|
|
110
|
+
</script>
|
|
111
|
+
</body>
|
|
112
|
+
</html>
|
|
113
|
+
`;
|
|
105
114
|
//# sourceMappingURL=access.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access.js","sourceRoot":"","sources":["../../server/src/access.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG1D,8FAA8F;AAC9F,0FAA0F;AAC1F,
|
|
1
|
+
{"version":3,"file":"access.js","sourceRoot":"","sources":["../../server/src/access.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meldivo",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Always-on local voice hub for coding agents: talk hands-free to your Pi, OpenCode, and Claude Code sessions from any browser or phone. Private on-device speech (Parakeet + Kokoro). No cloud speech APIs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|