botschat 0.1.4 → 0.1.7

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.
Files changed (66) hide show
  1. package/README.md +64 -24
  2. package/migrations/0011_e2e_encryption.sql +35 -0
  3. package/package.json +7 -2
  4. package/packages/api/package.json +2 -1
  5. package/packages/api/src/do/connection-do.ts +162 -42
  6. package/packages/api/src/index.ts +132 -13
  7. package/packages/api/src/routes/auth.ts +127 -30
  8. package/packages/api/src/routes/pairing.ts +14 -1
  9. package/packages/api/src/routes/setup.ts +72 -24
  10. package/packages/api/src/routes/upload.ts +12 -8
  11. package/packages/api/src/utils/auth.ts +212 -43
  12. package/packages/api/src/utils/id.ts +30 -14
  13. package/packages/api/src/utils/rate-limit.ts +73 -0
  14. package/packages/plugin/dist/src/accounts.d.ts.map +1 -1
  15. package/packages/plugin/dist/src/accounts.js +1 -0
  16. package/packages/plugin/dist/src/accounts.js.map +1 -1
  17. package/packages/plugin/dist/src/channel.d.ts +1 -0
  18. package/packages/plugin/dist/src/channel.d.ts.map +1 -1
  19. package/packages/plugin/dist/src/channel.js +151 -9
  20. package/packages/plugin/dist/src/channel.js.map +1 -1
  21. package/packages/plugin/dist/src/types.d.ts +16 -0
  22. package/packages/plugin/dist/src/types.d.ts.map +1 -1
  23. package/packages/plugin/dist/src/ws-client.d.ts +2 -0
  24. package/packages/plugin/dist/src/ws-client.d.ts.map +1 -1
  25. package/packages/plugin/dist/src/ws-client.js +14 -3
  26. package/packages/plugin/dist/src/ws-client.js.map +1 -1
  27. package/packages/plugin/package.json +4 -3
  28. package/packages/web/dist/architecture.png +0 -0
  29. package/packages/web/dist/assets/index-BoNQoJjQ.js +1497 -0
  30. package/packages/web/dist/assets/{index-DuGeoFJT.css → index-ewBIratI.css} +1 -1
  31. package/packages/web/dist/botschat-icon.svg +4 -0
  32. package/packages/web/dist/index.html +23 -3
  33. package/packages/web/dist/manifest.json +24 -0
  34. package/packages/web/dist/sw.js +40 -0
  35. package/packages/web/index.html +21 -1
  36. package/packages/web/package.json +1 -0
  37. package/packages/web/src/App.tsx +286 -103
  38. package/packages/web/src/analytics.ts +57 -0
  39. package/packages/web/src/api.ts +67 -3
  40. package/packages/web/src/components/ChatWindow.tsx +11 -11
  41. package/packages/web/src/components/ConnectionSettings.tsx +477 -0
  42. package/packages/web/src/components/CronDetail.tsx +475 -235
  43. package/packages/web/src/components/CronSidebar.tsx +1 -1
  44. package/packages/web/src/components/DebugLogPanel.tsx +116 -3
  45. package/packages/web/src/components/E2ESettings.tsx +122 -0
  46. package/packages/web/src/components/IconRail.tsx +56 -27
  47. package/packages/web/src/components/JobList.tsx +2 -6
  48. package/packages/web/src/components/LoginPage.tsx +143 -104
  49. package/packages/web/src/components/MobileLayout.tsx +480 -0
  50. package/packages/web/src/components/OnboardingPage.tsx +159 -21
  51. package/packages/web/src/components/ResizeHandle.tsx +34 -0
  52. package/packages/web/src/components/Sidebar.tsx +1 -1
  53. package/packages/web/src/components/TaskBar.tsx +2 -2
  54. package/packages/web/src/components/ThreadPanel.tsx +2 -5
  55. package/packages/web/src/e2e.ts +133 -0
  56. package/packages/web/src/hooks/useIsMobile.ts +27 -0
  57. package/packages/web/src/index.css +59 -0
  58. package/packages/web/src/main.tsx +12 -0
  59. package/packages/web/src/store.ts +16 -8
  60. package/packages/web/src/ws.ts +78 -4
  61. package/scripts/dev.sh +16 -16
  62. package/scripts/test-e2e-live.ts +194 -0
  63. package/scripts/verify-e2e-db.ts +48 -0
  64. package/scripts/verify-e2e.ts +56 -0
  65. package/wrangler.toml +3 -1
  66. package/packages/web/dist/assets/index-DyzTR_Y4.js +0 -847
