Skip to content

feat: email integration for BetterAuth callbacks#2038

Merged
omar-inkeep merged 41 commits intomainfrom
feat/email-integration
Mar 3, 2026
Merged

feat: email integration for BetterAuth callbacks#2038
omar-inkeep merged 41 commits intomainfrom
feat/email-integration

Conversation

@nick-inkeep
Copy link
Collaborator

Summary

Wire sendInvitationEmail and sendResetPassword BetterAuth callbacks to actually send branded, professional emails via Nodemailer SMTP transport.

New @inkeep/agents-email package

Standalone package (packages/agents-email/) with its own build (tsdown), test (vitest), and env schema. Contains the transport factory, email service, React Email templates, and shared theme — fully decoupled from agents-api.

Transport factory

Auto-detects Resend SMTP relay (smtp.resend.com:465), generic SMTP, or disabled — single abstraction for cloud, self-hosted, and local dev. Connection/socket timeouts (10s/10s/30s) prevent API response hangs.

React Email templates

Branded invitation + password reset templates using @react-email/components with Inkeep brand colors, left-aligned logo, system font stack, and subtle fallback link.

Self-service forgot password

New /forgot-password page with authClient.requestPasswordReset(), gated behind isSmtpConfigured. Email prefill from login page via query param. Generic "if account exists" messaging (enumeration prevention).

Graceful degradation

When SMTP is not configured, zero behavior changes from today — copy-link preserved, forgot-password hidden, bridge works. No errors thrown.

Per-send email status

In-memory bridge pattern (mirrors password-reset-link-store.ts) surfaces send success/failure to invitation UI. New internal GET /manage/api/invitations/:id/email-status endpoint with role-based authz and cross-tenant isolation.

Invitation UX improvements

  • Copy-link for all auth methods: Extended from email-password only to all methods (Google, SSO) as universal fallback — removed tooltip-only path for non-email invitations
  • Auto-copy clipboard: Single invite without email auto-copies link to clipboard with contextual messaging
  • Email-aware messaging: Shows "Invitation email sent" when email succeeds, fallback instructions otherwise

createAgentsApp() / createAgentsAuth() extension

Both functions now accept an optional emailService param, making email a composable concern that can be passed through the factory without coupling the core auth module to a specific transport.

Infrastructure

  • Mailpit: Added to docker-compose.yml for local dev email inspection (web UI on :8025, SMTP on :1025)
  • Docker Compose env passthrough: SMTP/Resend env vars forwarded to both manage-ui and api service definitions
  • .env.example and create-agents-template/.env.example: Email configuration section added with Resend + generic SMTP options
  • scripts/sync-licenses.mjs: New package added to license sync targets

Documentation

  • New: agents-docs/content/deployment/(docker)/email.mdx — full email configuration guide covering Resend, generic SMTP, Mailpit, graceful degradation, verification, and troubleshooting
  • Updated: agents-docs/content/visual-builder/access-control.mdx — "Inviting Team Members" and new "Password Reset" sections with email-aware flows and cross-link to email config

Spec

~/.claude/specs/email-integration/SPEC.md

Test plan

Automated (39 unit tests passing)

  • Transport factory: Resend, generic SMTP, and null transport paths
  • Email service: send success, send failure, unconfigured transport
  • Template rendering: invitation (all auth methods) + password reset
  • Email status store: set/get/auto-expire with TTL, organizationId cross-tenant isolation
  • Component rendering: email button, header, footer, layout
  • Environment variable parsing and validation

Manual — browser tested locally with Mailpit

  • Forgot password page: form submission works, email prefill from login page via query param, generic "if account exists" message (enumeration prevention)
  • Forgot password SMTP gate: link hidden on login page when SMTP not configured, visible when configured
  • Password reset email delivery: email arrives in Mailpit with branded template, reset link works end-to-end, redirects back to manage-ui
  • Invitation email delivery: invitation email arrives in Mailpit with branded template, correct CTA text per auth method
  • Invitation UI with email configured: shows "Invitation email sent" with backup copy-link button
  • Invitation UI without email, single invite: auto-copies link to clipboard, shows clipboard copied message
  • Invitation UI without email, multiple invites: shows numbered list instructions
  • Email template URL: fallback URL shown as subtle "Link:" label, not raw monospace
  • Cross-tenant isolation: email-status endpoint returns { emailSent: false } for invitations outside caller org
  • Mailpit integration: end-to-end email delivery in local dev, web UI at localhost:8025
  • Graceful degradation: no SMTP = zero behavior changes, copy-link works, no errors thrown
  • Login page UX: Forgot password link positioned below password input with subtle styling
  • Accessibility: aria-hidden on all decorative icons in forgot-password page
  • Logo alignment: left-aligned in email templates

Manual — Resend production transport

  • Resend SMTP relay: sendInvitationEmail returns { emailSent: true } via smtp.resend.com:465
  • Resend SMTP relay: sendPasswordResetEmail returns { emailSent: true } via smtp.resend.com:465
  • Emails sent from notifications@updates.inkeep.com to edwin@inkeep.com — verified delivery

🤖 Generated with Claude Code

nick-inkeep and others added 25 commits February 16, 2026 00:27
Starting point for email integration. See SPEC at ~/.claude/specs/email-integration/SPEC.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… and types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…port

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
In-memory set/get store with auto-expiry (5 min TTL) for tracking
email send results keyed by invitation ID. Mirrors the bridge
pattern from password-reset-link-store.ts. 9 tests covering
set/get, unknown keys, TTL expiry, and TTL reset on overwrite.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add EmailServiceConfig interface to BetterAuthConfig (optional)
- Update sendInvitationEmail callback: sends email via emailService,
  stores result in email-send-status-store (keyed by invitation ID)
- Update sendResetPassword callback: sends email alongside existing
  password-reset-link bridge (both fire in parallel)
- Construct invitation URL from INKEEP_AGENTS_MANAGE_UI_URL
- Wire emailService through factory.ts createAgentsAuth()
- Create emailService in agents-api index.ts default startup
- Add @inkeep/email dependency to agents-api
- Export email-send-status-store from agents-core barrel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…xample

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…isSmtpConfigured

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…l email status

- Add email status endpoint (GET /invitations/:id/email-status) to check send result
- Update invite-member-dialog to check email status after invite creation
- Show "Invitation email sent", "Email could not be sent — copy the link", or "Copy link" based on status
- Remove email-password-only guard on copy-link button — now available for all auth methods (D17)
- Update members-table to show copy-link for all pending invitations regardless of auth method
- Remove unused Info icon and Tooltip imports from members-table
- Update "Next Steps" messaging to reflect email vs copy-link flow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add connectionTimeout (10s), greetingTimeout (10s), and socketTimeout (30s)
to both Resend and generic SMTP transports per SPEC NFR requirements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e sync

- Add admin/owner authorization check on /invitations/:id/email-status endpoint
- Pin Mailpit Docker image to v1.24.1 for reproducibility
- Add error logging in forgot-password and invite-member-dialog catch blocks
- Improve email failure warning to handle missing error message edge case
- Add packages/email to license sync script
- Mark @inkeep/email as private (internal package)
- Add internal route comment for consistency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rect fix

- Move "Forgot password?" link below password input, right-aligned, subtle styling
- Pass typed email from login to forgot-password via query param
- Fix redirectTo to use window.location.origin so reset link goes to manage-ui, not API
- Remove tabIndex={-1} for keyboard accessibility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add aria-hidden="true" to all decorative icons in forgot-password page
- Update access-control docs with invitation email flow and password reset section
- Create deployment email configuration guide (SMTP, Resend, Mailpit)
- Add email page to deployment sidebar navigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Store organizationId in the email status bridge and verify it matches
the caller's active organization before returning status. Also strip
organizationId from the response to avoid leaking it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…r messaging

- Email template: replace raw URL with subtle "Or copy and paste this link"
  fallback text with a clickable link
- Auto-copy invite link to clipboard when email is not configured and a
  single user is invited
- Single invite (no email): "An invite link has been copied to your clipboard.
  Share the invite link with your team member to have them join!"
