Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |

---
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**.
55 changes: 54 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down Expand Up @@ -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 <session token>`; 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

Expand Down Expand Up @@ -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.

29 changes: 29 additions & 0 deletions docs/self-host.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading