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.
@@ -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. The lock page below is the only thing an unauthenticated visitor gets: it
5
- // reads the key from the link's #fragment (never sent to servers, so it stays out of proxy
6
- // logs), exchanges it for the cookie, and reloads.
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
- res.setHeader("Cache-Control", "no-store");
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 res.status(401).json({ error: "Invalid key" });
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
- // Deliberately bare: no title, branding, or hint of what is behind it.
72
- const LOCK_PAGE = `<!doctype html>
73
- <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
74
- <meta name="robots" content="noindex,nofollow"><title></title>
75
- <style>body{margin:0;min-height:100vh;display:grid;place-items:center;background:#050609;color:#aab;font:14px system-ui,sans-serif}
76
- form{display:none;gap:8px}input{width:260px;padding:9px 11px;border:1px solid #334;border-radius:8px;background:#0c0f16;color:#dde}
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 hashKey = keyFrom(location.hash);
90
- var form = document.getElementById("f");
91
- function showForm() { form.style.display = "flex"; }
92
- form.addEventListener("submit", function (event) {
93
- event.preventDefault();
94
- var key = keyFrom(document.getElementById("k").value);
95
- unlock(key).then(function (ok) {
96
- if (!ok) return;
97
- history.replaceState(null, "", location.pathname + location.search + "#token=" + encodeURIComponent(key));
98
- location.reload();
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></body></html>`;
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":"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"}
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,+FAA+F;AAC/F,4FAA4F;AAC5F,4FAA4F;AAC5F,+FAA+F;AAC/F,uEAAuE;AAEvE,MAAM,WAAW,GAAG,iBAAiB,CAAC;AACtC,MAAM,gBAAgB,GAAG,GAAG,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAW5C,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,kFAAkF;IAClF,MAAM,WAAW,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAE5F,SAAS,YAAY,CAAC,GAAY;QAChC,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,iBAAiB,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,eAAe,CAAC,EAAE,KAAK,CAAC,kBAAkB,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACrG,IAAI,KAAK,IAAI,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACnD,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;QAC5C,OAAO,MAAM,KAAK,SAAS,IAAI,SAAS,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IAChE,CAAC;IAED,MAAM,UAAU,GAAmB,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACpD,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,IAAI,GAAG,CAAC,IAAI,KAAK,aAAa;YAAE,OAAO,IAAI,EAAE,CAAC;QACvE,IAAI,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;YACtB,mEAAmE;YACnE,GAAG,CAAC,SAAS,CAAC,eAAe,EAAE,mBAAmB,CAAC,CAAC;YACpD,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC;QACD,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACrB,CAAC,CAAC;IAEF,MAAM,MAAM,GAAmB,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QAC1C,MAAM,KAAK,GAAG,OAAO,GAAG,CAAC,IAAI,EAAE,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/E,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC;YAAE,OAAO,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACnE,GAAG,CAAC,SAAS,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;QAC3C,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,WAAW,CAAC,CAAC;QACxC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;IACxB,CAAC,CAAC;IAEF,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;AAC9C,CAAC;AAED,2FAA2F;AAC3F,SAAS,QAAQ,CAAC,GAAY,EAAE,GAAa;IAC3C,GAAG,CAAC,YAAY,CAAC,cAAc,CAAC,CAAC;IACjC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,eAAe,EAAE,UAAU,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;IAClF,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,gBAAgB,CAAC,GAAY,EAAE,GAAa,EAAE,KAAa;IAClE,gGAAgG;IAChG,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC3D,MAAM,UAAU,GAAG,CAAC,GAAG,WAAW,IAAI,KAAK,EAAE,EAAE,QAAQ,EAAE,WAAW,gBAAgB,EAAE,EAAE,UAAU,EAAE,iBAAiB,CAAC,CAAC;IACvH,IAAI,MAAM;QAAE,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtC,GAAG,CAAC,SAAS,CAAC,YAAY,EAAE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AACrD,CAAC;AAED,SAAS,cAAc,CAAC,QAAgB;IACtC,OAAO,QAAQ,KAAK,WAAW,IAAI,QAAQ,KAAK,KAAK,IAAI,QAAQ,KAAK,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AAC3G,CAAC;AAED,SAAS,UAAU,CAAC,GAAY,EAAE,IAAY;IAC5C,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACjC,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAC9B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,KAAK,GAAG,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7F,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,SAAS,CAAC,QAAgB,EAAE,SAAiB;IACpD,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACjC,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACxD,CAAC;AAED,4FAA4F;AAC5F,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsCtB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meldivo",
3
- "version": "1.1.0",
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",