thevoidforge 21.0.7 → 21.0.9

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,7 +4,7 @@
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)
@@ -114,8 +114,8 @@ addRoute('POST', '/api/auth/logout', async (req, res) => {
114
114
  });
115
115
  // GET /api/auth/session — Check if current session is valid
116
116
  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 } });
117
+ if (!isRemoteMode() && !isLanMode()) {
118
+ sendJson(res, 200, { success: true, data: { authenticated: true, username: 'local', role: 'admin', remoteMode: false, lanMode: false } });
119
119
  return;
120
120
  }
121
121
  const token = parseSessionCookie(req.headers.cookie);
@@ -129,5 +129,5 @@ addRoute('GET', '/api/auth/session', async (req, res) => {
129
129
  sendJson(res, 200, { success: true, data: { authenticated: false, needsSetup: false } });
130
130
  return;
131
131
  }
132
- sendJson(res, 200, { success: true, data: { authenticated: true, username: session.username, role: session.role, remoteMode: true } });
132
+ sendJson(res, 200, { success: true, data: { authenticated: true, username: session.username, role: session.role, remoteMode: isRemoteMode(), lanMode: isLanMode() } });
133
133
  });
@@ -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);
@@ -669,6 +669,50 @@
669
669
  }
670
670
  });
671
671
 
672
+ // PRD file upload + drag-and-drop
673
+ const dropzone = $('#prd-dropzone');
674
+ const fileInput = $('#prd-file-input');
675
+ const prdTextarea = $('#prd-paste');
676
+
677
+ if (dropzone && fileInput) {
678
+ function handlePrdFile(file) {
679
+ const reader = new FileReader();
680
+ reader.onload = function (e) {
681
+ prdTextarea.value = e.target.result;
682
+ showStatus(prdStatus, 'success', 'Loaded: ' + file.name + ' (' + file.size + ' bytes)');
683
+ dropzone.style.borderColor = 'var(--accent, #5b5bf7)';
684
+ setTimeout(function () { dropzone.style.borderColor = ''; }, 2000);
685
+ };
686
+ reader.onerror = function () {
687
+ showStatus(prdStatus, 'error', 'Failed to read file');
688
+ };
689
+ reader.readAsText(file);
690
+ }
691
+
692
+ dropzone.addEventListener('click', function () { fileInput.click(); });
693
+ dropzone.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') fileInput.click(); });
694
+
695
+ fileInput.addEventListener('change', function () {
696
+ if (fileInput.files && fileInput.files[0]) handlePrdFile(fileInput.files[0]);
697
+ });
698
+
699
+ dropzone.addEventListener('dragover', function (e) {
700
+ e.preventDefault();
701
+ dropzone.style.borderColor = 'var(--accent, #5b5bf7)';
702
+ dropzone.style.background = 'rgba(91, 91, 247, 0.05)';
703
+ });
704
+ dropzone.addEventListener('dragleave', function () {
705
+ dropzone.style.borderColor = '';
706
+ dropzone.style.background = '';
707
+ });
708
+ dropzone.addEventListener('drop', function (e) {
709
+ e.preventDefault();
710
+ dropzone.style.borderColor = '';
711
+ dropzone.style.background = '';
712
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) handlePrdFile(e.dataTransfer.files[0]);
713
+ });
714
+ }
715
+
672
716
  let cachedPrompt = null;
673
717
  $('#copy-prd-prompt').addEventListener('click', async () => {
674
718
  const promptCopyStatus = $('#prompt-copy-status');
@@ -177,11 +177,19 @@
177
177
 
178
178
  <div class="tab-panel" id="tab-paste" role="tabpanel" aria-labelledby="tab-btn-paste">
179
179
  <div class="card">
180
- <p>Already have a PRD, or prefer to generate one with a different AI? Paste the finished result below.</p>
180
+ <p>Already have a PRD? Drop the file here, upload it, or paste below.</p>
181
+
182
+ <div class="field">
183
+ <div id="prd-dropzone" style="border: 2px dashed var(--border, #333); border-radius: 8px; padding: 2rem; text-align: center; cursor: pointer; transition: border-color 0.2s, background 0.2s; margin-bottom: 12px;" role="button" tabindex="0" aria-label="Drop a PRD file here or click to upload">
184
+ <p style="margin: 0 0 8px 0; font-size: 1.1em;">Drop your PRD file here</p>
185
+ <p style="margin: 0; opacity: 0.6; font-size: 0.9em;">or click to browse (.md, .txt, .yaml)</p>
186
+ <input type="file" id="prd-file-input" accept=".md,.txt,.yaml,.yml,.markdown" style="display: none;">
187
+ </div>
188
+ </div>
181
189
 
182
190
  <div class="field">
183
191
  <label>PRD Generator Prompt</label>
184
- <p class="field-hint" style="margin-bottom: 8px;">Copy this prompt into ChatGPT, Gemini, or any AI — then paste the output below.</p>
192
+ <p class="field-hint" style="margin-bottom: 8px;">Or copy this prompt into ChatGPT, Gemini, or any AI — then paste the output below.</p>
185
193
  <button class="btn btn-secondary" id="copy-prd-prompt" type="button">Copy Generator Prompt to Clipboard</button>
186
194
  <div class="status-row" id="prompt-copy-status" role="status" aria-live="polite"></div>
187
195
  </div>
@@ -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 */ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thevoidforge",
3
- "version": "21.0.7",
3
+ "version": "21.0.9",
4
4
  "description": "From nothing, everything. A methodology framework for building with Claude Code.",
5
5
  "type": "module",
6
6
  "engines": {