@lifeaitools/clauth 0.3.0 → 0.3.5
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/cli/api.js +4 -0
- package/cli/commands/serve.js +678 -33
- package/cli/index.js +492 -500
- package/package.json +1 -1
- package/supabase/functions/auth-vault/index.ts +14 -2
package/cli/commands/serve.js
CHANGED
|
@@ -8,7 +8,7 @@ import http from "http";
|
|
|
8
8
|
import fs from "fs";
|
|
9
9
|
import os from "os";
|
|
10
10
|
import path from "path";
|
|
11
|
-
import { getMachineHash, deriveToken } from "../fingerprint.js";
|
|
11
|
+
import { getMachineHash, deriveToken, deriveSeedHash } from "../fingerprint.js";
|
|
12
12
|
import * as api from "../api.js";
|
|
13
13
|
import chalk from "chalk";
|
|
14
14
|
|
|
@@ -36,12 +36,477 @@ function isProcessAlive(pid) {
|
|
|
36
36
|
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// ── Dashboard HTML ───────────────────────────────────────────
|
|
40
|
+
function dashboardHtml(port, whitelist) {
|
|
41
|
+
return `<!DOCTYPE html>
|
|
42
|
+
<html lang="en">
|
|
43
|
+
<head>
|
|
44
|
+
<meta charset="utf-8">
|
|
45
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
46
|
+
<title>clauth vault</title>
|
|
47
|
+
<style>
|
|
48
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
49
|
+
body{background:#0a0f1a;color:#e2e8f0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;min-height:100vh}
|
|
50
|
+
/* ── Lock screen ── */
|
|
51
|
+
#lock-screen{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;padding:2rem}
|
|
52
|
+
.lock-card{background:#1e293b;border:1px solid #334155;border-radius:12px;padding:2.5rem 2rem;width:100%;max-width:380px;text-align:center}
|
|
53
|
+
.lock-icon{font-size:2.5rem;margin-bottom:1rem}
|
|
54
|
+
.lock-title{font-size:1.25rem;font-weight:600;color:#f8fafc;margin-bottom:.4rem}
|
|
55
|
+
.lock-sub{font-size:.85rem;color:#64748b;margin-bottom:1.75rem}
|
|
56
|
+
.lock-input{width:100%;background:#0f172a;border:1px solid #334155;border-radius:8px;color:#e2e8f0;font-family:'Courier New',monospace;font-size:1rem;padding:10px 14px;outline:none;text-align:center;letter-spacing:.1em;transition:border-color .2s;margin-bottom:1rem}
|
|
57
|
+
.lock-input:focus{border-color:#3b82f6}
|
|
58
|
+
.lock-input.error{border-color:#ef4444;animation:shake .3s}
|
|
59
|
+
@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-6px)}75%{transform:translateX(6px)}}
|
|
60
|
+
.btn-unlock{width:100%;background:#3b82f6;color:#fff;border:none;border-radius:8px;padding:10px;font-size:.95rem;font-weight:600;cursor:pointer;transition:background .15s}
|
|
61
|
+
.btn-unlock:hover{background:#2563eb}
|
|
62
|
+
.btn-unlock:disabled{background:#1e3a5f;color:#4a6fa5;cursor:not-allowed}
|
|
63
|
+
.lock-err{color:#f87171;font-size:.82rem;margin-top:.75rem;min-height:1.2em}
|
|
64
|
+
/* ── Main view ── */
|
|
65
|
+
#main-view{display:none;padding:2rem}
|
|
66
|
+
.header{display:flex;align-items:center;gap:10px;margin-bottom:1.5rem;flex-wrap:wrap}
|
|
67
|
+
.header h1{font-size:1.4rem;font-weight:600;flex:1}
|
|
68
|
+
.dot{width:10px;height:10px;border-radius:50%;background:#22c55e;animation:pulse 2s infinite;flex-shrink:0}
|
|
69
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
|
70
|
+
.status-bar{display:flex;gap:1.5rem;margin-bottom:1.5rem;font-size:.82rem;color:#94a3b8;flex-wrap:wrap}
|
|
71
|
+
.status-bar span{color:#e2e8f0;font-weight:500}
|
|
72
|
+
.toolbar{display:flex;gap:8px;margin-bottom:1rem;flex-wrap:wrap;align-items:center}
|
|
73
|
+
.chpw-panel{display:none;background:#1a1f2e;border:1px solid #334155;border-radius:8px;padding:1.25rem;margin-bottom:1.5rem}
|
|
74
|
+
.chpw-panel h3{font-size:.9rem;font-weight:600;color:#f8fafc;margin-bottom:1rem}
|
|
75
|
+
.chpw-row{display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;margin-bottom:.75rem}
|
|
76
|
+
.chpw-field{display:flex;flex-direction:column;gap:4px}
|
|
77
|
+
.chpw-field label{font-size:.75rem;color:#64748b}
|
|
78
|
+
.chpw-input{background:#0f172a;border:1px solid #334155;border-radius:6px;color:#e2e8f0;font-family:'Courier New',monospace;font-size:.88rem;padding:7px 12px;outline:none;width:200px;transition:border-color .2s}
|
|
79
|
+
.chpw-input:focus{border-color:#3b82f6}
|
|
80
|
+
.chpw-foot{display:flex;gap:8px;align-items:center}
|
|
81
|
+
.btn-chpw-save{background:#1e3a5f;color:#60a5fa;padding:7px 18px;font-size:.85rem;border-radius:6px;border:none;cursor:pointer;font-weight:500;transition:background .15s}
|
|
82
|
+
.btn-chpw-save:hover{background:#1e4a7f}
|
|
83
|
+
.chpw-msg{font-size:.82rem}
|
|
84
|
+
.chpw-msg.ok{color:#4ade80} .chpw-msg.fail{color:#f87171}
|
|
85
|
+
.btn-refresh{background:#3b82f6;color:#fff;padding:7px 18px;font-size:.85rem;border-radius:7px;border:none;cursor:pointer;font-weight:500}
|
|
86
|
+
.btn-refresh:hover{background:#2563eb}
|
|
87
|
+
.btn-lock{background:#1e293b;color:#f87171;border:1px solid #334155;padding:7px 16px;font-size:.85rem;border-radius:7px;cursor:pointer;font-weight:500}
|
|
88
|
+
.btn-lock:hover{background:#2d1f1f;border-color:#f87171}
|
|
89
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem}
|
|
90
|
+
.card{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:1.25rem;transition:border-color .2s}
|
|
91
|
+
.card:hover{border-color:#3b82f6}
|
|
92
|
+
.card-name{font-size:1rem;font-weight:600;color:#f8fafc;margin-bottom:3px}
|
|
93
|
+
.card-type{font-size:.78rem;color:#64748b;text-transform:uppercase;letter-spacing:.5px}
|
|
94
|
+
.card-getkey{font-size:.75rem;color:#3b82f6;text-decoration:none;opacity:.7;transition:opacity .15s}
|
|
95
|
+
.card-getkey:hover{opacity:1;text-decoration:underline}
|
|
96
|
+
.card-value{font-family:'Courier New',monospace;font-size:.82rem;color:#22c55e;background:#0f172a;border-radius:4px;padding:8px 10px;margin-top:10px;word-break:break-all;max-height:80px;overflow:auto;display:none}
|
|
97
|
+
.card-actions{margin-top:10px;display:flex;gap:7px;flex-wrap:wrap}
|
|
98
|
+
.set-panel{display:none;margin-top:10px;background:#0f172a;border-radius:6px;padding:10px;border:1px solid #1e3a5f}
|
|
99
|
+
.set-panel label{font-size:.75rem;color:#64748b;display:block;margin-bottom:6px}
|
|
100
|
+
.set-input{width:100%;background:#0a0f1a;border:1px solid #1e3a5f;border-radius:4px;color:#e2e8f0;font-family:'Courier New',monospace;font-size:.85rem;padding:7px 10px;outline:none;resize:vertical;min-height:58px;transition:border-color .2s}
|
|
101
|
+
.set-input:focus{border-color:#3b82f6}
|
|
102
|
+
.set-foot{margin-top:8px;display:flex;gap:8px;align-items:center}
|
|
103
|
+
.set-msg{font-size:.8rem}
|
|
104
|
+
.set-msg.ok{color:#4ade80} .set-msg.fail{color:#f87171}
|
|
105
|
+
.btn{padding:6px 13px;border-radius:6px;border:none;cursor:pointer;font-size:.8rem;font-weight:500;transition:all .15s}
|
|
106
|
+
.btn-reveal{background:#1e3a5f;color:#60a5fa}.btn-reveal:hover{background:#1e4a7f}
|
|
107
|
+
.btn-copy{background:#1a3328;color:#4ade80}.btn-copy:hover{background:#1a4338}
|
|
108
|
+
.btn-set{background:#2d1f4a;color:#a78bfa}.btn-set:hover{background:#3d2f5a}
|
|
109
|
+
.btn-save{background:#1e3a5f;color:#60a5fa;padding:6px 16px}.btn-save:hover{background:#1e4a7f}
|
|
110
|
+
.btn-cancel{background:transparent;color:#64748b;padding:6px 10px}.btn-cancel:hover{color:#94a3b8}
|
|
111
|
+
.btn-check{background:#0f2d2d;color:#34d399;border:1px solid #064e3b;padding:7px 16px;font-size:.85rem;border-radius:7px;cursor:pointer;font-weight:500;transition:all .15s}
|
|
112
|
+
.btn-check:hover{background:#134e4a;border-color:#34d399}.btn-check:disabled{opacity:.5;cursor:not-allowed}
|
|
113
|
+
.status-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;opacity:0;transition:opacity .3s;margin-top:4px;cursor:default}
|
|
114
|
+
.status-dot.checking{background:#f59e0b;opacity:1;animation:pulse 1s infinite}
|
|
115
|
+
.status-dot.ok{background:#22c55e;opacity:1}
|
|
116
|
+
.status-dot.fail{background:#ef4444;opacity:1}
|
|
117
|
+
@keyframes sdot-fade{to{opacity:0}} .status-dot.fading{animation:sdot-fade 1.5s forwards}
|
|
118
|
+
.error-bar{background:#7f1d1d;color:#fca5a5;border:1px solid #991b1b;padding:10px 14px;border-radius:8px;margin-bottom:1rem;display:none;font-size:.85rem}
|
|
119
|
+
.loading{color:#64748b;font-style:italic}
|
|
120
|
+
.footer{margin-top:2rem;font-size:.75rem;color:#475569;text-align:center}
|
|
121
|
+
</style>
|
|
122
|
+
</head>
|
|
123
|
+
<body>
|
|
124
|
+
|
|
125
|
+
<!-- ── Lock screen ──────────────────────────── -->
|
|
126
|
+
<div id="lock-screen">
|
|
127
|
+
<div class="lock-card">
|
|
128
|
+
<div class="lock-icon">🔒</div>
|
|
129
|
+
<div class="lock-title">clauth vault</div>
|
|
130
|
+
<div class="lock-sub">Paste your password to unlock</div>
|
|
131
|
+
<input class="lock-input" id="lock-input" type="password" placeholder="••••••••••••" autocomplete="off">
|
|
132
|
+
<button class="btn-unlock" id="unlock-btn" onclick="unlock()">Unlock</button>
|
|
133
|
+
<div class="lock-err" id="lock-err"></div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<!-- ── Main view (shown after unlock) ──────── -->
|
|
138
|
+
<div id="main-view">
|
|
139
|
+
<div class="header">
|
|
140
|
+
<div class="dot" id="dot"></div>
|
|
141
|
+
<h1>🔐 clauth vault</h1>
|
|
142
|
+
</div>
|
|
143
|
+
<div id="error-bar" class="error-bar"></div>
|
|
144
|
+
<div class="status-bar">
|
|
145
|
+
<div>PID: <span id="s-pid">—</span></div>
|
|
146
|
+
<div>Port: <span id="s-port">${port}</span></div>
|
|
147
|
+
<div>Services: <span id="s-services">${whitelist ? whitelist.join(", ") : "all"}</span></div>
|
|
148
|
+
<div>Failures: <span id="s-fails">—</span></div>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="toolbar">
|
|
151
|
+
<button class="btn-refresh" onclick="loadServices()">↻ Refresh</button>
|
|
152
|
+
<button class="btn-check" id="check-btn" onclick="checkAll()">⬤ Check All</button>
|
|
153
|
+
<button class="btn-lock" onclick="lockVault()">🔒 Lock</button>
|
|
154
|
+
<button class="btn-cancel" style="margin-left:auto" onclick="toggleChangePw()">Change Password</button>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div class="chpw-panel" id="chpw-panel">
|
|
158
|
+
<h3>Change Master Password</h3>
|
|
159
|
+
<div class="chpw-row">
|
|
160
|
+
<div class="chpw-field">
|
|
161
|
+
<label>New password</label>
|
|
162
|
+
<input class="chpw-input" id="chpw-new" type="password" placeholder="min 8 chars" autocomplete="new-password">
|
|
163
|
+
</div>
|
|
164
|
+
<div class="chpw-field">
|
|
165
|
+
<label>Confirm</label>
|
|
166
|
+
<input class="chpw-input" id="chpw-confirm" type="password" placeholder="repeat" autocomplete="new-password">
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
<div class="chpw-foot">
|
|
170
|
+
<button class="btn-chpw-save" onclick="changePassword()">Update Password</button>
|
|
171
|
+
<button class="btn-cancel" onclick="toggleChangePw()">Cancel</button>
|
|
172
|
+
<span class="chpw-msg" id="chpw-msg"></span>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<div id="grid" class="grid"><p class="loading">Loading services…</p></div>
|
|
177
|
+
<div class="footer">localhost:${port} · 127.0.0.1 only · 3-strike lockout</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<script>
|
|
181
|
+
const BASE = "http://127.0.0.1:${port}";
|
|
182
|
+
|
|
183
|
+
const KEY_URLS = {
|
|
184
|
+
"github": "https://github.com/settings/tokens",
|
|
185
|
+
"vercel": "https://vercel.com/account/tokens",
|
|
186
|
+
"supabase-anon": "https://supabase.com/dashboard/project/uvojezuorjgqzmhhgluu/settings/api",
|
|
187
|
+
"supabase-service":"https://supabase.com/dashboard/project/uvojezuorjgqzmhhgluu/settings/api",
|
|
188
|
+
"supabase-db": "https://supabase.com/dashboard/project/uvojezuorjgqzmhhgluu/settings/database",
|
|
189
|
+
"anthropic": "https://console.anthropic.com/settings/keys",
|
|
190
|
+
"cloudflare": "https://dash.cloudflare.com/profile/api-tokens",
|
|
191
|
+
"r2": "https://dash.cloudflare.com/profile/api-tokens",
|
|
192
|
+
"r2-bucket": "https://dash.cloudflare.com/profile/api-tokens",
|
|
193
|
+
"rocketreach": "https://rocketreach.co/account?section=security",
|
|
194
|
+
"namecheap": "https://ap.www.namecheap.com/settings/tools/apiaccess/",
|
|
195
|
+
"neo4j": "https://console.neo4j.io/",
|
|
196
|
+
"npm": "https://www.npmjs.com/settings/~/tokens",
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// ── Boot: check lock state ──────────────────
|
|
200
|
+
async function boot() {
|
|
201
|
+
try {
|
|
202
|
+
const ping = await fetch(BASE + "/ping").then(r => r.json());
|
|
203
|
+
if (ping.locked) {
|
|
204
|
+
showLockScreen();
|
|
205
|
+
} else {
|
|
206
|
+
showMain(ping);
|
|
207
|
+
loadServices();
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
showLockScreen();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function showLockScreen() {
|
|
215
|
+
document.getElementById("lock-screen").style.display = "flex";
|
|
216
|
+
document.getElementById("main-view").style.display = "none";
|
|
217
|
+
setTimeout(() => document.getElementById("lock-input").focus(), 50);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function showMain(ping) {
|
|
221
|
+
document.getElementById("lock-screen").style.display = "none";
|
|
222
|
+
document.getElementById("main-view").style.display = "block";
|
|
223
|
+
if (ping) {
|
|
224
|
+
document.getElementById("s-pid").textContent = ping.pid || "—";
|
|
225
|
+
document.getElementById("s-fails").textContent =
|
|
226
|
+
ping.failures + "/" + (ping.failures + ping.failures_remaining);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Unlock ──────────────────────────────────
|
|
231
|
+
async function unlock() {
|
|
232
|
+
const input = document.getElementById("lock-input");
|
|
233
|
+
const btn = document.getElementById("unlock-btn");
|
|
234
|
+
const err = document.getElementById("lock-err");
|
|
235
|
+
const pw = input.value;
|
|
236
|
+
|
|
237
|
+
if (!pw) { err.textContent = "Password is required."; return; }
|
|
238
|
+
|
|
239
|
+
btn.disabled = true; btn.textContent = "Verifying…"; err.textContent = "";
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const r = await fetch(BASE + "/auth", {
|
|
243
|
+
method: "POST",
|
|
244
|
+
headers: { "Content-Type": "application/json" },
|
|
245
|
+
body: JSON.stringify({ password: pw })
|
|
246
|
+
}).then(r => r.json());
|
|
247
|
+
|
|
248
|
+
if (r.error) throw new Error(r.error);
|
|
249
|
+
|
|
250
|
+
input.value = "";
|
|
251
|
+
const ping = await fetch(BASE + "/ping").then(r => r.json());
|
|
252
|
+
showMain(ping);
|
|
253
|
+
loadServices();
|
|
254
|
+
} catch (e) {
|
|
255
|
+
input.value = "";
|
|
256
|
+
input.className = "lock-input error";
|
|
257
|
+
err.textContent = "✗ " + (e.message || "Invalid password");
|
|
258
|
+
setTimeout(() => input.className = "lock-input", 600);
|
|
259
|
+
} finally {
|
|
260
|
+
btn.disabled = false; btn.textContent = "Unlock";
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── Lock ────────────────────────────────────
|
|
265
|
+
async function lockVault() {
|
|
266
|
+
await fetch(BASE + "/lock", { method: "POST" }).catch(() => {});
|
|
267
|
+
showLockScreen();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── Load services ───────────────────────────
|
|
271
|
+
async function loadServices() {
|
|
272
|
+
const grid = document.getElementById("grid");
|
|
273
|
+
const err = document.getElementById("error-bar");
|
|
274
|
+
err.style.display = "none";
|
|
275
|
+
grid.innerHTML = '<p class="loading">Loading…</p>';
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const status = await fetch(BASE + "/status").then(r => r.json());
|
|
279
|
+
if (status.locked) { showLockScreen(); return; }
|
|
280
|
+
if (status.error) throw new Error(status.error);
|
|
281
|
+
|
|
282
|
+
const services = status.services || [];
|
|
283
|
+
if (!services.length) { grid.innerHTML = '<p class="loading">No services registered.</p>'; return; }
|
|
284
|
+
|
|
285
|
+
grid.innerHTML = services.map(s => \`
|
|
286
|
+
<div class="card">
|
|
287
|
+
<div style="display:flex;align-items:flex-start;justify-content:space-between">
|
|
288
|
+
<div>
|
|
289
|
+
<div class="card-name">\${s.name}</div>
|
|
290
|
+
<div class="card-type">\${s.key_type || "secret"}</div>
|
|
291
|
+
\${KEY_URLS[s.name] ? \`<a class="card-getkey" href="\${KEY_URLS[s.name]}" target="_blank" rel="noopener">↗ Get / rotate key</a>\` : ""}
|
|
292
|
+
</div>
|
|
293
|
+
<div class="status-dot" id="sdot-\${s.name}" title=""></div>
|
|
294
|
+
</div>
|
|
295
|
+
<div class="card-value" id="val-\${s.name}"></div>
|
|
296
|
+
<div class="card-actions">
|
|
297
|
+
<button class="btn btn-reveal" onclick="reveal('\${s.name}', this)">Reveal</button>
|
|
298
|
+
<button class="btn btn-copy" id="copybtn-\${s.name}" style="display:none" onclick="copyKey('\${s.name}')">Copy</button>
|
|
299
|
+
<button class="btn btn-set" onclick="toggleSet('\${s.name}')">Set</button>
|
|
300
|
+
</div>
|
|
301
|
+
<div class="set-panel" id="set-panel-\${s.name}">
|
|
302
|
+
<label>New value for <strong>\${s.name}</strong> — paste here, never in chat</label>
|
|
303
|
+
<textarea class="set-input" id="set-input-\${s.name}" placeholder="Paste credential…" spellcheck="false"></textarea>
|
|
304
|
+
<div class="set-foot">
|
|
305
|
+
<button class="btn btn-save" onclick="saveKey('\${s.name}')">Save</button>
|
|
306
|
+
<button class="btn btn-cancel" onclick="toggleSet('\${s.name}')">Cancel</button>
|
|
307
|
+
<span class="set-msg" id="set-msg-\${s.name}"></span>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
\`).join("");
|
|
312
|
+
} catch (e) {
|
|
313
|
+
err.textContent = "⚠ " + e.message;
|
|
314
|
+
err.style.display = "block";
|
|
315
|
+
document.getElementById("dot").style.background = "#ef4444";
|
|
316
|
+
grid.innerHTML = "";
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ── Reveal ──────────────────────────────────
|
|
321
|
+
async function reveal(name, btn) {
|
|
322
|
+
const valEl = document.getElementById("val-" + name);
|
|
323
|
+
const copyBtn = document.getElementById("copybtn-" + name);
|
|
324
|
+
if (valEl.style.display === "block") {
|
|
325
|
+
valEl.style.display = "none"; copyBtn.style.display = "none";
|
|
326
|
+
btn.textContent = "Reveal"; return;
|
|
327
|
+
}
|
|
328
|
+
valEl.textContent = "fetching…"; valEl.style.display = "block"; btn.textContent = "Hide";
|
|
329
|
+
try {
|
|
330
|
+
const r = await fetch(BASE + "/get/" + name).then(r => r.json());
|
|
331
|
+
if (r.locked) { showLockScreen(); return; }
|
|
332
|
+
if (r.error) throw new Error(r.error);
|
|
333
|
+
valEl.textContent = r.value; copyBtn.style.display = "inline-block";
|
|
334
|
+
} catch (e) {
|
|
335
|
+
valEl.textContent = "Error: " + e.message; valEl.style.color = "#ef4444";
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function copyKey(name) {
|
|
340
|
+
const val = document.getElementById("val-" + name).textContent;
|
|
341
|
+
try {
|
|
342
|
+
await navigator.clipboard.writeText(val);
|
|
343
|
+
const btn = document.getElementById("copybtn-" + name);
|
|
344
|
+
btn.textContent = "Copied!"; setTimeout(() => btn.textContent = "Copy", 1500);
|
|
345
|
+
} catch {}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ── Set key ─────────────────────────────────
|
|
349
|
+
function toggleSet(name) {
|
|
350
|
+
const panel = document.getElementById("set-panel-" + name);
|
|
351
|
+
const input = document.getElementById("set-input-" + name);
|
|
352
|
+
const msg = document.getElementById("set-msg-" + name);
|
|
353
|
+
const open = panel.style.display === "block";
|
|
354
|
+
panel.style.display = open ? "none" : "block";
|
|
355
|
+
if (!open) { input.value = ""; msg.textContent = ""; input.focus(); }
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function saveKey(name) {
|
|
359
|
+
const input = document.getElementById("set-input-" + name);
|
|
360
|
+
const msg = document.getElementById("set-msg-" + name);
|
|
361
|
+
const value = input.value.trim();
|
|
362
|
+
if (!value) { msg.className = "set-msg fail"; msg.textContent = "Value is empty."; return; }
|
|
363
|
+
|
|
364
|
+
msg.className = "set-msg"; msg.textContent = "Saving…";
|
|
365
|
+
try {
|
|
366
|
+
const r = await fetch(BASE + "/set/" + name, {
|
|
367
|
+
method: "POST",
|
|
368
|
+
headers: { "Content-Type": "application/json" },
|
|
369
|
+
body: JSON.stringify({ value })
|
|
370
|
+
}).then(r => r.json());
|
|
371
|
+
|
|
372
|
+
if (r.locked) { showLockScreen(); return; }
|
|
373
|
+
if (r.error) throw new Error(r.error);
|
|
374
|
+
msg.className = "set-msg ok"; msg.textContent = "✓ Saved";
|
|
375
|
+
input.value = "";
|
|
376
|
+
const dot = document.getElementById("sdot-" + name);
|
|
377
|
+
if (dot) { dot.className = "status-dot"; dot.title = ""; }
|
|
378
|
+
setTimeout(() => {
|
|
379
|
+
document.getElementById("set-panel-" + name).style.display = "none";
|
|
380
|
+
msg.textContent = "";
|
|
381
|
+
}, 1800);
|
|
382
|
+
} catch (e) {
|
|
383
|
+
msg.className = "set-msg fail"; msg.textContent = "✗ " + e.message;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ── Check all credentials ───────────────────
|
|
388
|
+
async function checkAll() {
|
|
389
|
+
const btn = document.getElementById("check-btn");
|
|
390
|
+
btn.disabled = true; btn.textContent = "Checking…";
|
|
391
|
+
|
|
392
|
+
// Show checking state on every visible dot
|
|
393
|
+
document.querySelectorAll(".status-dot").forEach(d => {
|
|
394
|
+
d.className = "status-dot checking"; d.title = "Checking…";
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
const r = await fetch(BASE + "/check-all").then(r => r.json());
|
|
399
|
+
if (r.locked) { showLockScreen(); return; }
|
|
400
|
+
if (r.error) throw new Error(r.error);
|
|
401
|
+
|
|
402
|
+
const results = r.results || {};
|
|
403
|
+
for (const [name, result] of Object.entries(results)) {
|
|
404
|
+
const dot = document.getElementById("sdot-" + name);
|
|
405
|
+
if (!dot) continue;
|
|
406
|
+
if (result.ok) {
|
|
407
|
+
dot.className = "status-dot ok"; dot.title = "OK";
|
|
408
|
+
} else {
|
|
409
|
+
dot.className = "status-dot fail";
|
|
410
|
+
dot.title = result.reason || "No key stored";
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Fade green dots after 3 s — red dots stay until next check
|
|
415
|
+
setTimeout(() => {
|
|
416
|
+
document.querySelectorAll(".status-dot.ok").forEach(d => d.classList.add("fading"));
|
|
417
|
+
}, 3000);
|
|
418
|
+
|
|
419
|
+
} catch (e) {
|
|
420
|
+
document.querySelectorAll(".status-dot").forEach(d => { d.className = "status-dot"; d.title = ""; });
|
|
421
|
+
const errBar = document.getElementById("error-bar");
|
|
422
|
+
errBar.textContent = "⚠ Check failed: " + e.message;
|
|
423
|
+
errBar.style.display = "block";
|
|
424
|
+
} finally {
|
|
425
|
+
btn.disabled = false; btn.textContent = "⬤ Check All";
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ── Change password ─────────────────────────
|
|
430
|
+
function toggleChangePw() {
|
|
431
|
+
const panel = document.getElementById("chpw-panel");
|
|
432
|
+
const open = panel.style.display === "block";
|
|
433
|
+
panel.style.display = open ? "none" : "block";
|
|
434
|
+
if (!open) {
|
|
435
|
+
document.getElementById("chpw-new").value = "";
|
|
436
|
+
document.getElementById("chpw-confirm").value = "";
|
|
437
|
+
document.getElementById("chpw-msg").textContent = "";
|
|
438
|
+
document.getElementById("chpw-new").focus();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function changePassword() {
|
|
443
|
+
const newPw = document.getElementById("chpw-new").value;
|
|
444
|
+
const confPw = document.getElementById("chpw-confirm").value;
|
|
445
|
+
const msg = document.getElementById("chpw-msg");
|
|
446
|
+
|
|
447
|
+
if (!newPw) { msg.className = "chpw-msg fail"; msg.textContent = "Enter a new password."; return; }
|
|
448
|
+
if (newPw.length < 8) { msg.className = "chpw-msg fail"; msg.textContent = "Minimum 8 characters."; return; }
|
|
449
|
+
if (newPw !== confPw) { msg.className = "chpw-msg fail"; msg.textContent = "Passwords don't match."; return; }
|
|
450
|
+
|
|
451
|
+
msg.className = "chpw-msg"; msg.textContent = "Updating…";
|
|
452
|
+
try {
|
|
453
|
+
const r = await fetch(BASE + "/change-pw", {
|
|
454
|
+
method: "POST",
|
|
455
|
+
headers: { "Content-Type": "application/json" },
|
|
456
|
+
body: JSON.stringify({ newPassword: newPw })
|
|
457
|
+
}).then(r => r.json());
|
|
458
|
+
|
|
459
|
+
if (r.locked) { showLockScreen(); return; }
|
|
460
|
+
if (r.error) throw new Error(r.error);
|
|
461
|
+
|
|
462
|
+
msg.className = "chpw-msg ok"; msg.textContent = "✓ Password updated";
|
|
463
|
+
document.getElementById("chpw-new").value = "";
|
|
464
|
+
document.getElementById("chpw-confirm").value = "";
|
|
465
|
+
setTimeout(() => {
|
|
466
|
+
document.getElementById("chpw-panel").style.display = "none";
|
|
467
|
+
msg.textContent = "";
|
|
468
|
+
}, 2000);
|
|
469
|
+
} catch (e) {
|
|
470
|
+
msg.className = "chpw-msg fail"; msg.textContent = "✗ " + e.message;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Enter key on lock screen
|
|
475
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
476
|
+
document.getElementById("lock-input").addEventListener("keydown", e => {
|
|
477
|
+
if (e.key === "Enter") unlock();
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
boot();
|
|
482
|
+
</script>
|
|
483
|
+
</body>
|
|
484
|
+
</html>`;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ── Body parser helper ────────────────────────────────────────
|
|
488
|
+
function readBody(req) {
|
|
489
|
+
return new Promise((resolve, reject) => {
|
|
490
|
+
let data = "";
|
|
491
|
+
req.on("data", chunk => { data += chunk; if (data.length > 65536) reject(new Error("Body too large")); });
|
|
492
|
+
req.on("end", () => { try { resolve(JSON.parse(data)); } catch { reject(new Error("Invalid JSON")); } });
|
|
493
|
+
req.on("error", reject);
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
39
497
|
// ── Server logic (shared by foreground + daemon) ─────────────
|
|
40
|
-
function createServer(
|
|
498
|
+
function createServer(initPassword, whitelist, port) {
|
|
41
499
|
const MAX_FAILS = 3;
|
|
42
500
|
let failCount = 0;
|
|
501
|
+
let password = initPassword || null; // null = locked; set via POST /auth
|
|
43
502
|
const machineHash = getMachineHash();
|
|
44
503
|
|
|
504
|
+
const CORS = {
|
|
505
|
+
"Access-Control-Allow-Origin": "*",
|
|
506
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
507
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
508
|
+
};
|
|
509
|
+
|
|
45
510
|
function strike(res, code, message) {
|
|
46
511
|
failCount++;
|
|
47
512
|
const remaining = MAX_FAILS - failCount;
|
|
@@ -55,7 +520,7 @@ function createServer(password, whitelist, port) {
|
|
|
55
520
|
...(failCount >= MAX_FAILS ? { shutdown: true } : {})
|
|
56
521
|
});
|
|
57
522
|
|
|
58
|
-
res.writeHead(code, { "Content-Type": "application/json" });
|
|
523
|
+
res.writeHead(code, { "Content-Type": "application/json", ...CORS });
|
|
59
524
|
res.end(body);
|
|
60
525
|
|
|
61
526
|
if (failCount >= MAX_FAILS) {
|
|
@@ -67,7 +532,7 @@ function createServer(password, whitelist, port) {
|
|
|
67
532
|
}
|
|
68
533
|
|
|
69
534
|
function ok(res, data) {
|
|
70
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
535
|
+
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
71
536
|
res.end(JSON.stringify(data));
|
|
72
537
|
}
|
|
73
538
|
|
|
@@ -79,15 +544,28 @@ function createServer(password, whitelist, port) {
|
|
|
79
544
|
return strike(res, 403, `Rejected non-local address: ${remote}`);
|
|
80
545
|
}
|
|
81
546
|
|
|
547
|
+
// CORS preflight
|
|
548
|
+
if (req.method === "OPTIONS") {
|
|
549
|
+
res.writeHead(204, CORS);
|
|
550
|
+
return res.end();
|
|
551
|
+
}
|
|
552
|
+
|
|
82
553
|
const url = new URL(req.url, `http://127.0.0.1:${port}`);
|
|
83
554
|
const reqPath = url.pathname;
|
|
84
555
|
const method = req.method;
|
|
85
556
|
|
|
557
|
+
// GET / — built-in web dashboard
|
|
558
|
+
if (method === "GET" && reqPath === "/") {
|
|
559
|
+
res.writeHead(200, { "Content-Type": "text/html", ...CORS });
|
|
560
|
+
return res.end(dashboardHtml(port, whitelist));
|
|
561
|
+
}
|
|
562
|
+
|
|
86
563
|
// GET /ping
|
|
87
564
|
if (method === "GET" && reqPath === "/ping") {
|
|
88
565
|
return ok(res, {
|
|
89
566
|
status: "ok",
|
|
90
567
|
pid: process.pid,
|
|
568
|
+
locked: !password,
|
|
91
569
|
failures: failCount,
|
|
92
570
|
failures_remaining: MAX_FAILS - failCount,
|
|
93
571
|
services: whitelist || "all",
|
|
@@ -103,8 +581,19 @@ function createServer(password, whitelist, port) {
|
|
|
103
581
|
return;
|
|
104
582
|
}
|
|
105
583
|
|
|
584
|
+
// Locked guard — returns true if locked and already responded
|
|
585
|
+
function lockedGuard(res) {
|
|
586
|
+
if (!password) {
|
|
587
|
+
res.writeHead(401, { "Content-Type": "application/json", ...CORS });
|
|
588
|
+
res.end(JSON.stringify({ error: "Vault is locked", locked: true }));
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
|
|
106
594
|
// GET /status
|
|
107
595
|
if (method === "GET" && reqPath === "/status") {
|
|
596
|
+
if (lockedGuard(res)) return;
|
|
108
597
|
try {
|
|
109
598
|
const { token, timestamp } = deriveToken(password, machineHash);
|
|
110
599
|
const result = await api.status(password, machineHash, token, timestamp);
|
|
@@ -123,6 +612,7 @@ function createServer(password, whitelist, port) {
|
|
|
123
612
|
// GET /get/:service
|
|
124
613
|
const getMatch = reqPath.match(/^\/get\/([a-zA-Z0-9_-]+)$/);
|
|
125
614
|
if (method === "GET" && getMatch) {
|
|
615
|
+
if (lockedGuard(res)) return;
|
|
126
616
|
const service = getMatch[1].toLowerCase();
|
|
127
617
|
|
|
128
618
|
if (whitelist && !whitelist.includes(service)) {
|
|
@@ -139,6 +629,135 @@ function createServer(password, whitelist, port) {
|
|
|
139
629
|
}
|
|
140
630
|
}
|
|
141
631
|
|
|
632
|
+
// POST /auth — unlock the vault with a password (verifies against Edge Function)
|
|
633
|
+
if (method === "POST" && reqPath === "/auth") {
|
|
634
|
+
let body;
|
|
635
|
+
try { body = await readBody(req); } catch {
|
|
636
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
637
|
+
return res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const pw = body.password;
|
|
641
|
+
if (!pw || typeof pw !== "string") {
|
|
642
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
643
|
+
return res.end(JSON.stringify({ error: "password is required" }));
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
try {
|
|
647
|
+
const { token, timestamp } = deriveToken(pw, machineHash);
|
|
648
|
+
const result = await api.test(pw, machineHash, token, timestamp);
|
|
649
|
+
if (result.error) throw new Error(result.error);
|
|
650
|
+
password = pw; // unlock — store in process memory only
|
|
651
|
+
const logLine = `[${new Date().toISOString()}] Vault unlocked\n`;
|
|
652
|
+
try { fs.appendFileSync(LOG_FILE, logLine); } catch {}
|
|
653
|
+
return ok(res, { ok: true, locked: false });
|
|
654
|
+
} catch {
|
|
655
|
+
// Wrong password — not a lockout strike, just a UI auth attempt
|
|
656
|
+
res.writeHead(401, { "Content-Type": "application/json", ...CORS });
|
|
657
|
+
return res.end(JSON.stringify({ error: "Invalid password" }));
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// POST /lock — clear password from memory
|
|
662
|
+
if (method === "POST" && reqPath === "/lock") {
|
|
663
|
+
password = null;
|
|
664
|
+
const logLine = `[${new Date().toISOString()}] Vault locked\n`;
|
|
665
|
+
try { fs.appendFileSync(LOG_FILE, logLine); } catch {}
|
|
666
|
+
return ok(res, { ok: true, locked: true });
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// GET /check-all — soft health check, no strikes for missing/disabled keys
|
|
670
|
+
if (method === "GET" && reqPath === "/check-all") {
|
|
671
|
+
if (lockedGuard(res)) return;
|
|
672
|
+
try {
|
|
673
|
+
const { token: st, timestamp: sts } = deriveToken(password, machineHash);
|
|
674
|
+
const statusResult = await api.status(password, machineHash, st, sts);
|
|
675
|
+
if (statusResult.error) return strike(res, 502, statusResult.error);
|
|
676
|
+
|
|
677
|
+
const services = (statusResult.services || []).filter(
|
|
678
|
+
s => !whitelist || whitelist.includes(s.name.toLowerCase())
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
const results = {};
|
|
682
|
+
for (const svc of services) {
|
|
683
|
+
try {
|
|
684
|
+
const { token, timestamp } = deriveToken(password, machineHash);
|
|
685
|
+
const r = await api.retrieve(password, machineHash, token, timestamp, svc.name);
|
|
686
|
+
results[svc.name] = r.error
|
|
687
|
+
? { ok: false, reason: r.error }
|
|
688
|
+
: { ok: true };
|
|
689
|
+
} catch (e) {
|
|
690
|
+
results[svc.name] = { ok: false, reason: e.message };
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return ok(res, { results });
|
|
694
|
+
} catch (err) {
|
|
695
|
+
return strike(res, 502, err.message);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// POST /change-pw — change master password (must be unlocked)
|
|
700
|
+
if (method === "POST" && reqPath === "/change-pw") {
|
|
701
|
+
if (lockedGuard(res)) return;
|
|
702
|
+
|
|
703
|
+
let body;
|
|
704
|
+
try { body = await readBody(req); } catch {
|
|
705
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
706
|
+
return res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const { newPassword } = body;
|
|
710
|
+
if (!newPassword || typeof newPassword !== "string" || newPassword.length < 8) {
|
|
711
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
712
|
+
return res.end(JSON.stringify({ error: "newPassword must be at least 8 characters" }));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
try {
|
|
716
|
+
const { token, timestamp } = deriveToken(password, machineHash);
|
|
717
|
+
const newSeedHash = deriveSeedHash(machineHash, newPassword);
|
|
718
|
+
const result = await api.changePassword(password, machineHash, token, timestamp, newSeedHash);
|
|
719
|
+
if (result.error) throw new Error(result.error);
|
|
720
|
+
password = newPassword; // update in-memory password to new one
|
|
721
|
+
const logLine = `[${new Date().toISOString()}] Password changed\n`;
|
|
722
|
+
try { fs.appendFileSync(LOG_FILE, logLine); } catch {}
|
|
723
|
+
return ok(res, { ok: true });
|
|
724
|
+
} catch (err) {
|
|
725
|
+
res.writeHead(502, { "Content-Type": "application/json", ...CORS });
|
|
726
|
+
return res.end(JSON.stringify({ error: err.message }));
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// POST /set/:service — write a new key value into vault
|
|
731
|
+
const setMatch = reqPath.match(/^\/set\/([a-zA-Z0-9_-]+)$/);
|
|
732
|
+
if (method === "POST" && setMatch) {
|
|
733
|
+
if (lockedGuard(res)) return;
|
|
734
|
+
const service = setMatch[1].toLowerCase();
|
|
735
|
+
|
|
736
|
+
if (whitelist && !whitelist.includes(service)) {
|
|
737
|
+
return strike(res, 403, `Service '${service}' not in whitelist`);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
let body;
|
|
741
|
+
try { body = await readBody(req); } catch {
|
|
742
|
+
return strike(res, 400, "Invalid JSON body");
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const value = body.value;
|
|
746
|
+
if (!value || typeof value !== "string" || !value.trim()) {
|
|
747
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
748
|
+
return res.end(JSON.stringify({ error: "value is required" }));
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
try {
|
|
752
|
+
const { token, timestamp } = deriveToken(password, machineHash);
|
|
753
|
+
const result = await api.write(password, machineHash, token, timestamp, service, value.trim());
|
|
754
|
+
if (result.error) return strike(res, 502, result.error);
|
|
755
|
+
return ok(res, { ok: true, service });
|
|
756
|
+
} catch (err) {
|
|
757
|
+
return strike(res, 502, err.message);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
142
761
|
// Unknown route
|
|
143
762
|
return strike(res, 404, `Unknown endpoint: ${reqPath}`);
|
|
144
763
|
});
|
|
@@ -172,12 +791,15 @@ async function actionStart(opts) {
|
|
|
172
791
|
|
|
173
792
|
// If we're the daemon child, run the server directly
|
|
174
793
|
if (process.env.__CLAUTH_DAEMON === "1") {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
794
|
+
// Verify password only if one was provided at start (optional — browser can unlock later)
|
|
795
|
+
if (password) {
|
|
796
|
+
try {
|
|
797
|
+
await verifyAuth(password);
|
|
798
|
+
} catch (err) {
|
|
799
|
+
const msg = `[${new Date().toISOString()}] Auth failed: ${err.message}\n`;
|
|
800
|
+
fs.appendFileSync(LOG_FILE, msg);
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
181
803
|
}
|
|
182
804
|
|
|
183
805
|
const server = createServer(password, whitelist, port);
|
|
@@ -199,23 +821,30 @@ async function actionStart(opts) {
|
|
|
199
821
|
return;
|
|
200
822
|
}
|
|
201
823
|
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
824
|
+
// If --pw provided, verify it before spawning (fast-fail on wrong password)
|
|
825
|
+
if (password) {
|
|
826
|
+
console.log(chalk.gray("\n Verifying vault credentials..."));
|
|
827
|
+
try {
|
|
828
|
+
await verifyAuth(password);
|
|
829
|
+
} catch (err) {
|
|
830
|
+
console.log(chalk.red(`\n Auth failed: ${err.message}\n`));
|
|
831
|
+
process.exit(1);
|
|
832
|
+
}
|
|
833
|
+
console.log(chalk.green(" ✓ Vault auth verified"));
|
|
834
|
+
} else {
|
|
835
|
+
console.log(chalk.yellow("\n Starting in locked state — open browser to unlock"));
|
|
209
836
|
}
|
|
210
|
-
console.log(chalk.green(" ✓ Vault auth verified"));
|
|
211
837
|
|
|
212
|
-
// Spawn detached daemon child
|
|
838
|
+
// Spawn detached daemon child via the clauth CLI entry point
|
|
213
839
|
const { spawn } = await import("child_process");
|
|
214
840
|
const { fileURLToPath } = await import("url");
|
|
841
|
+
const { dirname, join } = await import("path");
|
|
215
842
|
const __filename = fileURLToPath(import.meta.url);
|
|
843
|
+
const cliEntry = join(dirname(__filename), "..", "index.js");
|
|
216
844
|
|
|
217
|
-
// Build args: node
|
|
218
|
-
const childArgs = [
|
|
845
|
+
// Build args: node index.js serve start --port N [--pw PW] [--services S]
|
|
846
|
+
const childArgs = [cliEntry, "serve", "start", "--port", String(port)];
|
|
847
|
+
if (password) childArgs.push("--pw", password);
|
|
219
848
|
if (opts.services) childArgs.push("--services", opts.services);
|
|
220
849
|
|
|
221
850
|
const out = fs.openSync(LOG_FILE, "a");
|
|
@@ -226,16 +855,27 @@ async function actionStart(opts) {
|
|
|
226
855
|
});
|
|
227
856
|
child.unref();
|
|
228
857
|
|
|
229
|
-
// Give it
|
|
230
|
-
|
|
858
|
+
// Give it time to bind and write PID file (Windows spawn is slower)
|
|
859
|
+
// Verify via HTTP ping (process.kill(pid,0) fails on Windows for detached processes)
|
|
860
|
+
let started = false;
|
|
861
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
862
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
863
|
+
try {
|
|
864
|
+
const resp = await fetch(`http://127.0.0.1:${port}/ping`);
|
|
865
|
+
if (resp.ok) { started = true; break; }
|
|
866
|
+
} catch {}
|
|
867
|
+
}
|
|
231
868
|
|
|
232
869
|
const info = readPid();
|
|
233
|
-
if (
|
|
870
|
+
if (started && info) {
|
|
234
871
|
console.log(chalk.green(`\n 🔐 clauth serve started`));
|
|
235
872
|
console.log(chalk.gray(` PID: ${info.pid}`));
|
|
236
873
|
console.log(chalk.gray(` Port: 127.0.0.1:${info.port}`));
|
|
237
874
|
console.log(chalk.gray(` Services: ${whitelist ? whitelist.join(", ") : "all"}`));
|
|
238
875
|
console.log(chalk.gray(` Log: ${LOG_FILE}`));
|
|
876
|
+
if (!password) {
|
|
877
|
+
console.log(chalk.cyan(`\n 👉 Open http://127.0.0.1:${info.port} to unlock the vault`));
|
|
878
|
+
}
|
|
239
879
|
console.log(chalk.gray(` Stop: clauth serve stop\n`));
|
|
240
880
|
} else {
|
|
241
881
|
console.log(chalk.red(`\n ❌ Failed to start daemon — check ${LOG_FILE}\n`));
|
|
@@ -318,21 +958,25 @@ async function actionRestart(opts) {
|
|
|
318
958
|
}
|
|
319
959
|
|
|
320
960
|
async function actionForeground(opts) {
|
|
321
|
-
const port
|
|
322
|
-
const password
|
|
961
|
+
const port = parseInt(opts.port || "52437", 10);
|
|
962
|
+
const password = opts.pw || null;
|
|
323
963
|
const whitelist = opts.services
|
|
324
964
|
? opts.services.split(",").map(s => s.trim().toLowerCase())
|
|
325
965
|
: null;
|
|
326
966
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
967
|
+
if (password) {
|
|
968
|
+
console.log(chalk.gray("\n Verifying vault credentials..."));
|
|
969
|
+
try {
|
|
970
|
+
await verifyAuth(password);
|
|
971
|
+
} catch (err) {
|
|
972
|
+
console.log(chalk.red(`\n Auth failed: ${err.message}\n`));
|
|
973
|
+
process.exit(1);
|
|
974
|
+
}
|
|
975
|
+
console.log(chalk.green(" ✓ Vault auth verified"));
|
|
976
|
+
} else {
|
|
977
|
+
console.log(chalk.yellow("\n Starting in locked state — open browser to unlock"));
|
|
333
978
|
}
|
|
334
979
|
|
|
335
|
-
console.log(chalk.green(" ✓ Vault auth verified"));
|
|
336
980
|
console.log(chalk.gray(` Port: 127.0.0.1:${port}`));
|
|
337
981
|
console.log(chalk.gray(` Services: ${whitelist ? whitelist.join(", ") : "all"}`));
|
|
338
982
|
console.log(chalk.gray(` Lockout: 3 failures → exit\n`));
|
|
@@ -341,6 +985,7 @@ async function actionForeground(opts) {
|
|
|
341
985
|
server.listen(port, "127.0.0.1", () => {
|
|
342
986
|
writePid(process.pid, port);
|
|
343
987
|
console.log(chalk.green(` clauth serve → http://127.0.0.1:${port}`));
|
|
988
|
+
if (!password) console.log(chalk.cyan(` 👉 Open http://127.0.0.1:${port} to unlock`));
|
|
344
989
|
console.log(chalk.gray(" Ctrl+C to stop\n"));
|
|
345
990
|
});
|
|
346
991
|
|