feat(chat): auth, first-run email, Web Push notifications#67
Conversation
No-Slack-needed story: users without Slack can receive a login email on first boot via Resend (or a bootstrap token on stdout) and access the chat directly. Push notifications: users get browser notifications when long tasks complete, the agent messages in an unfocused session, scheduled jobs finish, or hard errors occur. New server modules: - email-login: rate-limited email login with Resend, neutral responses - first-run: boot-time detection, idempotent email/stdout trigger - notifications/vapid: ECDSA P-256 key pair via Web Crypto, persisted - notifications/subscriptions: push subscription CRUD with 10-error disable - notifications/sender: @block65/webcrypto-web-push wrapper - notifications/triggers: 4 triggers with focus gate, 30s threshold, 5s debounce - notifications/focus: server-side session focus map - login-redirect: same-origin redirect validation New client modules: - Service Worker with /chat/ scope, cache-first statics, push events - use-notifications hook for SW registration and permission management - notification-banner: soft permission prompt after first message - ios-install-banner: PWA install guidance for iOS Safari Migrations 40-43: first_run_state, chat_push_subscriptions tables. 45 new tests, 1,530 total, zero regressions.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 07c8eeee7b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (state?.email_sent_at || state?.stdout_printed_at) { | ||
| return; // Already handled in a previous boot |
There was a problem hiding this comment.
Reissue first-run token when prior bootstrap token may be stale
This early return skips all first-run handling once stdout_printed_at is set, even though the bootstrap token created in printBootstrapBanner is backed by in-memory session state. If the process restarts before the owner uses that token, handleLoginPost can no longer validate it (it only checks in-memory tokens), and no new token is printed, which can lock the owner out of /ui/login.
Useful? React with 👍 / 👎.
| } catch (err: unknown) { | ||
| const msg = err instanceof Error ? err.message : String(err); | ||
| console.error(`[email-login] Failed to send: ${msg}`); |
There was a problem hiding this comment.
Propagate email delivery failures to first-run fallback
Catching and swallowing all send errors here prevents handleFirstRun from detecting delivery failure, so it still records email_sent_at as successful and never falls back to printing a bootstrap token. In environments with RESEND_API_KEY set but misconfigured or temporarily failing, first-run auth bootstrap can silently fail without a usable recovery path.
Useful? React with 👍 / 👎.
| notificationTriggers = new NotificationTriggerService({ | ||
| db, | ||
| vapidKeys, | ||
| focusMap, | ||
| ownerEmail: process.env.OWNER_EMAIL, |
There was a problem hiding this comment.
Wire notification triggers into runtime event paths
The trigger service is initialized and injected, but no production path invokes onSessionDone, onAgentMessage, onScheduledJobResult, or onHardError (the only send path is manual /chat/push/test). That means push notifications never fire for normal chat/scheduler activity, so the feature is effectively non-functional despite successful initialization.
Useful? React with 👍 / 👎.
The builder added the package to package.json but did not commit the updated bun.lock. CI's --frozen-lockfile fails without it.
P1 fixes: - Bootstrap token survives process restarts: checkBootstrapMagicHash wired into login POST as third fallback after consumeMagicLink and isValidSession - sendLoginEmail re-throws on Resend failure so handleFirstRun detects it and falls through to stdout bootstrap banner - Notification triggers wired end to end: writer fires onSessionDone and onHardError, scheduler fires onScheduledJobResult via new onJobComplete callback - Dead code removed: login-redirect.ts (never imported) - Rate limit map bounded: evicts expired entries when size exceeds 1000 - Focus heartbeat: new useFocusHeartbeat hook sends POST /chat/focus every 10s while visible, fires unfocused on visibilitychange P2 fixes: - Push endpoint validated as https: (SSRF prevention) - Payload byte measurement uses TextEncoder, truncates at 3KB - Service Worker returns for API paths instead of respondWith(fetch) - First-run error messages clarify web UI vs MPC/CLI scope - Session title looked up from store for notification trigger
The tampered-ciphertext and tampered-auth-tag tests prepended "X" to the encrypted/authTag base64 string and sliced off the first char. When the original string already started with "X" (~1.5% probability per run), the tampered result equaled the original, decryption succeeded, and the toThrow() assertion failed. Replaced with flipFirstBase64Char() that guarantees a different first character. Verified 10 consecutive passes locally. This has been blocking CI on PRs #67, #69, and #70.
Summary
Completes the "no Slack needed" story and adds push notifications.
Email first-run
Web Push notifications
Schema
Test plan
bun test: 1,530 tests, 0 failures (45 new)bun run typecheckandbun run lint: cleanchat-uitypecheck and build: clean, 211KB gzipped