fluxy-bot 0.7.4 → 0.7.6
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-fluxy/assets/{fluxy-C2_hZ0e0.js → fluxy-Bcd5tJrt.js} +25 -25
- package/dist-fluxy/assets/{globals-DZhBbtf8.css → globals-Bs_wR6rP.css} +1 -1
- package/dist-fluxy/assets/globals-CMrTFJSE.js +18 -0
- package/dist-fluxy/assets/{onboard-LzOfScdU.js → onboard-BSlNrxVH.js} +1 -1
- package/dist-fluxy/fluxy.html +3 -3
- package/dist-fluxy/onboard.html +3 -3
- package/package.json +1 -1
- package/supervisor/chat/OnboardWizard.tsx +74 -10
- package/supervisor/chat/src/components/LoginScreen.tsx +44 -1
- package/dist-fluxy/assets/globals-C0DMRzvx.js +0 -18
|
@@ -1 +1 @@
|
|
|
1
|
-
import{e as o,j as e,R as n,O as r}from"./globals-
|
|
1
|
+
import{e as o,j as e,R as n,O as r}from"./globals-CMrTFJSE.js";function a(){const t=()=>{window.parent?.postMessage({type:"fluxy:onboard-complete"},"*")};return e.jsx(r,{onComplete:t,isInitialSetup:!0})}o.createRoot(document.getElementById("root")).render(e.jsx(n.StrictMode,{children:e.jsx(a,{})}));
|
package/dist-fluxy/fluxy.html
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content" />
|
|
6
6
|
<title>Fluxy Chat</title>
|
|
7
|
-
<script type="module" crossorigin src="/fluxy/assets/fluxy-
|
|
8
|
-
<link rel="modulepreload" crossorigin href="/fluxy/assets/globals-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/fluxy/assets/globals-
|
|
7
|
+
<script type="module" crossorigin src="/fluxy/assets/fluxy-Bcd5tJrt.js"></script>
|
|
8
|
+
<link rel="modulepreload" crossorigin href="/fluxy/assets/globals-CMrTFJSE.js">
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/fluxy/assets/globals-Bs_wR6rP.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body class="bg-background text-foreground">
|
|
12
12
|
<div id="root"></div>
|
package/dist-fluxy/onboard.html
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content" />
|
|
6
6
|
<title>Fluxy Setup</title>
|
|
7
|
-
<script type="module" crossorigin src="/fluxy/assets/onboard-
|
|
8
|
-
<link rel="modulepreload" crossorigin href="/fluxy/assets/globals-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/fluxy/assets/globals-
|
|
7
|
+
<script type="module" crossorigin src="/fluxy/assets/onboard-BSlNrxVH.js"></script>
|
|
8
|
+
<link rel="modulepreload" crossorigin href="/fluxy/assets/globals-CMrTFJSE.js">
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/fluxy/assets/globals-Bs_wR6rP.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body class="bg-background text-foreground">
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -174,11 +174,50 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
174
174
|
// Sub-phase within step 3: 'password' | 'totp-setup' | 'recovery'
|
|
175
175
|
const [step3Phase, setStep3Phase] = useState<'password' | 'totp-setup' | 'recovery'>('password');
|
|
176
176
|
|
|
177
|
+
// Clipboard feedback for TOTP secret copy
|
|
178
|
+
const [totpSecretCopied, setTotpSecretCopied] = useState(false);
|
|
179
|
+
|
|
177
180
|
// Pre-fill guard
|
|
178
181
|
const prefillDone = useRef(false);
|
|
179
182
|
|
|
180
183
|
const isConnected = authState[provider] === 'connected';
|
|
181
184
|
|
|
185
|
+
// Persist/restore TOTP setup state across page reloads (mobile: OS suspends PWA on app-switch)
|
|
186
|
+
const TOTP_STORAGE_KEY = 'fluxy_totp_setup';
|
|
187
|
+
|
|
188
|
+
function saveTotpState() {
|
|
189
|
+
try {
|
|
190
|
+
sessionStorage.setItem(TOTP_STORAGE_KEY, JSON.stringify({
|
|
191
|
+
secret: totpSecret, qrUri: totpQrUri, otpauthUri: totpOtpauthUri, phase: step3Phase,
|
|
192
|
+
// Also persist password fields so user doesn't re-type after returning
|
|
193
|
+
portalPass, portalPassConfirm,
|
|
194
|
+
}));
|
|
195
|
+
} catch {}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function restoreTotpState() {
|
|
199
|
+
try {
|
|
200
|
+
const raw = sessionStorage.getItem(TOTP_STORAGE_KEY);
|
|
201
|
+
if (!raw) return false;
|
|
202
|
+
const saved = JSON.parse(raw);
|
|
203
|
+
if (saved.secret && saved.phase === 'totp-setup') {
|
|
204
|
+
setTotpSecret(saved.secret);
|
|
205
|
+
setTotpQrUri(saved.qrUri || '');
|
|
206
|
+
setTotpOtpauthUri(saved.otpauthUri || '');
|
|
207
|
+
setTotpEnabled(true);
|
|
208
|
+
setStep3Phase('totp-setup');
|
|
209
|
+
setStep(3);
|
|
210
|
+
if (saved.portalPass) { setPortalPass(saved.portalPass); setPortalPassConfirm(saved.portalPassConfirm || ''); }
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
} catch {}
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function clearTotpStorage() {
|
|
218
|
+
try { sessionStorage.removeItem(TOTP_STORAGE_KEY); } catch {}
|
|
219
|
+
}
|
|
220
|
+
|
|
182
221
|
// Pre-fill from existing settings (re-run wizard)
|
|
183
222
|
useEffect(() => {
|
|
184
223
|
fetch('/api/onboard/status')
|
|
@@ -204,6 +243,9 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
204
243
|
// If user has existing handle, default to 'relay'; otherwise default to 'tunnel'
|
|
205
244
|
if (!data.handle) setHandleChoice('tunnel');
|
|
206
245
|
prefillDone.current = true;
|
|
246
|
+
|
|
247
|
+
// Restore TOTP setup state if returning from authenticator app
|
|
248
|
+
if (!data.totpEnabled) restoreTotpState();
|
|
207
249
|
})
|
|
208
250
|
.catch(() => { prefillDone.current = true; });
|
|
209
251
|
}, []);
|
|
@@ -1258,6 +1300,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
1258
1300
|
setTotpCode('');
|
|
1259
1301
|
setTotpError('');
|
|
1260
1302
|
setStep3Phase('password');
|
|
1303
|
+
clearTotpStorage();
|
|
1261
1304
|
}}
|
|
1262
1305
|
className="w-7 h-7 rounded-full bg-white/[0.04] flex items-center justify-center text-white/40 hover:text-white/70 transition-colors shrink-0"
|
|
1263
1306
|
>
|
|
@@ -1280,20 +1323,39 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
1280
1323
|
<p className="text-white/40 text-[13px] leading-relaxed mb-4">
|
|
1281
1324
|
Add Fluxy to your authenticator app, then enter the 6-digit code below to confirm.
|
|
1282
1325
|
</p>
|
|
1326
|
+
|
|
1327
|
+
{/* Primary: copy secret key (doesn't leave the app) */}
|
|
1328
|
+
<button
|
|
1329
|
+
onClick={() => {
|
|
1330
|
+
navigator.clipboard.writeText(totpSecret);
|
|
1331
|
+
setTotpSecretCopied(true);
|
|
1332
|
+
setTimeout(() => setTotpSecretCopied(false), 3000);
|
|
1333
|
+
}}
|
|
1334
|
+
className={`w-full py-3 text-[14px] font-medium rounded-xl transition-colors flex items-center justify-center gap-2 ${
|
|
1335
|
+
totpSecretCopied
|
|
1336
|
+
? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'
|
|
1337
|
+
: 'bg-white/[0.05] text-white/70 hover:bg-white/[0.08] border border-white/[0.08]'
|
|
1338
|
+
}`}
|
|
1339
|
+
>
|
|
1340
|
+
{totpSecretCopied ? (
|
|
1341
|
+
<><Check className="h-4 w-4" />Secret key copied</>
|
|
1342
|
+
) : (
|
|
1343
|
+
<><Copy className="h-4 w-4" />Copy secret key</>
|
|
1344
|
+
)}
|
|
1345
|
+
</button>
|
|
1346
|
+
<p className="text-[11px] text-white/20 text-center mt-2">
|
|
1347
|
+
Paste it in your authenticator app → Add account → Enter key
|
|
1348
|
+
</p>
|
|
1349
|
+
|
|
1350
|
+
{/* Secondary: deep link (saves state before leaving) */}
|
|
1283
1351
|
<a
|
|
1284
1352
|
href={totpOtpauthUri}
|
|
1285
|
-
|
|
1353
|
+
onClick={() => saveTotpState()}
|
|
1354
|
+
className="w-full mt-3 py-2.5 text-[13px] text-white/30 hover:text-white/50 flex items-center justify-center gap-1.5 transition-colors"
|
|
1286
1355
|
>
|
|
1287
|
-
<Smartphone className="h-
|
|
1288
|
-
|
|
1356
|
+
<Smartphone className="h-3.5 w-3.5" />
|
|
1357
|
+
Or open directly in authenticator
|
|
1289
1358
|
</a>
|
|
1290
|
-
<button
|
|
1291
|
-
onClick={() => { navigator.clipboard.writeText(totpSecret); }}
|
|
1292
|
-
className="w-full mt-2 text-[11px] text-white/25 hover:text-white/40 flex items-center justify-center gap-1 py-1 transition-colors"
|
|
1293
|
-
>
|
|
1294
|
-
<Copy className="h-3 w-3" />
|
|
1295
|
-
Or copy secret key manually
|
|
1296
|
-
</button>
|
|
1297
1359
|
</div>
|
|
1298
1360
|
) : (
|
|
1299
1361
|
<div className="mt-4 flex gap-4 items-start">
|
|
@@ -1353,6 +1415,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
1353
1415
|
setTotpVerified(true);
|
|
1354
1416
|
setRecoveryCodes(data.recoveryCodes || []);
|
|
1355
1417
|
setStep3Phase('recovery');
|
|
1418
|
+
clearTotpStorage();
|
|
1356
1419
|
} else {
|
|
1357
1420
|
setTotpError(data.error || 'Verification failed');
|
|
1358
1421
|
}
|
|
@@ -1427,6 +1490,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
1427
1490
|
onClick={() => {
|
|
1428
1491
|
setRecoveryCodes([]);
|
|
1429
1492
|
setStep3Phase('password');
|
|
1493
|
+
clearTotpStorage();
|
|
1430
1494
|
}}
|
|
1431
1495
|
disabled={!recoveryCodesCopied}
|
|
1432
1496
|
className="w-full mt-3 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
|
|
@@ -2,6 +2,8 @@ import { useState, useRef, useEffect, type KeyboardEvent } from 'react';
|
|
|
2
2
|
import { Lock, LoaderCircle, ArrowRight, ArrowLeft, Shield, Check } from 'lucide-react';
|
|
3
3
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
4
4
|
|
|
5
|
+
const LOGIN_STORAGE_KEY = 'fluxy_login_totp';
|
|
6
|
+
|
|
5
7
|
interface Props {
|
|
6
8
|
onLogin: (token: string) => void;
|
|
7
9
|
totpEnabled?: boolean;
|
|
@@ -20,6 +22,21 @@ export default function LoginScreen({ onLogin, totpEnabled }: Props) {
|
|
|
20
22
|
const [useRecovery, setUseRecovery] = useState(false);
|
|
21
23
|
const totpInputRef = useRef<HTMLInputElement>(null);
|
|
22
24
|
|
|
25
|
+
// Restore TOTP phase after page reload (mobile: OS suspends PWA on app-switch)
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
try {
|
|
28
|
+
const raw = sessionStorage.getItem(LOGIN_STORAGE_KEY);
|
|
29
|
+
if (!raw) return;
|
|
30
|
+
const saved = JSON.parse(raw);
|
|
31
|
+
if (saved.pendingToken && saved.expiresAt > Date.now()) {
|
|
32
|
+
setPendingToken(saved.pendingToken);
|
|
33
|
+
setPhase('totp');
|
|
34
|
+
} else {
|
|
35
|
+
sessionStorage.removeItem(LOGIN_STORAGE_KEY);
|
|
36
|
+
}
|
|
37
|
+
} catch {}
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
23
40
|
// Auto-focus TOTP input when switching to TOTP phase
|
|
24
41
|
useEffect(() => {
|
|
25
42
|
if (phase === 'totp') {
|
|
@@ -27,6 +44,20 @@ export default function LoginScreen({ onLogin, totpEnabled }: Props) {
|
|
|
27
44
|
}
|
|
28
45
|
}, [phase]);
|
|
29
46
|
|
|
47
|
+
function saveLoginState(token: string) {
|
|
48
|
+
try {
|
|
49
|
+
// Pending token has 5min server expiry — save with matching client expiry
|
|
50
|
+
sessionStorage.setItem(LOGIN_STORAGE_KEY, JSON.stringify({
|
|
51
|
+
pendingToken: token,
|
|
52
|
+
expiresAt: Date.now() + 4.5 * 60 * 1000,
|
|
53
|
+
}));
|
|
54
|
+
} catch {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function clearLoginState() {
|
|
58
|
+
try { sessionStorage.removeItem(LOGIN_STORAGE_KEY); } catch {}
|
|
59
|
+
}
|
|
60
|
+
|
|
30
61
|
const handleSubmit = async () => {
|
|
31
62
|
if (!password.trim() || loading) return;
|
|
32
63
|
setLoading(true);
|
|
@@ -41,9 +72,11 @@ export default function LoginScreen({ onLogin, totpEnabled }: Props) {
|
|
|
41
72
|
const data = await res.json();
|
|
42
73
|
|
|
43
74
|
if (res.ok && data.token) {
|
|
75
|
+
clearLoginState();
|
|
44
76
|
onLogin(data.token);
|
|
45
77
|
} else if (res.ok && data.requiresTOTP) {
|
|
46
78
|
setPendingToken(data.pendingToken);
|
|
79
|
+
saveLoginState(data.pendingToken);
|
|
47
80
|
setPhase('totp');
|
|
48
81
|
setError('');
|
|
49
82
|
} else {
|
|
@@ -69,6 +102,7 @@ export default function LoginScreen({ onLogin, totpEnabled }: Props) {
|
|
|
69
102
|
const data = await res.json();
|
|
70
103
|
|
|
71
104
|
if (res.ok && data.token) {
|
|
105
|
+
clearLoginState();
|
|
72
106
|
onLogin(data.token);
|
|
73
107
|
} else {
|
|
74
108
|
setError(data.error || 'Invalid code');
|
|
@@ -87,6 +121,14 @@ export default function LoginScreen({ onLogin, totpEnabled }: Props) {
|
|
|
87
121
|
}
|
|
88
122
|
};
|
|
89
123
|
|
|
124
|
+
const handleBackToPassword = () => {
|
|
125
|
+
setPhase('password');
|
|
126
|
+
setError('');
|
|
127
|
+
setTotpCode('');
|
|
128
|
+
setUseRecovery(false);
|
|
129
|
+
clearLoginState();
|
|
130
|
+
};
|
|
131
|
+
|
|
90
132
|
const inputCls = 'w-full bg-white/[0.05] border border-white/[0.08] text-white rounded-xl px-4 py-3 text-base outline-none input-glow placeholder:text-white/20 transition-all';
|
|
91
133
|
|
|
92
134
|
return (
|
|
@@ -183,6 +225,7 @@ export default function LoginScreen({ onLogin, totpEnabled }: Props) {
|
|
|
183
225
|
}}
|
|
184
226
|
onKeyDown={handleKeyDown}
|
|
185
227
|
placeholder={useRecovery ? 'Recovery code' : '000000'}
|
|
228
|
+
autoFocus
|
|
186
229
|
className={inputCls + (useRecovery ? '' : ' tracking-[0.3em] text-center font-mono')}
|
|
187
230
|
/>
|
|
188
231
|
|
|
@@ -215,7 +258,7 @@ export default function LoginScreen({ onLogin, totpEnabled }: Props) {
|
|
|
215
258
|
|
|
216
259
|
<div className="flex items-center gap-4 mt-4">
|
|
217
260
|
<button
|
|
218
|
-
onClick={
|
|
261
|
+
onClick={handleBackToPassword}
|
|
219
262
|
className="text-[12px] text-white/30 hover:text-white/50 flex items-center gap-1 transition-colors"
|
|
220
263
|
>
|
|
221
264
|
<ArrowLeft className="h-3 w-3" />
|