Skip to content

feat(chat): auth, first-run email, Web Push notifications#67

Merged
mcheemaa merged 3 commits into
mainfrom
project-4-chat-auth-push
Apr 15, 2026
Merged

feat(chat): auth, first-run email, Web Push notifications#67
mcheemaa merged 3 commits into
mainfrom
project-4-chat-auth-push

Conversation

@mcheemaa
Copy link
Copy Markdown
Member

Summary

Completes the "no Slack needed" story and adds push notifications.

Email first-run

  • First boot without Slack: sends login email via Resend to OWNER_EMAIL
  • Fallback: prints bootstrap magic token to stdout if no Resend
  • Idempotent (persisted in first_run_state table, survives restarts)
  • Email login form added to /ui/login page with rate limiting (1/60s per IP)
  • ?redirect=/chat support on login page for direct chat entry

Web Push notifications

  • VAPID key pair generated via Web Crypto, persisted in encrypted secrets table
  • @block65/webcrypto-web-push (not canonical web-push, Bun bug)
  • Four triggers: session complete (30s+ unfocused), agent message, scheduled job, hard error
  • Focus gate via server-side heartbeat map, 5-second per-tag debounce
  • Service Worker at /chat/sw.js: cache-first statics, network-only API, push display
  • Soft permission banner after first message (not page load)
  • iOS PWA install banner for Safari

Schema

  • Migrations 40-43: first_run_state, chat_push_subscriptions with indices

Test plan

  • bun test: 1,530 tests, 0 failures (45 new)
  • bun run typecheck and bun run lint: clean
  • chat-ui typecheck and build: clean, 211KB gzipped
  • Manual: first boot without Slack sends email / prints token
  • Manual: push notification received on macOS Safari
  • Manual: iOS PWA install + push notification

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.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/chat/first-run.ts
Comment on lines +39 to +40
if (state?.email_sent_at || state?.stdout_printed_at) {
return; // Already handled in a previous boot
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread src/chat/email-login.ts
Comment on lines +92 to +94
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
console.error(`[email-login] Failed to send: ${msg}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread src/index.ts
Comment on lines +393 to +397
notificationTriggers = new NotificationTriggerService({
db,
vapidKeys,
focusMap,
ownerEmail: process.env.OWNER_EMAIL,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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
@mcheemaa mcheemaa merged commit 5114cc8 into main Apr 15, 2026
1 check passed
mcheemaa added a commit that referenced this pull request Apr 16, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant