feat: email integration for BetterAuth callbacks#2038
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
🦋 Changeset detectedLatest commit: 029ecc8 The changes in this PR will be included in the next version bump. This PR includes changesets to release 10 packages
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 |
|
📚 Documentation Review The documentation included in this PR is comprehensive and well-structured. No additional docs changes needed. What's included:
Cross-linking: Both docs properly reference each other, making it easy for users to navigate between feature overview and setup instructions. |
There was a problem hiding this comment.
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:
- 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.
- Persist to runtime DB — Store email send status in the runtime database (e.g., add an
email_sent_atcolumn to theinvitationtable or a separateemail_statustable). - 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:
- email-send-status-store.ts
- password-reset-link-store.ts (existing pattern)
🟠 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:
- Add a short delay — Wait 500-1000ms before polling to give the email send time to complete
- Retry with backoff — Poll 2-3 times with 500ms intervals before giving up
- Make email send synchronous — Move email sending before the invitation API returns (changes the response latency tradeoff)
- Return status in response — Include
emailSentin 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:
- 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).
- Store reset link status — Similar to
email-send-status-store, create apassword-reset-status-storeand update the forgot-password UI to check it. - 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.jsonMissing biome.json for new package - 🟡 Minor:
agents-api/package.json:60Inconsistent workspace specifier (workspace:*vsworkspace:^) - 🟡 Minor:
packages/agents-email/src/send.ts:30PII (email address) logged in error messages - 🟡 Minor:
agents-api/src/domains/manage/routes/invitations.ts:142Raw 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:385Missing aria-hidden on decorative icon - 💭 Consider:
agents-docs/content/deployment/(docker)/email.mdx:76Mailpit 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.
agents-manage-ui/src/components/settings/invite-member-dialog.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
PR Review Summary
(0) New Issues | Risk: Low (delta only)
This is a delta re-review scoped to changes since the last automated review (2fc8233 → 3bc3b55).
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 | ✅ Fixed — org-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: whenauthMethod='google'butPUBLIC_GOOGLE_CLIENT_IDis not set (orauthMethod='sso'butPUBLIC_AUTH0_DOMAINis not set), the CardContent renders empty with no way for users to proceed - 🟠
auth.ts:208-221— New security-criticalhasCredentialAccount()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:
-
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.
-
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.
There was a problem hiding this comment.
PR Review Summary
(1) Total Issues | Risk: Medium (delta only)
This is a delta re-review scoped to changes since the last automated review (c07e9e28 → 7131668).
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-460— Empty form state when auth provider not configured: WhenauthMethod='google'butPUBLIC_GOOGLE_CLIENT_IDis not set (orauthMethod='sso'butPUBLIC_AUTH0_DOMAINis 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:460Empty 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> |
There was a problem hiding this comment.
🟠 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 buttonisSSOAuth && PUBLIC_AUTH0_DOMAIN→ SSO buttonisEmailPassword(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:
- Falls back to email-password signup when the preferred auth method's provider is not configured
- 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:
There was a problem hiding this comment.
PR Review Summary
(0) New Issues | Risk: Low (delta only)
This is a delta re-review scoped to changes since the last automated review (713166881 → 9a45d7b2).
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'butPUBLIC_GOOGLE_CLIENT_IDis not set → Shows informative warning - ✅ When
authMethod='sso'/'auth0'butPUBLIC_AUTH0_DOMAINis not set → Shows informative warning - ✅ Uses
aria-hiddenon 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-emailpackage — standalone with transport factory, React Email templates, and comprehensive tests (39 unit tests) - BetterAuth callbacks —
sendInvitationEmailandsendResetPasswordwired to Nodemailer SMTP transport - Self-service forgot password — gated behind
isSmtpConfiguredwith 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.
|
🔎💬 Inkeep AI search and chat service is syncing content for source 'Inkeep Agent Framework Docs' |
Summary
Wire
sendInvitationEmailandsendResetPasswordBetterAuth callbacks to actually send branded, professional emails via Nodemailer SMTP transport.New
@inkeep/agents-emailpackageStandalone 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 fromagents-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/componentswith Inkeep brand colors, left-aligned logo, system font stack, and subtle fallback link.Self-service forgot password
New
/forgot-passwordpage withauthClient.requestPasswordReset(), gated behindisSmtpConfigured. 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 internalGET /manage/api/invitations/:id/email-statusendpoint with role-based authz and cross-tenant isolation.Invitation UX improvements
createAgentsApp()/createAgentsAuth()extensionBoth functions now accept an optional
emailServiceparam, making email a composable concern that can be passed through the factory without coupling the core auth module to a specific transport.Infrastructure
docker-compose.ymlfor local dev email inspection (web UI on:8025, SMTP on:1025)manage-uiandapiservice definitions.env.exampleandcreate-agents-template/.env.example: Email configuration section added with Resend + generic SMTP optionsscripts/sync-licenses.mjs: New package added to license sync targetsDocumentation
agents-docs/content/deployment/(docker)/email.mdx— full email configuration guide covering Resend, generic SMTP, Mailpit, graceful degradation, verification, and troubleshootingagents-docs/content/visual-builder/access-control.mdx— "Inviting Team Members" and new "Password Reset" sections with email-aware flows and cross-link to email configSpec
~/.claude/specs/email-integration/SPEC.mdTest plan
Automated (39 unit tests passing)
Manual — browser tested locally with Mailpit
{ emailSent: false }for invitations outside caller orgManual — Resend production transport
sendInvitationEmailreturns{ emailSent: true }viasmtp.resend.com:465sendPasswordResetEmailreturns{ emailSent: true }viasmtp.resend.com:465notifications@updates.inkeep.comtoedwin@inkeep.com— verified delivery🤖 Generated with Claude Code