Skip to content

feat: add mailing and organizations - #28

Merged
RedStar071 merged 13 commits into
mainfrom
feat/mailing-and-organizations
Aug 13, 2026
Merged

feat: add mailing and organizations#28
RedStar071 merged 13 commits into
mainfrom
feat/mailing-and-organizations

Conversation

@RedStar071

@RedStar071RedStar071 commented Aug 12, 2026

Copy link
Copy Markdown
Member

Summary

Following supastarter's Nuxt guides (mailing overview, organizations: store data for organizations):

  • packages/mail: a Maizzle-backed mail package. Templates live in emails/ (Vue SFC + Tailwind, compiled and inlined per send), registered by id in src/util/templates.ts with their subject. Delivery goes through a single MailProvider contract selected in src/provider/index.ts; console is the default (logs instead of sending), with Resend and Nodemailer SMTP behind it, both imported lazily so only the configured transport has to be installed.
  • Organizations, added at the auth layer and in the dashboard: Better Auth's organization plugin wired into packages/auth, a Drizzle migration for organization/member/invitation plus session.active_organization_id, and an app/modules/organizations dashboard module (switcher, member list, invite form, accept-invitation page).
  • apps/dashboard restructured into supastarter-style feature modules (modules/{auth,dashboard,shared,organizations}) before adding the above, so the new organizations module has somewhere to land that matches the rest of the app.

Architecture boundary

packages/auth declares SendInvitationEmail structurally and never imports @agent-zero/mail — one capability package must not import another. apps/auth-server (the composition root) builds a mailer with createMailer() and injects it into createAuth(). createAuth refuses to construct when organizations are enabled without a transport, so a misconfigured deployment fails at startup rather than accepting invitations nobody is ever told about.

apps/dashboard stays frontend-only: the organizations module only calls the Better Auth client (organizationClient()), declares its own Organization/OrganizationMember types rather than importing them from @agent-zero/auth (which would pull the database adapter into the browser bundle), and every mutation refetches from the server rather than patching local state.

Both features are off by default: AUTH_ENABLE_ORGANIZATIONS=false and MAIL_PROVIDER=console, so this PR doesn't change behavior for existing deployments.

