mobygate 0.5.2 → 0.5.3
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/CHANGELOG.md +45 -0
- package/index.html +18 -7
- package/lib/config.js +9 -0
- package/package.json +4 -1
- package/server.js +7 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,51 @@ All notable changes to mobygate are documented here. Format loosely follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version numbers are
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.5.3] — 2026-04-19
|
|
8
|
+
|
|
9
|
+
Security pass.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **Default listen address is now `127.0.0.1`** (loopback only). Earlier
|
|
14
|
+
versions called `app.listen(PORT)` with no host, which on macOS binds
|
|
15
|
+
to `::` (IPv6 all interfaces) — meaning anyone on your Wi-Fi could
|
|
16
|
+
reach `:3456`, use your Claude Max subscription, and read your
|
|
17
|
+
request logs. New default blocks that; startup banner now calls
|
|
18
|
+
out the bind ("loopback only" vs "⚠ network-reachable — add auth").
|
|
19
|
+
- **Opt-in LAN sharing** via `bind: 0.0.0.0` (or any specific
|
|
20
|
+
interface) in `~/.mobygate/config.yaml`, or via the `BIND` env
|
|
21
|
+
var. If you opt in, consider putting an auth proxy in front of
|
|
22
|
+
the port — the dashboard and HTTP endpoints have no authentication.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **Dashboard XSS** in live-requests and sessions rows. User-
|
|
27
|
+
controlled fields (`model`, `session key`, `model` on session
|
|
28
|
+
entries) were being interpolated directly into `innerHTML`. A
|
|
29
|
+
malicious local process that can reach :3456 could have
|
|
30
|
+
injected `<script>` via a crafted `model` string and executed
|
|
31
|
+
JS in whichever browser tab had the dashboard open. Added an
|
|
32
|
+
`escHtml()` helper and wrapped every user-controlled field
|
|
33
|
+
interpolated via innerHTML.
|
|
34
|
+
- Added `hono >= 4.12.14` as an npm `overrides` entry to clear
|
|
35
|
+
the single `moderate` audit finding (a transitive via
|
|
36
|
+
`@modelcontextprotocol/sdk` → `hono/jsx`). We don't actually
|
|
37
|
+
load hono/jsx, so it was never exploitable, but `npm audit`
|
|
38
|
+
now reports `0 vulnerabilities` — cleaner for downstream users.
|
|
39
|
+
|
|
40
|
+
### Migration
|
|
41
|
+
|
|
42
|
+
For existing installs: `mobygate update` or
|
|
43
|
+
`npm install -g mobygate@latest` — the postinstall hook restarts
|
|
44
|
+
your service and the new loopback-only bind kicks in.
|
|
45
|
+
|
|
46
|
+
If you were **intentionally** exposing mobygate on the LAN (e.g.,
|
|
47
|
+
"one proxy for the family"), add `bind: 0.0.0.0` to
|
|
48
|
+
`~/.mobygate/config.yaml` and restart. Strongly recommend adding
|
|
49
|
+
an auth proxy (nginx with Basic Auth, Cloudflare Access, etc.)
|
|
50
|
+
in front of the port if you do this.
|
|
51
|
+
|
|
7
52
|
## [0.5.2] — 2026-04-19
|
|
8
53
|
|
|
9
54
|
### Added
|
package/index.html
CHANGED
|
@@ -357,6 +357,17 @@
|
|
|
357
357
|
<script type="module">
|
|
358
358
|
// ───────────────────────── helpers
|
|
359
359
|
const $ = (id) => document.getElementById(id);
|
|
360
|
+
// Escape HTML in user-controlled strings (request model/session/error
|
|
361
|
+
// fields, session keys, etc.) before innerHTML interpolation. The
|
|
362
|
+
// dashboard is unauthenticated, so any process that can reach the
|
|
363
|
+
// proxy could otherwise inject a <script> via a crafted request and
|
|
364
|
+
// execute JS in whoever's tab is viewing the dashboard.
|
|
365
|
+
const escHtml = (s) => String(s ?? '')
|
|
366
|
+
.replace(/&/g, '&')
|
|
367
|
+
.replace(/</g, '<')
|
|
368
|
+
.replace(/>/g, '>')
|
|
369
|
+
.replace(/"/g, '"')
|
|
370
|
+
.replace(/'/g, ''');
|
|
360
371
|
const fmt = {
|
|
361
372
|
time(ts) { return new Date(ts).toLocaleTimeString([], { hour12: false }); },
|
|
362
373
|
ms(n) { return n == null ? '—' : `${n}`; },
|
|
@@ -553,10 +564,10 @@
|
|
|
553
564
|
<div class="w-[72px] shrink-0 text-[#C9D9A8] text-xs leading-4">${fmt.time(startEv.ts)}</div>
|
|
554
565
|
<div class="w-[100px] flex shrink-0 gap-1">${kindChips(startEv)}</div>
|
|
555
566
|
<div class="w-[180px] flex flex-col shrink-0 gap-0.5">
|
|
556
|
-
<div class="text-[#F3EFE4] text-xs leading-4 truncate">${startEv.model || '—'}</div>
|
|
557
|
-
<div class="text-[#5A5F54] text-[10px] leading-3">${fmt.modelBase(startEv.model)} · ${fmt.modelCtx(startEv.resolvedModel)}</div>
|
|
567
|
+
<div class="text-[#F3EFE4] text-xs leading-4 truncate">${escHtml(startEv.model) || '—'}</div>
|
|
568
|
+
<div class="text-[#5A5F54] text-[10px] leading-3">${escHtml(fmt.modelBase(startEv.model))} · ${escHtml(fmt.modelCtx(startEv.resolvedModel))}</div>
|
|
558
569
|
</div>
|
|
559
|
-
<div class="w-[110px] shrink-0 text-[#8A9A6A] text-xs leading-4 truncate" title="${startEv.session || ''}">${startEv.session ? fmt.short(startEv.session) : '—'}</div>
|
|
570
|
+
<div class="w-[110px] shrink-0 text-[#8A9A6A] text-xs leading-4 truncate" title="${escHtml(startEv.session || '')}">${startEv.session ? escHtml(fmt.short(startEv.session)) : '—'}</div>
|
|
560
571
|
<div class="grow flex flex-col gap-1">${latencyBar(endEv)}</div>
|
|
561
572
|
<div class="w-[100px] text-right shrink-0 text-[#8A9A6A] text-[11px] leading-[14px]">${endEv && (endEv.inputTokens || endEv.outputTokens) ? `${endEv.inputTokens || 0}/${endEv.outputTokens || 0}` : '—'}</div>
|
|
562
573
|
<div class="w-[70px] flex justify-end shrink-0">${statusPill(endEv)}</div>
|
|
@@ -680,12 +691,12 @@
|
|
|
680
691
|
const row = document.createElement('div');
|
|
681
692
|
row.className = 'flex items-center py-3 px-6 gap-4 border-b border-[#1A1A15]';
|
|
682
693
|
row.innerHTML = `
|
|
683
|
-
<div class="grow min-w-0 text-[#F3EFE4] text-xs leading-4 truncate" title="${s.key}">${s.key}</div>
|
|
684
|
-
<div class="w-[160px] shrink-0 text-[#8A9A6A] text-xs leading-4 truncate">${s.model || '—'}</div>
|
|
685
|
-
<div class="w-[60px] text-right shrink-0 text-[#8A9A6A] text-xs">${s.messageCount}</div>
|
|
694
|
+
<div class="grow min-w-0 text-[#F3EFE4] text-xs leading-4 truncate" title="${escHtml(s.key)}">${escHtml(s.key)}</div>
|
|
695
|
+
<div class="w-[160px] shrink-0 text-[#8A9A6A] text-xs leading-4 truncate">${escHtml(s.model) || '—'}</div>
|
|
696
|
+
<div class="w-[60px] text-right shrink-0 text-[#8A9A6A] text-xs">${Number(s.messageCount) || 0}</div>
|
|
686
697
|
<div class="w-[80px] text-right shrink-0 text-[#5A5F54] text-[11px]">${fmt.uptime(s.idleSec)}</div>
|
|
687
698
|
<div class="w-[80px] text-right shrink-0 text-[#5A5F54] text-[11px]">${fmt.uptime(s.ttlRemainingSec)} left</div>
|
|
688
|
-
<button class="text-[#E89B2E] text-[11px] hover:brightness-110 shrink-0" data-key="${s.key}">expire</button>
|
|
699
|
+
<button class="text-[#E89B2E] text-[11px] hover:brightness-110 shrink-0" data-key="${escHtml(s.key)}">expire</button>
|
|
689
700
|
`;
|
|
690
701
|
row.querySelector('button').addEventListener('click', async () => {
|
|
691
702
|
await fetch('/sessions/' + encodeURIComponent(s.key), { method: 'DELETE' });
|
package/lib/config.js
CHANGED
|
@@ -27,6 +27,7 @@ export const LOGS_DIR = join(CONFIG_DIR, 'logs');
|
|
|
27
27
|
|
|
28
28
|
const DEFAULTS = {
|
|
29
29
|
port: 3456,
|
|
30
|
+
bind: '127.0.0.1', // loopback only by default (no LAN exposure)
|
|
30
31
|
default_model: 'claude-opus-4-7[1m]',
|
|
31
32
|
session_ttl_minutes: 60,
|
|
32
33
|
max_concurrent: null, // reserved for future (per-session throttling)
|
|
@@ -57,6 +58,7 @@ export function loadConfig() {
|
|
|
57
58
|
|
|
58
59
|
const merged = {
|
|
59
60
|
port: parseInt(process.env.PORT || String(fileConfig.port ?? DEFAULTS.port), 10),
|
|
61
|
+
bind: process.env.BIND || fileConfig.bind || DEFAULTS.bind,
|
|
60
62
|
default_model: process.env.DEFAULT_MODEL || fileConfig.default_model || DEFAULTS.default_model,
|
|
61
63
|
session_ttl_minutes: parseInt(
|
|
62
64
|
process.env.SESSION_TTL_MINUTES
|
|
@@ -91,6 +93,13 @@ export function writeConfig(values = {}) {
|
|
|
91
93
|
`# HTTP port the proxy listens on.`,
|
|
92
94
|
`port: ${merged.port}`,
|
|
93
95
|
'',
|
|
96
|
+
`# Network interface to bind to. Defaults to 127.0.0.1 (loopback only —`,
|
|
97
|
+
`# the proxy is only reachable from this machine). Change to 0.0.0.0 to`,
|
|
98
|
+
`# share it on the LAN (e.g., "one proxy for the whole family"), but be`,
|
|
99
|
+
`# aware: whoever can reach :port can use your Claude Max subscription`,
|
|
100
|
+
`# and read logs containing your prompts. Add auth if you go LAN-public.`,
|
|
101
|
+
`bind: ${JSON.stringify(merged.bind)}`,
|
|
102
|
+
'',
|
|
94
103
|
`# Default Claude model when the client does not specify one.`,
|
|
95
104
|
`# Other aliases (opus, sonnet, haiku) resolve per MODEL_MAP in server.js.`,
|
|
96
105
|
`default_model: ${JSON.stringify(merged.default_model)}`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobygate",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "OpenAI-compatible local proxy for Claude Max. The Möbius-strip gateway: OpenAI shape in, Claude Max out.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
"js-yaml": "^4.1.1",
|
|
23
23
|
"uuid": "^11.1.0"
|
|
24
24
|
},
|
|
25
|
+
"overrides": {
|
|
26
|
+
"hono": ">=4.12.14"
|
|
27
|
+
},
|
|
25
28
|
"engines": {
|
|
26
29
|
"node": ">=18"
|
|
27
30
|
},
|
package/server.js
CHANGED
|
@@ -58,6 +58,10 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
58
58
|
const __dirname = dirname(__filename);
|
|
59
59
|
|
|
60
60
|
const PORT = parseInt(process.env.PORT || '3456', 10);
|
|
61
|
+
// Bind to loopback only by default — no LAN exposure. Users who intentionally
|
|
62
|
+
// want to share the proxy on a network can set bind: 0.0.0.0 (or a specific
|
|
63
|
+
// interface) in ~/.mobygate/config.yaml, but should add auth in front of it.
|
|
64
|
+
const BIND = process.env.BIND || '127.0.0.1';
|
|
61
65
|
const DEFAULT_MODEL = process.env.DEFAULT_MODEL || 'claude-opus-4-7[1m]';
|
|
62
66
|
const SESSION_TTL_MS = parseInt(process.env.SESSION_TTL_MS || String(60 * 60 * 1000), 10); // 1 hour default
|
|
63
67
|
|
|
@@ -1090,14 +1094,14 @@ app.get('/dashboard/logs', async (req, res) => {
|
|
|
1090
1094
|
// Start
|
|
1091
1095
|
// ---------------------------------------------------------------------------
|
|
1092
1096
|
|
|
1093
|
-
app.listen(PORT, async () => {
|
|
1097
|
+
app.listen(PORT, BIND, async () => {
|
|
1094
1098
|
const ttlMin = Math.round(SESSION_TTL_MS / 60000);
|
|
1095
1099
|
const meta = await loadBuildMeta();
|
|
1096
1100
|
console.log(banner({ version: meta.version }));
|
|
1097
|
-
console.log(`
|
|
1101
|
+
console.log(` bind ${BIND}:${PORT}${BIND === '127.0.0.1' ? ' (loopback only)' : ' (⚠ network-reachable — add auth)'}`);
|
|
1098
1102
|
console.log(` model ${DEFAULT_MODEL}`);
|
|
1099
1103
|
console.log(` session TTL ${ttlMin} min`);
|
|
1100
1104
|
console.log(` dashboard http://localhost:${PORT}`);
|
|
1101
1105
|
console.log('');
|
|
1102
|
-
dashboardBus.emitEvent({ type: 'server.boot', port: PORT, defaultModel: DEFAULT_MODEL });
|
|
1106
|
+
dashboardBus.emitEvent({ type: 'server.boot', port: PORT, bind: BIND, defaultModel: DEFAULT_MODEL });
|
|
1103
1107
|
});
|