thevoidforge 21.0.0 → 21.0.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/wizard/danger-room.config.json +5 -0
- package/dist/wizard/ui/app.js +1231 -0
- package/dist/wizard/ui/danger-room-prophecy.js +217 -0
- package/dist/wizard/ui/danger-room.html +626 -0
- package/dist/wizard/ui/danger-room.js +880 -0
- package/dist/wizard/ui/deploy.html +177 -0
- package/dist/wizard/ui/deploy.js +582 -0
- package/dist/wizard/ui/favicon.svg +11 -0
- package/dist/wizard/ui/index.html +394 -0
- package/dist/wizard/ui/lobby.html +228 -0
- package/dist/wizard/ui/lobby.js +783 -0
- package/dist/wizard/ui/login.html +110 -0
- package/dist/wizard/ui/login.js +184 -0
- package/dist/wizard/ui/rollback.js +107 -0
- package/dist/wizard/ui/styles.css +1029 -0
- package/dist/wizard/ui/tower.html +171 -0
- package/dist/wizard/ui/tower.js +444 -0
- package/dist/wizard/ui/war-room-prophecy.js +217 -0
- package/dist/wizard/ui/war-room.html +219 -0
- package/dist/wizard/ui/war-room.js +285 -0
- package/package.json +2 -2
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Login — VoidForge</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
|
8
|
+
<link rel="stylesheet" href="styles.css">
|
|
9
|
+
<style>
|
|
10
|
+
body { min-height: 100vh; display: flex; align-items: center; justify-content: center; }
|
|
11
|
+
|
|
12
|
+
.login-container { width: 100%; max-width: 400px; padding: 24px; }
|
|
13
|
+
.login-header { text-align: center; margin-bottom: 32px; }
|
|
14
|
+
.login-logo { font-size: 24px; font-weight: 700; color: var(--accent); margin-bottom: 4px; }
|
|
15
|
+
.login-subtitle { font-size: 13px; color: var(--text-dim); }
|
|
16
|
+
|
|
17
|
+
.login-card {
|
|
18
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
19
|
+
border-radius: var(--radius); padding: 24px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.totp-field { display: none; }
|
|
23
|
+
.totp-field.visible { display: block; }
|
|
24
|
+
|
|
25
|
+
.login-footer {
|
|
26
|
+
text-align: center; margin-top: 16px; font-size: 12px; color: var(--text-muted);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.setup-section { display: none; }
|
|
30
|
+
.setup-section.active { display: block; }
|
|
31
|
+
.login-section { display: none; }
|
|
32
|
+
.login-section.active { display: block; }
|
|
33
|
+
|
|
34
|
+
.totp-display {
|
|
35
|
+
text-align: center; padding: 16px; margin: 12px 0;
|
|
36
|
+
background: var(--bg-input); border: 1px solid var(--border); border-radius: var(--radius-sm);
|
|
37
|
+
font-family: var(--mono); font-size: 14px; word-break: break-all;
|
|
38
|
+
}
|
|
39
|
+
</style>
|
|
40
|
+
</head>
|
|
41
|
+
<body>
|
|
42
|
+
<a href="#setup-section" class="skip-nav">Skip to main content</a>
|
|
43
|
+
<noscript><div class="noscript-msg">VoidForge requires JavaScript to run.</div></noscript>
|
|
44
|
+
<main class="login-container" aria-label="VoidForge Login">
|
|
45
|
+
<div class="login-header">
|
|
46
|
+
<div class="login-logo">VoidForge</div>
|
|
47
|
+
<div class="login-subtitle">Avengers Tower Remote Access</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<!-- Setup Form (first time only) -->
|
|
51
|
+
<div class="setup-section" id="setup-section">
|
|
52
|
+
<div class="login-card">
|
|
53
|
+
<h2 style="margin-bottom: 16px;">Initial Setup</h2>
|
|
54
|
+
<p class="subtitle">Create your admin account to secure this instance.</p>
|
|
55
|
+
<div class="field">
|
|
56
|
+
<label for="setup-username">Username</label>
|
|
57
|
+
<input type="text" id="setup-username" autocomplete="username" minlength="3" required>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="field">
|
|
60
|
+
<label for="setup-password">Password</label>
|
|
61
|
+
<input type="password" id="setup-password" autocomplete="new-password" minlength="12" required>
|
|
62
|
+
<div class="field-hint">Minimum 12 characters. This is your login password (not vault password).</div>
|
|
63
|
+
</div>
|
|
64
|
+
<div id="setup-status" class="status-row" role="status" aria-live="polite"></div>
|
|
65
|
+
<div class="btn-row">
|
|
66
|
+
<button class="btn btn-primary" id="setup-submit" style="width: 100%;">Create Account</button>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<!-- TOTP Setup (shown after account creation) -->
|
|
70
|
+
<div id="totp-setup" style="display: none; margin-top: 16px;">
|
|
71
|
+
<h3>Set Up Two-Factor Authentication</h3>
|
|
72
|
+
<p style="font-size: 13px; color: var(--text-dim); margin-bottom: 8px;">Scan this code with Google Authenticator, 1Password, or Authy:</p>
|
|
73
|
+
<div class="totp-display" id="totp-secret"></div>
|
|
74
|
+
<div class="field-hint" style="margin-bottom: 12px;">Or enter this secret manually in your authenticator app.</div>
|
|
75
|
+
<button class="btn btn-primary" id="totp-done" style="width: 100%;">Done — Go to Login</button>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<!-- Login Form -->
|
|
81
|
+
<div class="login-section" id="login-section">
|
|
82
|
+
<div class="login-card">
|
|
83
|
+
<h2 style="margin-bottom: 16px;">Sign In</h2>
|
|
84
|
+
<div class="field">
|
|
85
|
+
<label for="login-username">Username</label>
|
|
86
|
+
<input type="text" id="login-username" autocomplete="username" required>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="field">
|
|
89
|
+
<label for="login-password">Password</label>
|
|
90
|
+
<input type="password" id="login-password" autocomplete="current-password" required>
|
|
91
|
+
</div>
|
|
92
|
+
<div class="field" id="totp-field">
|
|
93
|
+
<label for="login-totp">Authenticator Code</label>
|
|
94
|
+
<input type="text" id="login-totp" autocomplete="one-time-code" inputmode="numeric" maxlength="6" pattern="[0-9]{6}" placeholder="000000">
|
|
95
|
+
</div>
|
|
96
|
+
<div id="login-status" class="status-row" role="status" aria-live="polite"></div>
|
|
97
|
+
<div class="btn-row">
|
|
98
|
+
<button class="btn btn-primary" id="login-submit" style="width: 100%;">Sign In</button>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="login-footer">
|
|
104
|
+
VoidForge Avengers Tower Remote — 5-layer security
|
|
105
|
+
</div>
|
|
106
|
+
</main>
|
|
107
|
+
|
|
108
|
+
<script src="login.js"></script>
|
|
109
|
+
</body>
|
|
110
|
+
</html>
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login page — handles initial setup and authentication.
|
|
3
|
+
* Two flows: setup (first visit) and login (subsequent visits).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function () {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
// ── DOM refs ───────────────────────────────────────
|
|
10
|
+
const setupSection = document.getElementById('setup-section');
|
|
11
|
+
const loginSection = document.getElementById('login-section');
|
|
12
|
+
const setupUsername = document.getElementById('setup-username');
|
|
13
|
+
const setupPassword = document.getElementById('setup-password');
|
|
14
|
+
const setupSubmit = document.getElementById('setup-submit');
|
|
15
|
+
const setupStatus = document.getElementById('setup-status');
|
|
16
|
+
const totpSetup = document.getElementById('totp-setup');
|
|
17
|
+
const totpSecret = document.getElementById('totp-secret');
|
|
18
|
+
const totpDone = document.getElementById('totp-done');
|
|
19
|
+
const loginUsername = document.getElementById('login-username');
|
|
20
|
+
const loginPassword = document.getElementById('login-password');
|
|
21
|
+
const loginTotp = document.getElementById('login-totp');
|
|
22
|
+
const totpField = document.getElementById('totp-field');
|
|
23
|
+
const loginSubmit = document.getElementById('login-submit');
|
|
24
|
+
const loginStatus = document.getElementById('login-status');
|
|
25
|
+
|
|
26
|
+
// ── API helpers ────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
async function checkSession() {
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetch('/api/auth/session');
|
|
31
|
+
const body = await res.json();
|
|
32
|
+
return body.data || {};
|
|
33
|
+
} catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function setupAccount(username, password) {
|
|
39
|
+
const res = await fetch('/api/auth/setup', {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'Content-Type': 'application/json', 'X-VoidForge-Request': '1' },
|
|
42
|
+
body: JSON.stringify({ username, password }),
|
|
43
|
+
});
|
|
44
|
+
const body = await res.json();
|
|
45
|
+
if (!res.ok) throw new Error(body.error || 'Setup failed');
|
|
46
|
+
return body.data;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function loginUser(username, password, totpCode) {
|
|
50
|
+
const res = await fetch('/api/auth/login', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'Content-Type': 'application/json', 'X-VoidForge-Request': '1' },
|
|
53
|
+
body: JSON.stringify({ username, password, totpCode }),
|
|
54
|
+
});
|
|
55
|
+
const body = await res.json();
|
|
56
|
+
if (!res.ok) throw new Error(body.error || 'Login failed');
|
|
57
|
+
return body.data;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Setup flow ─────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
function showSetup() {
|
|
63
|
+
setupSection.classList.add('active');
|
|
64
|
+
loginSection.classList.remove('active');
|
|
65
|
+
setupUsername.focus();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function showLogin() {
|
|
69
|
+
loginSection.classList.add('active');
|
|
70
|
+
setupSection.classList.remove('active');
|
|
71
|
+
loginUsername.focus();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
setupSubmit.addEventListener('click', async () => {
|
|
75
|
+
const username = setupUsername.value.trim();
|
|
76
|
+
const password = setupPassword.value;
|
|
77
|
+
|
|
78
|
+
if (username.length < 3) {
|
|
79
|
+
setupStatus.textContent = 'Username must be at least 3 characters';
|
|
80
|
+
setupStatus.className = 'status-row error';
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (password.length < 12) {
|
|
84
|
+
setupStatus.textContent = 'Password must be at least 12 characters';
|
|
85
|
+
setupStatus.className = 'status-row error';
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setupSubmit.disabled = true;
|
|
90
|
+
setupStatus.textContent = 'Creating account...';
|
|
91
|
+
setupStatus.className = 'status-row loading';
|
|
92
|
+
|
|
93
|
+
try {
|
|
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';
|
|
101
|
+
} catch (err) {
|
|
102
|
+
setupStatus.textContent = err.message;
|
|
103
|
+
setupStatus.className = 'status-row error';
|
|
104
|
+
setupSubmit.disabled = false;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
totpDone.addEventListener('click', () => {
|
|
109
|
+
showLogin();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ── Login flow ─────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
// TOTP field is always visible (removed conditional show — accessibility fix)
|
|
115
|
+
|
|
116
|
+
loginSubmit.addEventListener('click', async () => {
|
|
117
|
+
const username = loginUsername.value.trim();
|
|
118
|
+
const password = loginPassword.value;
|
|
119
|
+
const totp = loginTotp.value.trim();
|
|
120
|
+
|
|
121
|
+
if (!username || !password) {
|
|
122
|
+
loginStatus.textContent = 'Username and password are required';
|
|
123
|
+
loginStatus.className = 'status-row error';
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!totp || totp.length !== 6) {
|
|
127
|
+
loginStatus.textContent = 'Enter your 6-digit authenticator code';
|
|
128
|
+
loginStatus.className = 'status-row error';
|
|
129
|
+
loginTotp.focus();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
loginSubmit.disabled = true;
|
|
134
|
+
loginStatus.textContent = 'Signing in...';
|
|
135
|
+
loginStatus.className = 'status-row loading';
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
await loginUser(username, password, totp);
|
|
139
|
+
// Success — redirect to The Lobby
|
|
140
|
+
window.location.href = '/lobby.html';
|
|
141
|
+
} catch (err) {
|
|
142
|
+
loginStatus.textContent = err.message;
|
|
143
|
+
loginStatus.className = 'status-row error';
|
|
144
|
+
loginSubmit.disabled = false;
|
|
145
|
+
|
|
146
|
+
// Clear TOTP field on failure (codes are single-use)
|
|
147
|
+
loginTotp.value = '';
|
|
148
|
+
loginTotp.focus();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Submit on Enter from any field
|
|
153
|
+
[loginUsername, loginPassword, loginTotp].forEach((el) => {
|
|
154
|
+
el.addEventListener('keydown', (e) => {
|
|
155
|
+
if (e.key === 'Enter') loginSubmit.click();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
[setupUsername, setupPassword].forEach((el) => {
|
|
160
|
+
el.addEventListener('keydown', (e) => {
|
|
161
|
+
if (e.key === 'Enter') setupSubmit.click();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ── Init ───────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
async function init() {
|
|
168
|
+
const session = await checkSession();
|
|
169
|
+
|
|
170
|
+
if (session.authenticated) {
|
|
171
|
+
// Already logged in — go to The Lobby
|
|
172
|
+
window.location.href = '/lobby.html';
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (session.needsSetup) {
|
|
177
|
+
showSetup();
|
|
178
|
+
} else {
|
|
179
|
+
showLogin();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
init();
|
|
184
|
+
})();
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rollback Panel — Deploy history and one-click rollback for Avengers Tower.
|
|
3
|
+
* Loaded by tower.html. Renders as a collapsible sidebar panel.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function () {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
var panel = document.getElementById('rollback-panel');
|
|
10
|
+
var toggleBtn = document.getElementById('btn-toggle-rollback');
|
|
11
|
+
var deployList = document.getElementById('deploy-list');
|
|
12
|
+
var rollbackStatus = document.getElementById('rollback-status');
|
|
13
|
+
|
|
14
|
+
if (!panel || !toggleBtn || !deployList) return;
|
|
15
|
+
|
|
16
|
+
var projectId = new URLSearchParams(window.location.search).get('project') || '';
|
|
17
|
+
var isOpen = false;
|
|
18
|
+
|
|
19
|
+
function escapeHtml(str) {
|
|
20
|
+
if (str == null) return '';
|
|
21
|
+
var div = document.createElement('div');
|
|
22
|
+
div.appendChild(document.createTextNode(String(str)));
|
|
23
|
+
return div.innerHTML;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function timeAgo(dateStr) {
|
|
27
|
+
if (!dateStr) return 'Unknown';
|
|
28
|
+
var diff = Date.now() - new Date(dateStr).getTime();
|
|
29
|
+
var mins = Math.floor(diff / 60000);
|
|
30
|
+
if (mins < 1) return 'Just now';
|
|
31
|
+
if (mins < 60) return mins + 'm ago';
|
|
32
|
+
var hours = Math.floor(mins / 60);
|
|
33
|
+
if (hours < 24) return hours + 'h ago';
|
|
34
|
+
var days = Math.floor(hours / 24);
|
|
35
|
+
return days + 'd ago';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
toggleBtn.addEventListener('click', function () {
|
|
39
|
+
isOpen = !isOpen;
|
|
40
|
+
panel.classList.toggle('open', isOpen);
|
|
41
|
+
toggleBtn.setAttribute('aria-expanded', String(isOpen));
|
|
42
|
+
if (isOpen) loadDeploys();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
async function loadDeploys() {
|
|
46
|
+
if (!projectId) {
|
|
47
|
+
deployList.innerHTML = '<div style="color: var(--text-dim); font-size: 12px; padding: 8px;">No project selected</div>';
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
deployList.innerHTML = '<div style="color: var(--text-dim); font-size: 12px; padding: 8px;">Loading deploy history...</div>';
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
var res = await fetch('/api/projects/get?id=' + encodeURIComponent(projectId));
|
|
55
|
+
var body = await res.json();
|
|
56
|
+
if (!res.ok || !body.data) {
|
|
57
|
+
deployList.innerHTML = '<div style="color: var(--text-dim); font-size: 12px; padding: 8px;">Could not load project</div>';
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
var project = body.data;
|
|
62
|
+
|
|
63
|
+
// Viewers cannot see deploy details
|
|
64
|
+
if (project.userRole === 'viewer') {
|
|
65
|
+
deployList.innerHTML = '<div style="color: var(--text-dim); font-size: 12px; padding: 8px;">Deploy history requires deployer access</div>';
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
deployList.innerHTML = '';
|
|
69
|
+
|
|
70
|
+
if (!project.lastDeployAt) {
|
|
71
|
+
deployList.innerHTML = '<div style="color: var(--text-dim); font-size: 12px; padding: 8px;">No deploys yet</div>';
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Show the last known deploy as an entry
|
|
76
|
+
var entry = document.createElement('div');
|
|
77
|
+
entry.className = 'deploy-entry';
|
|
78
|
+
entry.innerHTML =
|
|
79
|
+
'<div class="deploy-meta">' +
|
|
80
|
+
'<span class="deploy-time">' + escapeHtml(timeAgo(project.lastDeployAt)) + '</span>' +
|
|
81
|
+
'<span class="deploy-target badge deploy">' + escapeHtml(project.deployTarget) + '</span>' +
|
|
82
|
+
'</div>' +
|
|
83
|
+
'<div class="deploy-url">' + (project.deployUrl ? escapeHtml(project.deployUrl) : 'No URL') + '</div>';
|
|
84
|
+
|
|
85
|
+
deployList.appendChild(entry);
|
|
86
|
+
|
|
87
|
+
// Note: full deploy history requires ~/.voidforge/deploys/ integration (future enhancement)
|
|
88
|
+
var note = document.createElement('div');
|
|
89
|
+
note.style.cssText = 'color: var(--text-dim); font-size: 11px; padding: 8px; border-top: 1px solid var(--border);';
|
|
90
|
+
note.textContent = 'Full deploy history coming in a future update.';
|
|
91
|
+
deployList.appendChild(note);
|
|
92
|
+
|
|
93
|
+
} catch (err) {
|
|
94
|
+
deployList.innerHTML = '<div style="color: var(--error); font-size: 12px; padding: 8px;">Failed to load: ' + escapeHtml(err.message) + '</div>';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Keyboard: Escape closes panel
|
|
99
|
+
document.addEventListener('keydown', function (e) {
|
|
100
|
+
if (e.key === 'Escape' && isOpen) {
|
|
101
|
+
isOpen = false;
|
|
102
|
+
panel.classList.remove('open');
|
|
103
|
+
toggleBtn.setAttribute('aria-expanded', 'false');
|
|
104
|
+
toggleBtn.focus();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
})();
|