- Multiple invites (no email): numbered list — "1. Copy the link for each
  team member. 2. Share the invite link and ask them to redeem!"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Align package naming with the monorepo convention (agents-core, agents-sdk, agents-work-apps, etc.).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Feb 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agents-manage-ui Ready Ready Preview, Comment Mar 3, 2026 1:34am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
agents-api Ignored Ignored Preview Mar 3, 2026 1:34am
agents-docs Ignored Ignored Preview Mar 3, 2026 1:34am

Request Review

@changeset-bot
Copy link

changeset-bot bot commented Feb 16, 2026

🦋 Changeset detected

Latest commit: 029ecc8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
Name Type
@inkeep/agents-core Patch
@inkeep/agents-api Patch
@inkeep/agents-manage-ui Patch
@inkeep/agents-cli Patch
@inkeep/agents-sdk Patch
@inkeep/agents-work-apps Patch
@inkeep/ai-sdk-provider Patch
@inkeep/create-agents Patch
@inkeep/agents-email Patch
@inkeep/agents-mcp Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@inkeep
Copy link
Contributor

inkeep bot commented Feb 16, 2026

📚 Documentation Review

The documentation included in this PR is comprehensive and well-structured. No additional docs changes needed.

What's included:

  • ✅ New deployment/email.mdx — complete email configuration guide (Resend, generic SMTP, Mailpit, graceful degradation, troubleshooting)
  • ✅ Updated visual-builder/access-control.mdx — new invitation flow and password reset sections with email-aware messaging
  • ✅ Navigation updated to include the email page in the deployment section

Cross-linking: Both docs properly reference each other, making it easy for users to navigate between feature overview and setup instructions.

Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(7) Total Issues | Risk: Medium

🟠⚠️ Major (3) 🟠⚠️

🟠 1) email-send-status-store.ts In-memory store doesn't persist across instances

files: packages/agents-core/src/auth/email-send-status-store.ts

Issue: The email-send-status-store uses an in-memory Map that is process-local. In multi-instance deployments (typical for production), the API instance that sends the email may differ from the instance that handles the subsequent /email-status poll, resulting in { emailSent: false } even when the email was successfully sent.

Why: This creates a confusing UX where users see "email status unknown" or fallback messaging even though their email was delivered. The existing password-reset-link-store.ts has the same architectural constraint but serves a different purpose (promise bridge for blocking flows). For non-blocking status polling, this pattern is unreliable under load.

Fix: Consider these approaches:

  1. Accept the limitation — Document that email status is best-effort in multi-instance deployments. The current graceful degradation ("Copy the link to share manually") is adequate for this use case.
  2. Persist to runtime DB — Store email send status in the runtime database (e.g., add an email_sent_at column to the invitation table or a separate email_status table).
  3. Return status synchronously — Return the email send result directly in the invitation creation response instead of polling.

Given that the fallback UX is reasonable and this is a "nice to have" feature, approach #1 may be acceptable for v1. If you expect multi-instance deployments to be common, approach #3 is the cleanest.

Refs:


🟠 2) invite-member-dialog.tsx:175-190 Race condition in email status polling

file: agents-manage-ui/src/components/settings/invite-member-dialog.tsx:175-190

Issue: The client immediately polls /email-status after the invitation API returns, but the email is sent asynchronously in the BetterAuth callback. There's no guarantee the email send has completed (or even started) by the time the poll occurs.

Why: This creates a race where the status may not yet be written to the store, causing false "email status unknown" results even on single-instance deployments. The 5-minute TTL provides eventual availability, but the initial poll is unreliable.

Fix: Consider these options:

  1. Add a short delay — Wait 500-1000ms before polling to give the email send time to complete
  2. Retry with backoff — Poll 2-3 times with 500ms intervals before giving up
  3. Make email send synchronous — Move email sending before the invitation API returns (changes the response latency tradeoff)
  4. Return status in response — Include emailSent in the invitation creation response (requires BetterAuth callback changes)

Option #2 is the most pragmatic for the current architecture:

let emailSent = false;
for (let i = 0; i < 3 && !emailSent; i++) {
  if (i > 0) await new Promise(r => setTimeout(r, 500));
  const statusRes = await fetch(...);
  if (statusRes.ok) {
    const data = await statusRes.json();
    emailSent = data.emailSent === true;
  }
}

Refs:


🟠 3) auth.ts:225-232 Password reset email failure silently swallowed

file: packages/agents-core/src/auth/auth.ts:225-232

Issue: When sendPasswordResetEmail fails, the error is logged but the user still sees the generic "check your email" success message. Unlike invitations (which have the email-status store and UI fallback), password reset has no way for the user to know their email wasn't sent.

Why: A user who types their email, clicks "Send reset link", and sees "Check your email" will wait indefinitely for an email that never arrives. This is a poor UX for a critical auth flow.

Fix: Consider these options:

  1. Throw on failure — Let the error propagate so the user sees an error message. This changes the security posture (timing attacks may reveal account existence).
  2. Store reset link status — Similar to email-send-status-store, create a password-reset-status-store and update the forgot-password UI to check it.
  3. Add admin alerting — Keep current behavior but add monitoring/alerting for failed password reset emails.

Given the security tradeoffs, option #3 (monitoring) combined with clear documentation may be acceptable. The current console.error provides some visibility.

Refs:

🟡 Minor (1) 🟡

🟡 1) invitations.ts Missing integration tests for email-status endpoint

file: agents-api/src/domains/manage/routes/invitations.ts:111-143

Issue: The new GET /:id/email-status endpoint has authorization logic (role check, cross-tenant isolation) but no integration tests. The email-send-status-store has unit tests, but the API route's auth behavior is untested.

Why: Auth bugs are high-severity and hard to catch without explicit test coverage. The cross-tenant check at lines 136-138 is particularly important to verify.

Fix: Add integration tests covering:

  • Non-authenticated request returns { emailSent: false } (not an error)
  • Member role (non-admin) returns 403
  • Admin from different org returns { emailSent: false } (not the actual status)
  • Admin from same org returns actual status

Refs:

Inline Comments:

  • 🟡 Minor: packages/agents-email/package.json Missing biome.json for new package
  • 🟡 Minor: agents-api/package.json:60 Inconsistent workspace specifier (workspace:* vs workspace:^)
  • 🟡 Minor: packages/agents-email/src/send.ts:30 PII (email address) logged in error messages
  • 🟡 Minor: agents-api/src/domains/manage/routes/invitations.ts:142 Raw SMTP error exposed to client

💭 Consider (2) 💭

💭 1) types.ts + auth.ts Duplicate EmailServiceConfig interface

Issue: EmailServiceConfig is defined in both packages/agents-email/src/types.ts (as EmailService) and packages/agents-core/src/auth/auth.ts (as EmailServiceConfig). The interfaces are identical.

Why: Maintenance burden — changes need to be made in two places.

Fix: Export EmailService from @inkeep/agents-email and use it as the type in agents-core. Since agents-email is a workspace dependency, this avoids duplication.

Refs:

💭 2) invite-member-dialog.tsx:175-190 Sequential API calls create waterfall

Issue: Email status checks for multiple invitations are made sequentially in a for...of loop.

Why: Batch invitations will be slower than necessary. For 5 invitations, this adds ~500ms+ of sequential network latency.

Fix: Use Promise.all() or Promise.allSettled() to parallelize the status checks:

const statusResults = await Promise.allSettled(
  pendingResults.map(async (result) => {
    const statusRes = await fetch(`.../${result.invitationId}/email-status`, ...);
    return statusRes.ok ? statusRes.json() : null;
  })
);

Refs:

Inline Comments:

  • 💭 Consider: agents-manage-ui/src/components/settings/invite-member-dialog.tsx:385 Missing aria-hidden on decorative icon
  • 💭 Consider: agents-docs/content/deployment/(docker)/email.mdx:76 Mailpit docs link uses unofficial domain

💡 APPROVE WITH SUGGESTIONS

Summary: This is a well-architected feature with clean separation of concerns, comprehensive test coverage for the new package, and thoughtful graceful degradation. The main concerns are around the in-memory store's behavior in multi-instance deployments and the race condition in email status polling — both have reasonable workarounds documented. The inline comments address minor consistency and hygiene issues. Nice work on the React Email templates and the factory extension pattern! 🎉