Also in this branch

  • refactor(dashboard): reorganizes apps/dashboard/app into modules/{auth,dashboard,shared} (prerequisite restructuring, done first at the user's request and reviewed separately in-session).
  • chore(ci): drops two CI jobs (knip, i18n) that were unrelated to this work and got swept into an earlier commit — the i18n job checked packages/i18n/* paths that don't exist in this repo (i18n lives under apps/dashboard/i18n/), so it would have failed or silently no-op'd against the wrong paths. Left for whoever intended that CI coverage to reintroduce it correctly, separately.

Type of Change

  • New feature
  • Bug fix
  • Breaking change
  • Refactor
  • Cosmetic
  • Documentation
  • Workflow

Test Procedure

Ran the full required check set from CONTRIBUTING.md:

aube run check:repo # pass
aube run lint:ci # pass, 0 warnings/errors across all 20 packages, incl. new packages/mail
aube run typecheck # pass, 20/20 packages
aube test # pass, 20/20 task files — packages/mail: 12 tests (incl. a real Maizzle
# render pass: interpolation, Tailwind inlining, plaintext), packages/auth:
# 19 tests (incl. the sendInvitationEmail startup guard), apps/dashboard:
# 35 tests
aube run build # pass, 12/12 tasks

Also manually rendered OrganizationInvitation.vue through @maizzle/framework's render() outside the test suite to confirm Tailwind classes compile to inlined style= attributes rather than leaking class= into the output (mail clients routinely strip <style>/classes).

resend is pinned to 6.17.1 in packages/mail/package.json: aube add refused 6.18.1 because it dropped the trusted-publisher provenance that 6.17.1 carries (ERR_AUBE_TRUST_DOWNGRADE) — not bypassed.

Safety Impact

  • packages/auth and apps/dashboard still do not execute repository work or own persistence outside apps/auth-server, per the architecture boundaries in AGENTS.md.
  • The console mail provider never logs a message body, since invitation and password-reset links carry single-use tokens that routinely end up in shared terminal scrollback and CI logs otherwise — covered by a test.
  • The organization Drizzle migration (packages/auth/drizzle/0001_peaceful_franklin_storm.sql) is additive only: new tables plus one nullable column on session, safe against an existing database.
  • Invitation-accept links resolve against the dashboard origin, not the auth server's, and the invitation id from the email link is passed straight to the auth server without being trusted to build a redirect target.

View with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is enabled.

Confidence Score: 4/5

Member removal is not ready to merge until a rejected request reliably remains visible to the administrator.

One confirmed user-facing failure remains: the follow-up member refresh clears the error from a rejected removal request.

Files Needing Attention: apps/dashboard/app/modules/organizations/composables/useOrganizations.ts

T-Rex T-Rex Logs

What T-Rex did

  • Generated and validated proofs for a posted P1 finding, including two finding-comment proofs.
  • Ran an authored executable that started the auth-server composition root with organizations enabled, first without MAIL_PROVIDER and then with MAIL_PROVIDER=console, and observed exits before binding a listener, confirming console-only invitation delivery is rejected.
  • Performed an SMTP STARTTLS integration check against a local relay to verify plaintext fallback is blocked, with STARTTLS attempt leading to a guarded failure as expected.
  • Verified the useOrganizations test context and code behavior, including where error state is cleared at the start and how refreshMembers is invoked after removeMember, with related outputs uploaded and no production code changes.
  • Captured and preserved the authored executable source and runtime outputs, including guard messages for both provider configurations.

View all artifacts

T-Rex Ran code and verified through T-Rex

Fix All in Greploop

Fix All in Claude CodeFix All in CursorFix All in Cursor Cloud Agents

Prompt To Fix All With AI
### Issue 1
apps/dashboard/app/modules/organizations/composables/useOrganizations.ts:138
**Member-removal errors are cleared by the refresh**
When `organization.removeMember()` resolves with an API error, `run` records that error, but `removeMember` immediately calls `refreshMembers`. A successful refresh clears the shared error at the start of its own `run`, so the organizations page has no error to display for the rejected removal. Refresh members only after a successful removal, or preserve the mutation error across the refresh.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (3): Last reviewed commit: "Merge branch 'main' into feat/mailing-an..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

…dules
Reorganizes apps/dashboard/app into modules/{auth,dashboard,shared}, moving
components, composables, types and utils under the module that owns them, and
turns pages/ into a thin routing layer using (auth) and (dashboard) route
groups so the URLs stay /login and /.
Component tag names and composable call sites are unchanged: nuxt.config.ts
now registers the three module component roots explicitly, so each file keeps
the auto-import name it had under the default app/components scan.
The former package-root shared/ directory is folded into modules/shared/utils.
It existed for Nuxt's app-plus-server sharing convention, but the dashboard
owns no server, so the #shared alias is dropped from vitest, knip and tsconfig.
Adds packages/mail, following supastarter's mail layout: Maizzle templates in
emails/, a registry in src/util/templates.ts that maps a template id to its
file and subject, and a single provider export point in src/provider/.
Console delivery is the default so an unconfigured deployment logs rather than
attempting real delivery, and the suite never opens a socket. Resend and
Nodemailer SMTP sit behind the same MailProvider contract and are imported
lazily, so only the configured transport has to be installed.
sendEmail renders HTML and plaintext through Maizzle per send; the templates
carry per-recipient tokens, so there is no reusable compiled artifact to cache.
createMailer binds a provider and sender once for injection, which is what lets
packages/auth consume mail structurally instead of depending on this package.
resend is pinned to 6.17.1: 6.18.1 dropped the trusted-publisher provenance
that earlier releases carry, and the installer refuses it.
…izations commit
These two jobs predate this branch and are unrelated to mailing or
organizations. The i18n job also checks packages/i18n/schema.json,
packages/i18n/schemas and packages/i18n/locales, none of which exist in this
repo — i18n lives under apps/dashboard/i18n/ — so the check would either fail
or silently no-op against the wrong paths. Left for whoever intended to add
CI coverage for the dashboard's i18n tooling to reintroduce correctly, in its
own change.
…ons code
Hoists regex literals used inside functions to module scope, replaces unsafe
type assertions in mail/provider tests with a narrowing assertion function and
optional chaining, rewrites mailProviderFromEnvironment's provider switch as
if/else so every path is a recognized return, moves useOrganizations'
messageFrom helper to module scope since it captures nothing from its parent,
and adds a vi.fn() type parameter.
Also declares packages/mail in knip.jsonc: maizzle.config.ts is discovered by
Maizzle's render() through its own filesystem convention rather than imported,
and @maizzle/tailwindcss is resolved by package name as a Maizzle plugin.
@socket-security

socket-securityBot commented Aug 12, 2026

Copy link
Copy Markdown

@socket-security

socket-securityBot commented Aug 12, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

ActionSeverityAlert (click "▶" to expand/collapse)
WarnHigh
Obfuscated code: npm culori is 90.0% likely obfuscated

Confidence: 0.90

Location:Package overview

From:pnpm-lock.yamlnpm/@maizzle/framework@6.0.13npm/culori@4.0.2

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/culori@4.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

Comment threadapps/auth-server/src/index.ts Outdated
Comment threadpackages/mail/src/provider/smtp.ts
RedStar071and others added 6 commits August 12, 2026 08:22
…nd delivery guarantees
- surface Better Auth API errors that resolve with { data, error } in the
organizations composable and the accept-invitation page instead of
treating them as success
- initialize the active organization on refresh so the switcher's
displayed selection matches state
- require STARTTLS on non-implicit-TLS SMTP connections so delivery
never falls back to plaintext
- withhold sendInvitationEmail from createAuth when the console mail
provider is configured, so the startup guard rejects an
organizations-enabled deployment with no delivering transport
Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
…nt result types intact
The generic unwrap helper collapsed the client's { data, error } result
union to {}, failing typecheck and type-aware lint. Destructuring at
each call site and throwing via throwOnApiError preserves the inferred
data types while still routing resolved API errors through run's shared
error handling.
Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
Following supastarter's package layout, moves everything i18n-related out of
apps/dashboard into a new shared @agent-zero/i18n package: the locale registry
(config/i18n.ts -> src/config.ts), the translation content (i18n/locales,
i18n/schemas), the maintenance scripts (compare-translations,
find-invalid-translations, generate-i18n-schema, i18n-status,
remove-unused-translations, and their shared i18n-locale-files.ts helpers),
and the Lunaria translation-status config. config/i18n-empty-placeholders.ts
stays in the dashboard — it's a Vite plugin, not shared content.
apps/dashboard now imports @agent-zero/i18n. @nuxtjs/i18n's langDir does not
support absolute paths in production, so nuxt.config.ts resolves the
package's installed locales/ directory and passes absolute file paths per
locale instead (the module's own documented pattern for module-provided
locale files), keeping the package's own i18nLocales export portable.
Also fixes the two i18n tooling scripts that scan the consuming app's source
(find-invalid-translations, remove-unused-translations): their VUE_FILES_GLOB
now points at ../../apps/dashboard/app since the scripts no longer run from
inside that app. Running the moved i18n:report surfaced a real pre-existing
issue in this branch's own InviteForm.vue — a dynamic i18n key
(`organizations.roles.${value}`) that vue-i18n-extract cannot statically
verify — replaced with three static t() calls.
Restores the CI jobs (knip, i18n) dropped earlier in this branch as broken:
the i18n job's schema-drift check now points at the real packages/i18n/{schemas,locales}
paths. Also wires i18n:report and i18n:schema as root aube run scripts
(turbo run passthrough) alongside the existing i18n:status — neither had ever
been wired at the root, so the CI job would have failed regardless of the
path fix.
…running knip
The standalone knip CI job runs 'aube run knip' straight after install, but
apps/dashboard/nuxt.config.ts (and vitest.config.ts through
defineVitestProject) now import @agent-zero/i18n and @agent-zero/auth, whose
exports resolve to dist/. With no build, knip fails to load both configs and
cascades into false unused-file/dependency/export findings; the lint job's
knip run passes only because 'turbo run lint' builds dependencies first.
Route the root knip script through a filtered turbo build of the two packages
(turbo-cached, so it is a no-op replay where dists already exist).
Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
nuxt dev and vite dev both load .env automatically; a plain `node --import
tsx` entrypoint does not. apps/auth-server's dev script crashed on startup
with "missing required environment variable: AUTH_DASHBOARD_ORIGIN" even
though .env already had it set, because nothing ever read the file into the
process. Node 24's --env-file-if-exists loads it without failing when a
contributor hasn't created .env yet.
@RedStar071

Copy link
Copy Markdown
MemberAuthor

@greptile-apps

@RedStar071

Copy link
Copy Markdown
MemberAuthor

@greptile-apps

Comment threadapps/dashboard/app/modules/organizations/composables/useOrganizations.ts Outdated
removeMember() called refreshMembers() unconditionally, whose own run() call
clears the shared error state at its start. A failed removal's error was
silently wiped out the moment the follow-up refresh succeeded. Refresh only
after a successful removal, matching the pattern already used in create().
@greptile-apps

Copy link
Copy Markdown

Too many files changed for review (101 files, 100 file limit).

Bypass the limit by tagging @greptile-apps to review.

@RedStar071
RedStar071 merged commit accb010 into mainAug 13, 2026
13 of 14 checks passed
@RedStar071
RedStar071 deleted the feat/mailing-and-organizations branch August 13, 2026 12:17
RedStar071 pushed a commit that referenced this pull request Aug 13, 2026
Merges main's mailing, i18n, and organizations work (PR #28) into the
source-control branch. No overlap with the provider-neutral adapters or
apps/server routing; pnpm-lock.yaml is reconciled by hand (aube is
unavailable in this environment) using the same packages/source-control
rename applied to the previous merge.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJQxEwqaaP4aMG4E7yeaky
Sign up for freeto 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

@RedStar071