From 2b0b79f09ffe31943edb9caee73774a820e950ed Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 9 Oct 2026 12:05:19 +0000 Subject: [PATCH] docs: document auth email, rate limits, and SMTP self-host setup Align architecture.md, self-host.md, and README with merged Better Auth work (verification, password reset, origin hardening, managed-AI limits). Co-Authored-By: Cursor Co-authored-by: drferdiiskandar --- README.md | 3 ++- docs/README.md | 2 +- docs/architecture.md | 55 +++++++++++++++++++++++++++++++++++++++++++- docs/self-host.md | 29 +++++++++++++++++++++++ 4 files changed, 86 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4155bae4..3fecc2a9 100644 --- a/README.md +++ b/README.md @@ -515,7 +515,8 @@ Production uses `backup-prod.sh` with a systemd timer, seven-day rotation, and m | `E2B_API_KEY` / `DAYTONA_API_KEY` / `DAYTONA_API_URL` / `DAYTONA_TARGET` / `BOX_API_KEY` / `BOX_API_URL` | Computer-provider credentials. | | `COMPOSIO_API_KEY` | Optional plugins / connectors. | | `EXPO_PUBLIC_API_URL` / `SENTRABOT_WEB_URL` | Mobile production origin and Electron web-URL override. | -| `SMTP_URL` / `VAPID_*` | Optional email and push. | +| `SMTP_URL` / `SMTP_FROM` | Outbound SMTP for auth verification and password-reset mail; omit and those flows log a warning instead of failing boot. | +| `VAPID_*` | Optional web push. | | `OTEL_EXPORTER_OTLP_ENDPOINT` / `LOG_LEVEL` | Observability. | --- diff --git a/docs/README.md b/docs/README.md index d26be444..7a6a14e7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -43,4 +43,4 @@ Public product copy lives in `apps/docs` (Mintlify) and `apps/site`. Implementat ## Verification date -This map was reconciled against the repository on **2026-09-03**. +This map was reconciled against the repository on **2026-10-09**. diff --git a/docs/architecture.md b/docs/architecture.md index 6c831ca9..49bb7cb6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,7 +1,7 @@ # Sentra Bot runtime architecture This document describes what the repository does today, verified against the code on -2026-09-03. It is the single source of truth for the runtime topology; other documents link here +2026-10-09. It is the single source of truth for the runtime topology; other documents link here instead of restating it. Product *requirements* (what the system must do) live in `docs/requirements/`. The assessment behind the 2026-09-02 topology decisions lives in `docs/superpowers/plans/2026-09-02-convergence-directive.md`. @@ -47,6 +47,44 @@ packages/bot-templates (66 role packages), infra/updater (Compose sidecar). | Sandbox lifecycle | `SandboxProvider` adapters (`packages/adapters/src/sandbox-factory.ts`); Docker computers through `infra/sandboxes/supervisor` (bearer token, loopback by default). Computer state and control/execution leases live in `computers` and `computer_execution_leases`. | | Background jobs | `packages/adapter-kit/src/background-jobs.ts`. Every job carries a `jobKey`, so redelivery replaces instead of duplicating. The reconciler re-enqueues queued runs, expired leases, near-due routines, and expired control leases. When a worker dies holding a job, the reconciler's keyed re-enqueue makes Graphile (0.17) mark the locked row permanently failed and insert a fresh one, so a new worker picks the work up within its poll interval; recovery never waits for Graphile's 4-hour stale-lock window (`packages/adapters/src/graphile-restart.postgres.test.ts`). | | Durable state | PostgreSQL via Prisma (`packages/db/prisma/schema.prisma`); files under `DATA_DIR`; secrets encrypted with `ENCRYPTION_KEY` in the `secrets` table | +| Identity, sessions, auth email | Better Auth in `packages/auth` (`createAuth`), mounted at `/api/auth/*` from `apps/api` | + +## Authentication and transactional email + +Identity is [Better Auth](https://www.better-auth.com/) with the Prisma adapter, email/password, +the `bearer` plugin (mobile sends `Authorization: Bearer `; the API synthesizes the +session cookie before resolving the session), and the `organization` plugin with team-management +paths blocked in `blockedAuthPaths`. Signup policy is deployment state (`signupsEnabled`, +`signupAllowlist`) enforced in an auth hook on sign-up, not frozen at process start. + +**Email verification** is enabled with `sendOnSignUp: true` and +`autoSignInAfterVerification: true`. `requireEmailVerification` stays off so first login is not +blocked before the user clicks the link; `user.emailVerified` is set when the token is consumed. +Risky-action gates on `emailVerified` are planned but not wired everywhere yet (see +`docs/plans/user-journey-audit-2026-09-10.md`). + +**Password reset** uses Better Auth's reset flow; a successful reset revokes other sessions +(`revokeSessionsOnPasswordReset`). The web app exposes forgot-password and `/reset-password`. + +**Transactional email** goes through the vendor-neutral `EmailSender` seam +(`packages/adapter-kit`) and the SMTP adapter (`packages/adapters/src/smtp-email.ts`) when +`SMTP_URL` is set. `deliverAuthEmail` never throws: without SMTP the API still boots, auth +responses stay generic (no address enumeration), and the API logs `[auth] …` via +`onEmailUnavailable`. Copy is Indonesian plain text + HTML (`packages/auth/src/auth-emails.ts`). + +**Rate limiting:** Better Auth's limiter is pinned on (`rateLimit.enabled` defaults true) so +protection does not depend on Better Auth's own `NODE_ENV` detection. Vitest sets +`AUTH_RATE_LIMIT_ENABLED=false` indirectly via `VITEST=true`. Storage is in-process memory — fine +for a single API instance; multi-instance self-hosts would need shared `secondaryStorage`. +Managed AI is separate: `POST /v1/managed-ai/responses` applies a fixed window of 60 requests per +minute per client IP (`apps/api/src/managed-ai.ts`) before authentication, to cap spend from a +misbehaving runtime. + +**Origins:** `trustedOrigins` for Better Auth are `WEB_ORIGIN`, `BETTER_AUTH_URL`, plus +`sentrabot://` (production mobile) and, outside production, Expo dev origins. CORS uses +`isTrustedOrigin` in `apps/api/src/app.ts`, which parses real `Origin` serializations and rejects +userinfo/path tricks; do not reuse that helper for CSRF decisions where a missing `Origin` must +not imply trust. ## Run lifecycle @@ -245,3 +283,18 @@ boundary is needed. - Consequence: dependency changes in sentrabot must never be validated by installing at the Monorepo root, and vice versa; the Monorepo lockfile no longer pins anything for sentrabot. +### 2026-10-09 — auth email, explicit rate limits, and origin hardening + +- Problem: operators had no documented path for verification/reset email, Better Auth rate limits + could silently follow library `NODE_ENV` heuristics, `/v1/managed-ai/responses` had no spend cap, + and permissive CORS/origin checks were easy to misconfigure on production. +- Decision: wire SMTP through `EmailSender`, enable verification and password reset without + `requireEmailVerification`, pin Better Auth `rateLimit.enabled`, add managed-AI IP limiting, and + tighten `isTrustedOrigin` / `trustedOrigins` (exact `sentrabot://`, no dev Expo origins in + production). +- Evidence: `packages/auth/src/index.ts`, `apps/api/src/app.ts`, `apps/api/src/managed-ai.ts`, + `apps/web/src/pages/ResetPassword.tsx`. +- Migration consequence: set `SMTP_URL` (and usually `SMTP_FROM`) on any deployment that should + deliver auth mail; without it, sign-up and reset still work from the API's perspective but no + message is sent and logs record the gap. + diff --git a/docs/self-host.md b/docs/self-host.md index 0e11d274..e7bbd0bc 100644 --- a/docs/self-host.md +++ b/docs/self-host.md @@ -74,6 +74,35 @@ API_URL=https://app.example.com Cookies and CORS follow those origins. `SIGNUPS_ENABLED` / `SIGNUP_ALLOWLIST` seed the initial deployment settings. After initialization, the deployment owner's Settings values are the effective signup policy. +### Transactional email (verification and password reset) + +Sign-up can send a verification link and the sign-in page can request a password reset. Both require +outbound SMTP. Without it the API still starts; auth endpoints respond normally and the UI shows +honest "belum tersedia" style messaging, while the API logs warnings such as +`[auth] SMTP_URL is not configured; auth email was not sent`. + +```env +# smtp://user:pass@host:587 or smtps://user:pass@host:465 +SMTP_URL=smtp://bot:secret@mail.example.com:587 +SMTP_FROM=no-reply@example.com +``` + +`SMTP_FROM` defaults to the user in `SMTP_URL` when credentials are embedded. Use a relay that +allows that sender. Links expire in about one hour (Better Auth defaults). Email copy is +Indonesian (`packages/auth/src/auth-emails.ts`). + +**Troubleshooting** + +| Symptom | Check | +| --- | --- | +| No verification mail after sign-up | `SMTP_URL` set in the **API/worker** environment (Compose: `.env` loaded by both services); API logs for `[auth]` delivery errors | +| Reset request succeeds but no mail | Same as above; confirm firewall egress to the SMTP port | +| Auth works locally but CORS fails on HTTPS | `BETTER_AUTH_URL`, `WEB_ORIGIN`, and `API_URL` must match the browser's public origin exactly | +| Mobile cannot sign in against production | Mobile sends `Origin: sentrabot://`; production trusts only that exact scheme, not suffixed variants | + +Login is **not** blocked when `emailVerified` is false; treat verified email as a trust signal for +future gated actions, not as a hard gate today. + Optional: ```env