Discarded (5)
Location Issue Reason Discarded
transport.ts Missing retry logic for transient SMTP failures Valid observation but adds complexity; current error handling + logging is adequate for v1
forgot-password/page.tsx Missing aria-hidden on icons Already has aria-hidden="true" on all decorative icons (Loader2, Mail, AlertCircle, ArrowLeft)
env.ts New env vars not in centralized env schemas The agents-email package handles its own env parsing; this is intentional decoupling for a standalone package
email.mdx Missing troubleshooting for rate limiting Nice-to-have but not critical for v1 docs
invitations.ts Endpoint not excluded from OpenAPI Marked as internal (comment says "not exposed in OpenAPI spec") — this is intentional
Reviewers (13)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-standards 8 1 1 0 2 0 4
pr-review-product 6 1 0 0 0 0 5
pr-review-consistency 5 0 1 0 1 0 3
pr-review-security-iam 4 0 0 0 1 0 3
pr-review-architecture 4 1 0 0 0 0 3
pr-review-frontend 5 1 1 0 1 0 2
pr-review-errors 4 1 0 0 0 0 3
pr-review-tests 3 1 0 0 0 0 2
pr-review-sre 3 0 0 0 1 0 2
pr-review-docs 3 0 0 0 1 0 2
pr-review-devops 2 0 0 0 0 0 2
pr-review-breaking-changes 2 0 0 0 0 0 2
pr-review-types 2 0 0 0 0 0 2
Total 51 6 3 0 7 0 35

Note: High discard rate reflects thorough filtering — many findings were pre-existing issues, already addressed by the PR's graceful degradation design, or not applicable to this PR's scope.

@github-actions github-actions bot deleted a comment from claude bot Feb 16, 2026
@github-actions github-actions bot deleted a comment from claude bot Mar 2, 2026
Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(0) New Issues | Risk: Low (delta only)

This is a delta re-review scoped to changes since the last automated review (2fc82333bc3b55).

Delta Analysis

The delta consists of 4 commits:

Commit Description
c07e9e28e feedback — Addresses accessibility + duplicate constants
3bc3b5545 Merge branch 'main' into feat/email-integration
9e3c0ef5d Version Packages (#2474)
6977aff5f fix: restrict API keys to run domain only (#2478)

The feedback commit addresses previously raised issues:

Previous Issue Status
🟡 Missing aria-hidden on AlertCircleIcon Fixed — Line 355
🟡 Missing aria-hidden on Loader2 (Google button) Fixed — Line 369
🟡 Missing aria-hidden on Loader2 (SSO button) Fixed — Line 390
🟡 Missing aria-hidden on GoogleColorIcon Fixed — Line 374
🟡 Missing aria-hidden on InkeepIcon Fixed — Line 395
🧹 Duplicate role options constants Fixedorg-role-selector.tsx now imports ROLE_OPTIONS from ./types

The merge from main brings infrastructure improvements (version bumps, API key auth restriction) that are unrelated to the email integration feature.

🕐 Pending Recommendations (2)

These Major issues from the previous review remain unaddressed:

  • 🟠 accept-invitation/[invitationId]/page.tsx:360-400 — Empty form state when auth provider not configured: when authMethod='google' but PUBLIC_GOOGLE_CLIENT_ID is not set (or authMethod='sso' but PUBLIC_AUTH0_DOMAIN is not set), the CardContent renders empty with no way for users to proceed
  • 🟠 auth.ts:208-221 — New security-critical hasCredentialAccount() function lacks test coverage

🚫 REQUEST CHANGES

Summary: The feedback commit successfully addresses all previously raised Minor accessibility issues and the "While You're Here" duplicate constants finding. However, the two Major issues from the previous review remain unaddressed:

  1. Configuration-dependent bug: When an invitation specifies Google or SSO auth but the deployment doesn't have the corresponding provider configured, users see an empty form with no way to proceed or understand what's wrong.

  2. Missing test coverage: The hasCredentialAccount() function gates password reset emails (a security-critical path) but has zero test coverage.

Both issues were flagged in the previous review with suggested fixes. Please address these before merging.

Discarded (0)

No new findings to discard — this is a delta-only re-review.

Reviewers (0)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
(delta-only review — sub-reviewers not dispatched)

Note: Sub-reviewers were not dispatched because this is a delta-only re-review evaluating whether previously raised issues were addressed. The delta contains only feedback fixes and infrastructure merges from main.

@github-actions github-actions bot deleted a comment from claude bot Mar 3, 2026
Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(1) Total Issues | Risk: Medium (delta only)

This is a delta re-review scoped to changes since the last automated review (c07e9e287131668).

Delta Analysis

The delta consists of 4 commits:

Commit Description
713166881 new test — Adds hasCredentialAccount() test coverage
3bc3b5545 Merge branch 'main' into feat/email-integration
9e3c0ef5d Version Packages (#2474)
6977aff5f fix: restrict API keys to run domain only (#2478)

✅ Previously Raised Issue FIXED

🟠 hasCredentialAccount() lacks test coverage — Now addressed!

The new test file packages/agents-core/src/auth/__tests__/hasCredentialAccount.test.ts provides comprehensive coverage:

  • ✅ User with credential account → returns true
  • ✅ User with only OAuth account (google) → returns false
  • ✅ User with both credential and OAuth accounts → returns true
  • ✅ Non-existent user → returns false
  • ✅ User with only auth0 account → returns false

This exactly matches what was requested in the prior review.

🕐 Pending Recommendations (1)

This Major issue from the previous review remains unaddressed:

  • 🟠 accept-invitation/[invitationId]/page.tsx:360-460Empty form state when auth provider not configured: When authMethod='google' but PUBLIC_GOOGLE_CLIENT_ID is not set (or authMethod='sso' but PUBLIC_AUTH0_DOMAIN is not set), the CardContent renders empty with no way for users to proceed. See inline comment for details and suggested fix.

Inline Comments:

  • 🟠 Major: accept-invitation/[invitationId]/page.tsx:460 Empty form state when auth provider not configured

🚫 REQUEST CHANGES

Summary: Good progress — the hasCredentialAccount() test coverage issue has been resolved with a well-structured test file covering all edge cases. However, the configuration-dependent UI bug on the accept-invitation page remains unaddressed. When an invitation specifies Google or SSO auth but the deployment doesn't have the corresponding provider configured, users see an empty form with no way to proceed or understand what's wrong. This needs a fallback UX (either fall back to email-password or show an error message) before shipping.

Discarded (0)

No findings discarded in this delta review.

Reviewers (0)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
(delta-only review — sub-reviewers not dispatched)

Note: Sub-reviewers were not dispatched because this is a delta-only re-review. The previous full review already covered the feature scope; this review evaluates whether previously raised issues were addressed.

</Button>
</form>
)}
</CardContent>
Copy link
Contributor

Choose a reason for hiding this comment

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

🟠 MAJOR: Empty form state when auth provider not configured (unresolved from prior review)

Issue: When authMethod='google' but PUBLIC_GOOGLE_CLIENT_ID is not set, or authMethod='sso'/'auth0' but PUBLIC_AUTH0_DOMAIN is not set, the CardContent renders empty — no buttons, no form, no explanation.

The conditional rendering logic at lines 360-400:

  • isGoogleAuth && PUBLIC_GOOGLE_CLIENT_ID → Google button
  • isSSOAuth && PUBLIC_AUTH0_DOMAIN → SSO button
  • isEmailPassword (which is !isGoogleAuth && !isSSOAuth) → Email form

When isGoogleAuth=true but PUBLIC_GOOGLE_CLIENT_ID is falsy, neither the Google button nor the email form renders.

Why: Users see a broken/empty invitation page with no way to proceed or understand what went wrong. This is a deployment configuration bug that manifests as a silent UX failure.

Fix: Add a fallback branch that either:

  1. Falls back to email-password signup when the preferred auth method's provider is not configured
  2. Shows an error message explaining the configuration issue

For example, after line 459:

{/* Fallback when auth method's provider is not configured */}
{!isEmailPassword && (
  (isGoogleAuth && !PUBLIC_GOOGLE_CLIENT_ID) || 
  (isSSOAuth && !PUBLIC_AUTH0_DOMAIN)
) && (
  <Alert variant="destructive">
    <AlertCircleIcon className="h-4 w-4" aria-hidden="true" />
    <AlertDescription>
      This organization requires {isGoogleAuth ? 'Google' : 'SSO'} authentication, 
      but it is not configured. Please contact your administrator.
    </AlertDescription>
  </Alert>
)}

Refs:

Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(0) New Issues | Risk: Low (delta only)

This is a delta re-review scoped to changes since the last automated review (7131668819a45d7b2).

Delta Analysis

The delta consists of a single commit:

Commit Description
9a45d7b23 page fallback — Adds fallback UI when auth provider not configured

✅ Previously Raised Major Issue FIXED

🟠 Empty form state when auth provider not configured — Now addressed!

The commit adds a fallback Alert at lines 402-411 in accept-invitation/[invitationId]/page.tsx:

{((isGoogleAuth && !PUBLIC_GOOGLE_CLIENT_ID) ||
  (isSSOAuth && !PUBLIC_AUTH0_DOMAIN)) && (
  <Alert className="border-amber-200 bg-amber-50 dark:border-amber-900 dark:bg-amber-950">
    <AlertCircleIcon aria-hidden className="h-4 w-4 text-amber-600" />
    <AlertDescription>
      The sign-in method for this invitation is not available. Please contact your
      organization administrator.
    </AlertDescription>
  </Alert>
)}

This correctly handles the configuration edge case:

  • ✅ When authMethod='google' but PUBLIC_GOOGLE_CLIENT_ID is not set → Shows informative warning
  • ✅ When authMethod='sso'/'auth0' but PUBLIC_AUTH0_DOMAIN is not set → Shows informative warning
  • ✅ Uses aria-hidden on the decorative icon (consistent with prior accessibility feedback)
  • ✅ Uses amber/warning styling to clearly indicate a configuration issue (not a user error)
  • ✅ Provides actionable guidance ("contact your organization administrator")

This directly addresses the concern raised in the prior review.

Prior Review Summary

All previously raised issues have now been addressed:

Issue Status Resolution
🟠 Empty form state (auth provider not configured) Fixed Commit 9a45d7b23
🟠 hasCredentialAccount() lacks test coverage Fixed Commit 713166881
🟡 Missing aria-hidden on icons Fixed Commit c07e9e28e
🧹 Duplicate role options constants Fixed Commit c07e9e28e
Earlier feedback (biome.json, workspace specifier, error sanitization, docs link) Fixed Commit 11826b4d4

Email Integration Feature Summary

This PR adds a well-architected email integration with:

  • New @inkeep/agents-email package — standalone with transport factory, React Email templates, and comprehensive tests (39 unit tests)
  • BetterAuth callbackssendInvitationEmail and sendResetPassword wired to Nodemailer SMTP transport
  • Self-service forgot password — gated behind isSmtpConfigured with enumeration prevention
  • Auth-method-aware invitation flow — Google/SSO/email-password branches with proper fallback UI
  • Graceful degradation — zero behavior changes when SMTP is not configured
  • Documentation — deployment guide + access control updates

✅ APPROVE

Summary: The final outstanding issue — the empty form state when auth provider is not configured — has been addressed with an informative warning Alert. All previously raised issues across multiple review iterations have now been resolved. The email integration feature is well-tested, documented, and handles edge cases gracefully. Ship it! 🚀

Reviewers (0)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
(delta-only review — sub-reviewers not dispatched)

Note: Sub-reviewers were not dispatched because this is a delta-only re-review. The delta is a single-file, 11-line fix directly addressing previously raised feedback. The full feature was thoroughly reviewed in prior iterations.

@github-actions github-actions bot deleted a comment from claude bot Mar 3, 2026
@omar-inkeep omar-inkeep enabled auto-merge (squash) March 3, 2026 01:32
@omar-inkeep omar-inkeep merged commit 4414e25 into main Mar 3, 2026
7 of 9 checks passed
@omar-inkeep omar-inkeep deleted the feat/email-integration branch March 3, 2026 01:34
@github-actions
Copy link
Contributor

github-actions bot commented Mar 3, 2026

🔎💬 Inkeep AI search and chat service is syncing content for source 'Inkeep Agent Framework Docs'

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.

2 participants