@@ -1,5 +1,6 @@
1
- import React, { useState } from "react";
2
- import { authApi, setToken } from "../api";
1
+ import React, { useState, useEffect } from "react";
2
+ import { authApi, setToken, setRefreshToken } from "../api";
3
+ import type { AuthConfig } from "../api";
3
4
  import { useAppDispatch } from "../store";
4
5
  import { dlog } from "../debug-log";
5
6
  import { isFirebaseConfigured, signInWithGoogle, signInWithGitHub } from "../firebase";
@@ -34,12 +35,26 @@ export function LoginPage() {
34
35
  const [error, setError] = useState("");
35
36
  const [loading, setLoading] = useState(false);
36
37
  const [oauthLoading, setOauthLoading] = useState<"google" | "github" | null>(null);
38
+ const [authConfig, setAuthConfig] = useState<AuthConfig | null>(null);
37
39
 
38
40
  const firebaseEnabled = isFirebaseConfigured();
39
41
  const anyLoading = loading || !!oauthLoading;
40
42
 
41
- const handleAuthSuccess = (res: { id: string; email: string; displayName?: string; token: string }) => {
43
+ // Fetch server-side auth config to determine which methods are available
44
+ useEffect(() => {
45
+ authApi.config().then(setAuthConfig).catch(() => {
46
+ // Fallback: assume email enabled (local dev) if config endpoint fails
47
+ setAuthConfig({ emailEnabled: true, googleEnabled: firebaseEnabled, githubEnabled: firebaseEnabled });
48
+ });
49
+ }, [firebaseEnabled]);
50
+
51
+ const emailEnabled = authConfig?.emailEnabled ?? true;
52
+ const configLoaded = authConfig !== null;
53
+ const hasAnyLoginMethod = configLoaded && (firebaseEnabled || emailEnabled);
54
+
55
+ const handleAuthSuccess = (res: { id: string; email: string; displayName?: string; token: string; refreshToken?: string }) => {
42
56
  setToken(res.token);
57
+ if (res.refreshToken) setRefreshToken(res.refreshToken);
43
58
  dispatch({
44
59
  type: "SET_USER",
45
60
  user: { id: res.id, email: res.email, displayName: res.displayName },
@@ -130,11 +145,27 @@ export function LoginPage() {
130
145
  }}
131
146
  >
132
147
  <h2 className="text-h1 mb-6" style={{ color: "var(--text-primary)" }}>
133
- {isRegister ? "Create account" : "Sign in"}
148
+ {emailEnabled
149
+ ? (isRegister ? "Create account" : "Sign in")
150
+ : "Sign in"}
134
151
  </h2>
135
152
 
153
+ {/* Loading: avoid showing empty card on first paint before config is loaded */}
154
+ {!configLoaded && (
155
+ <div className="py-8 text-center" style={{ color: "var(--text-muted)" }}>
156
+ <span className="text-body">Loading sign-in options…</span>
157
+ </div>
158
+ )}
159
+
160
+ {/* No methods available (e.g. misconfiguration) */}
161
+ {configLoaded && !hasAnyLoginMethod && (
162
+ <div className="py-4 text-caption" style={{ color: "var(--text-secondary)" }}>
163
+ Sign-in is not configured. Please contact support.
164
+ </div>
165
+ )}
166
+
136
167
  {/* OAuth buttons */}
137
- {firebaseEnabled && (
168
+ {configLoaded && firebaseEnabled && (
138
169
  <>
139
170
  <div className="space-y-3">
140
171
  {/* Google */}
@@ -182,113 +213,121 @@ export function LoginPage() {
182
213
  </button>
183
214
  </div>
184
215
 
185
- {/* Divider */}
186
- <div className="flex items-center gap-3 my-5">
187
- <div className="flex-1 h-px" style={{ background: "var(--border)" }} />
188
- <span className="text-caption" style={{ color: "var(--text-muted)" }}>
189
- or
190
- </span>
191
- <div className="flex-1 h-px" style={{ background: "var(--border)" }} />
192
- </div>
216
+ {/* Divider — only show if email login is also available */}
217
+ {configLoaded && emailEnabled && (
218
+ <div className="flex items-center gap-3 my-5">
219
+ <div className="flex-1 h-px" style={{ background: "var(--border)" }} />
220
+ <span className="text-caption" style={{ color: "var(--text-muted)" }}>
221
+ or
222
+ </span>
223
+ <div className="flex-1 h-px" style={{ background: "var(--border)" }} />
224
+ </div>
225
+ )}
193
226
  </>
194
227
  )}
195
228
 
196
- <form onSubmit={handleSubmit} className="space-y-4">
197
- {isRegister && (
198
- <div>
199
- <label className="block text-caption font-bold mb-1" style={{ color: "var(--text-secondary)" }}>
200
- Display Name
201
- </label>
202
- <input
203
- type="text"
204
- value={displayName}
205
- onChange={(e) => setDisplayName(e.target.value)}
206
- className="w-full px-3 py-2.5 text-body rounded-sm focus:outline-none placeholder:text-[--text-muted]"
207
- style={{
208
- background: "var(--bg-surface)",
209
- color: "var(--text-primary)",
210
- border: "1px solid var(--border)",
211
- }}
212
- placeholder="Your name"
213
- />
214
- </div>
215
- )}
216
-
217
- <div>
218
- <label className="block text-caption font-bold mb-1" style={{ color: "var(--text-secondary)" }}>
219
- Email
220
- </label>
221
- <input
222
- type="email"
223
- value={email}
224
- onChange={(e) => setEmail(e.target.value)}
225
- required
226
- className="w-full px-3 py-2.5 text-body rounded-sm focus:outline-none placeholder:text-[--text-muted]"
227
- style={{
228
- background: "var(--bg-surface)",
229
- color: "var(--text-primary)",
230
- border: "1px solid var(--border)",
231
- }}
232
- placeholder="you@example.com"
233
- />
229
+ {/* Error display (always visible, e.g. OAuth errors) */}
230
+ {error && (
231
+ <div
232
+ className="text-caption px-3 py-2 rounded-sm mt-4"
233
+ style={{ background: "rgba(224,30,90,0.1)", color: "var(--accent-red)" }}
234
+ >
235
+ {error}
234
236
  </div>
237
+ )}
235
238
 
236
- <div>
237
- <label className="block text-caption font-bold mb-1" style={{ color: "var(--text-secondary)" }}>
238
- Password
239
- </label>
240
- <input
241
- type="password"
242
- value={password}
243
- onChange={(e) => setPassword(e.target.value)}
244
- required
245
- className="w-full px-3 py-2.5 text-body rounded-sm focus:outline-none placeholder:text-[--text-muted]"
246
- style={{
247
- background: "var(--bg-surface)",
248
- color: "var(--text-primary)",
249
- border: "1px solid var(--border)",
250
- }}
251
- placeholder="Enter password"
252
- />
253
- </div>
239
+ {/* Email/password form — only in local/dev mode */}
240
+ {configLoaded && emailEnabled && (
241
+ <>
242
+ <form onSubmit={handleSubmit} className="space-y-4">
243
+ {isRegister && (
244
+ <div>
245
+ <label className="block text-caption font-bold mb-1" style={{ color: "var(--text-secondary)" }}>
246
+ Display Name
247
+ </label>
248
+ <input
249
+ type="text"
250
+ value={displayName}
251
+ onChange={(e) => setDisplayName(e.target.value)}
252
+ className="w-full px-3 py-2.5 text-body rounded-sm focus:outline-none placeholder:text-[--text-muted]"
253
+ style={{
254
+ background: "var(--bg-surface)",
255
+ color: "var(--text-primary)",
256
+ border: "1px solid var(--border)",
257
+ }}
258
+ placeholder="Your name"
259
+ />
260
+ </div>
261
+ )}
254
262
 
255
- {error && (
256
- <div
257
- className="text-caption px-3 py-2 rounded-sm"
258
- style={{ background: "rgba(224,30,90,0.1)", color: "var(--accent-red)" }}
259
- >
260
- {error}
261
- </div>
262
- )}
263
+ <div>
264
+ <label className="block text-caption font-bold mb-1" style={{ color: "var(--text-secondary)" }}>
265
+ Email
266
+ </label>
267
+ <input
268
+ type="email"
269
+ value={email}
270
+ onChange={(e) => setEmail(e.target.value)}
271
+ required
272
+ className="w-full px-3 py-2.5 text-body rounded-sm focus:outline-none placeholder:text-[--text-muted]"
273
+ style={{
274
+ background: "var(--bg-surface)",
275
+ color: "var(--text-primary)",
276
+ border: "1px solid var(--border)",
277
+ }}
278
+ placeholder="you@example.com"
279
+ />
280
+ </div>
263
281
 
264
- <button
265
- type="submit"
266
- disabled={anyLoading}
267
- className="w-full py-2.5 font-bold text-body text-white rounded-sm disabled:opacity-50 transition-colors hover:brightness-110"
268
- style={{ background: "var(--bg-active)" }}
269
- >
270
- {loading
271
- ? "..."
272
- : isRegister
273
- ? "Create account"
274
- : "Sign in with email"}
275
- </button>
276
- </form>
282
+ <div>
283
+ <label className="block text-caption font-bold mb-1" style={{ color: "var(--text-secondary)" }}>
284
+ Password
285
+ </label>
286
+ <input
287
+ type="password"
288
+ value={password}
289
+ onChange={(e) => setPassword(e.target.value)}
290
+ required
291
+ className="w-full px-3 py-2.5 text-body rounded-sm focus:outline-none placeholder:text-[--text-muted]"
292
+ style={{
293
+ background: "var(--bg-surface)",
294
+ color: "var(--text-primary)",
295
+ border: "1px solid var(--border)",
296
+ }}
297
+ placeholder="Enter password"
298
+ />
299
+ </div>
277
300
 
278
- <div className="mt-6 text-center">
279
- <button
280
- onClick={() => {
281
- setIsRegister(!isRegister);
282
- setError("");
283
- }}
284
- className="text-caption hover:underline"
285
- style={{ color: "var(--text-link)" }}
286
- >
287
- {isRegister
288
- ? "Already have an account? Sign in"
289
- : "Don't have an account? Register"}
290
- </button>
291
- </div>
301
+ <button
302
+ type="submit"
303
+ disabled={anyLoading}
304
+ className="w-full py-2.5 font-bold text-body text-white rounded-sm disabled:opacity-50 transition-colors hover:brightness-110"
305
+ style={{ background: "var(--bg-active)" }}
306
+ >
307
+ {loading
308
+ ? "..."
309
+ : isRegister
310
+ ? "Create account"
311
+ : "Sign in with email"}
312
+ </button>
313
+ </form>
314
+
315
+ <div className="mt-6 text-center">
316
+ <button
317
+ onClick={() => {
318
+ setIsRegister(!isRegister);
319
+ setError("");
320
+ }}
321
+ className="text-caption hover:underline"
322
+ style={{ color: "var(--text-link)" }}
323
+ >
324
+ {isRegister
325
+ ? "Already have an account? Sign in"
326
+ : "Don't have an account? Register"}
327
+ </button>
328
+ </div>
329
+ </>
330
+ )}
292
331
  </div>
293
332
  </div>
294
333
  </div>