thevoidforge 21.0.8 → 21.0.10

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.
@@ -4,13 +4,13 @@
4
4
  */
5
5
  import { addRoute } from '../router.js';
6
6
  import { parseJsonBody } from '../lib/body-parser.js';
7
- import { hasUsers, createUser, login, logout, validateSession, parseSessionCookie, buildSessionCookie, clearSessionCookie, isRemoteMode, checkRateLimit, getClientIp, getUserRole, isValidUsername, } from '../lib/tower-auth.js';
7
+ import { hasUsers, createUser, login, logout, validateSession, parseSessionCookie, buildSessionCookie, clearSessionCookie, isRemoteMode, isLanMode, checkRateLimit, getClientIp, getUserRole, isValidUsername, } from '../lib/tower-auth.js';
8
8
  import { audit } from '../lib/audit-log.js';
9
9
  import { sendJson } from '../lib/http-helpers.js';
10
10
  // POST /api/auth/setup — Create initial admin user (only when no users exist)
11
11
  addRoute('POST', '/api/auth/setup', async (req, res) => {
12
- if (!isRemoteMode()) {
13
- sendJson(res, 400, { success: false, error: 'Auth setup is only available in remote mode' });
12
+ if (!isRemoteMode() && !isLanMode()) {
13
+ sendJson(res, 400, { success: false, error: 'Auth setup is only available in remote or LAN mode' });
14
14
  return;
15
15
  }
16
16
  // Rate-limit the setup endpoint (prevents race-to-setup attacks)
@@ -58,8 +58,8 @@ addRoute('POST', '/api/auth/setup', async (req, res) => {
58
58
  });
59
59
  // POST /api/auth/login — Authenticate with username + password + TOTP
60
60
  addRoute('POST', '/api/auth/login', async (req, res) => {
61
- if (!isRemoteMode()) {
62
- sendJson(res, 400, { success: false, error: 'Auth is only required in remote mode' });
61
+ if (!isRemoteMode() && !isLanMode()) {
62
+ sendJson(res, 400, { success: false, error: 'Auth is only required in remote or LAN mode' });
63
63
  return;
64
64
  }
65
65
  const body = await parseJsonBody(req);
@@ -68,19 +68,25 @@ addRoute('POST', '/api/auth/login', async (req, res) => {
68
68
  return;
69
69
  }
70
70
  const { username, password, totpCode } = body;
71
- if (typeof username !== 'string' || typeof password !== 'string' || typeof totpCode !== 'string') {
72
- sendJson(res, 400, { success: false, error: 'username, password, and totpCode are required strings' });
71
+ if (typeof username !== 'string' || typeof password !== 'string') {
72
+ sendJson(res, 400, { success: false, error: 'username and password are required strings' });
73
+ return;
74
+ }
75
+ // LAN mode: TOTP is optional (password-only auth)
76
+ const requireTotp = isRemoteMode();
77
+ const totp = typeof totpCode === 'string' ? totpCode : '';
78
+ if (requireTotp && (totp.length !== 6 || !/^\d{6}$/.test(totp))) {
79
+ sendJson(res, 400, { success: false, error: 'totpCode must be exactly 6 digits' });
73
80
  return;
74
81
  }
75
82
  // Cap field lengths to prevent DoS via oversized PBKDF2 input
76
- // QA-R3-018 + CROSS-R4-010: TOTP must be exactly 6 digits per RFC 6238
77
- if (username.length > 64 || password.length > 256 || totpCode.length !== 6 || !/^\d{6}$/.test(totpCode)) {
83
+ if (username.length > 64 || password.length > 256) {
78
84
  sendJson(res, 400, { success: false, error: 'Field length exceeded' });
79
85
  return;
80
86
  }
81
87
  const ip = getClientIp(req);
82
- await audit('login_attempt', ip, username.slice(0, 64), { method: 'password+totp' });
83
- const result = await login(username, password, totpCode, ip);
88
+ await audit('login_attempt', ip, username.slice(0, 64), { method: requireTotp ? 'password+totp' : 'password' });
89
+ const result = await login(username, password, totp, ip);
84
90
  if ('error' in result) {
85
91
  await audit('login_failure', ip, username.slice(0, 64), { reason: result.error });
86
92
  const status = result.retryAfterMs ? 429 : 401;
@@ -114,8 +120,8 @@ addRoute('POST', '/api/auth/logout', async (req, res) => {
114
120
  });
115
121
  // GET /api/auth/session — Check if current session is valid
116
122
  addRoute('GET', '/api/auth/session', async (req, res) => {
117
- if (!isRemoteMode()) {
118
- sendJson(res, 200, { success: true, data: { authenticated: true, username: 'local', role: 'admin', remoteMode: false } });
123
+ if (!isRemoteMode() && !isLanMode()) {
124
+ sendJson(res, 200, { success: true, data: { authenticated: true, username: 'local', role: 'admin', remoteMode: false, lanMode: false } });
119
125
  return;
120
126
  }
121
127
  const token = parseSessionCookie(req.headers.cookie);
@@ -129,5 +135,5 @@ addRoute('GET', '/api/auth/session', async (req, res) => {
129
135
  sendJson(res, 200, { success: true, data: { authenticated: false, needsSetup: false } });
130
136
  return;
131
137
  }
132
- sendJson(res, 200, { success: true, data: { authenticated: true, username: session.username, role: session.role, remoteMode: true } });
138
+ sendJson(res, 200, { success: true, data: { authenticated: true, username: session.username, role: session.role, remoteMode: isRemoteMode(), lanMode: isLanMode() } });
133
139
  });
@@ -159,11 +159,10 @@ async function handleRequest(req, res) {
159
159
  sendJson(res, 403, { success: false, error: 'Missing X-VoidForge-Request header' });
160
160
  return;
161
161
  }
162
- // LAN modefull access. LAN is a private network (ZeroTier, local subnet),
163
- // inherently more secure than remote. No endpoint restrictions.
164
- // Auth: optional password (no TOTP). All features available.
165
- // Auth middleware in remote mode, require valid session for non-exempt paths
166
- if (isRemoteMode()) {
162
+ // Auth middlewarein remote and LAN modes, require valid session for non-exempt paths.
163
+ // LAN mode has full access (same as remote) but simpler auth (password only, no TOTP).
164
+ // Local mode (127.0.0.1) has no auth it's your own machine.
165
+ if (isRemoteMode() || isLanMode()) {
167
166
  const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
168
167
  if (!isAuthExempt(url.pathname)) {
169
168
  const token = parseSessionCookie(req.headers.cookie);
@@ -710,14 +710,14 @@
710
710
  const res = await fetch('/api/auth/session');
711
711
  const body = await res.json();
712
712
  const data = body.data || {};
713
- if (data.remoteMode && data.authenticated) {
713
+ if ((data.remoteMode || data.lanMode) && data.authenticated) {
714
714
  currentUser = { username: data.username || '', role: data.role || 'viewer' };
715
715
  const roleLabel = { admin: 'Admin', deployer: 'Deployer', viewer: 'Viewer' }[data.role] || '';
716
716
  authUser.textContent = data.username + (roleLabel ? ' (' + roleLabel + ')' : '');
717
717
  authUser.style.display = '';
718
718
  btnLogout.style.display = '';
719
719
  }
720
- if (data.remoteMode && !data.authenticated) {
720
+ if ((data.remoteMode || data.lanMode) && !data.authenticated) {
721
721
  window.location.href = '/login.html';
722
722
  }
723
723
  } catch { /* local mode — no auth needed */ }
@@ -92,12 +92,19 @@
92
92
 
93
93
  try {
94
94
  const data = await setupAccount(username, password);
95
- // Show TOTP setup
96
- setupStatus.textContent = '';
97
- setupStatus.className = 'status-row';
98
- totpSecret.textContent = data.totpSecret;
99
- totpSetup.style.display = 'block';
100
- setupSubmit.style.display = 'none';
95
+ if (requireTotp && data.totpSecret) {
96
+ // Show TOTP setup (remote mode)
97
+ setupStatus.textContent = '';
98
+ setupStatus.className = 'status-row';
99
+ totpSecret.textContent = data.totpSecret;
100
+ totpSetup.style.display = 'block';
101
+ setupSubmit.style.display = 'none';
102
+ } else {
103
+ // LAN mode — skip TOTP, go straight to login
104
+ setupStatus.textContent = 'Account created. Please log in.';
105
+ setupStatus.className = 'status-row success';
106
+ setTimeout(function () { showLogin(); }, 1000);
107
+ }
101
108
  } catch (err) {
102
109
  setupStatus.textContent = err.message;
103
110
  setupStatus.className = 'status-row error';
@@ -111,7 +118,15 @@
111
118
 
112
119
  // ── Login flow ─────────────────────────────────────
113
120
 
114
- // TOTP field is always visible (removed conditional show accessibility fix)
121
+ // Detect if TOTP is required (remote mode) or optional (LAN mode)
122
+ var requireTotp = true;
123
+ fetch('/api/auth/session').then(function (r) { return r.json(); }).then(function (d) {
124
+ var data = d.data || {};
125
+ if (data.lanMode && !data.remoteMode) {
126
+ requireTotp = false;
127
+ if (totpField) totpField.style.display = 'none';
128
+ }
129
+ }).catch(function () {});
115
130
 
116
131
  loginSubmit.addEventListener('click', async () => {
117
132
  const username = loginUsername.value.trim();
@@ -123,7 +138,7 @@
123
138
  loginStatus.className = 'status-row error';
124
139
  return;
125
140
  }
126
- if (!totp || totp.length !== 6) {
141
+ if (requireTotp && (!totp || totp.length !== 6)) {
127
142
  loginStatus.textContent = 'Enter your 6-digit authenticator code';
128
143
  loginStatus.className = 'status-row error';
129
144
  loginTotp.focus();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thevoidforge",
3
- "version": "21.0.8",
3
+ "version": "21.0.10",
4
4
  "description": "From nothing, everything. A methodology framework for building with Claude Code.",
5
5
  "type": "module",
6
6
  "engines": {