Security
Takhtabush is self-hosted authentication and data software. This page explains
the model and the operator’s responsibilities. To report a vulnerability, use
the private advisory link in SECURITY.md, not a public issue.
Authentication
- Sign-in is an email magic link. The token is stored only as a hash and expires after 15 minutes.
- A successful sign-in creates an HTTP-only
sidsession cookie withSameSite=LaxandSecurewhen the request is HTTPS. - Browser mutations use a double-submit CSRF token: a readable
csrfcookie plus thex-csrf-tokenheader. Bearer requests are exempt. - Sessions are first-class: list them, revoke one, or revoke all from settings.
- Auth routes are rate limited in D1 with a
Retry-Afterresponse.
Single tenancy
The default is one tenant. The first account to sign in becomes the owner.
| Setting | Effect |
|---|---|
OWNER_EMAIL |
Pins the only allowed address |
ALLOWED_EMAILS |
Complete allowlist for new accounts when set |
| neither | The first user claims ownership; later addresses are blocked |
OWNER_INVITE_TOKEN |
Requires a matching invite value on every magic-link request |
SETUP_TOKEN |
Enables /setup to create or recover the owner without email |
Existing accounts keep working unless OWNER_EMAIL pins a different owner.
The gate is applied when a magic link is requested and again when it is
verified.
This release is not a multi-tenant product. Content is owned by a user, and
study progress is stored per (user_id, card_id). A future shared workspace
must add an explicit workspace scope to content; do not treat the current
owner_id convention as tenant isolation.
Secrets
Set these as Worker secrets, never in a committed file:
| Secret | Required | Purpose |
|---|---|---|
SESSION_SECRET |
Yes | Hashes session and magic-link tokens; signs OAuth consent cookies. Use 32+ random bytes. |
SETUP_TOKEN |
For first-run setup | Creates or recovers the owner |
OWNER_INVITE_TOKEN |
Optional | Extra invite gate on magic-link requests |
AI_GATEWAY_TOKEN |
Optional | Clef through AI Gateway |
SESSION_SECRET must be stable across deploys or every session is invalidated.
Rotate SETUP_TOKEN after setup if you do not need recovery, or keep it strong.
API keys and OAuth
- API keys are shown once and stored as a hash. Each key has a name, scopes, an optional expiry, and a last-used timestamp.
- Keys can call only tools covered by their scopes.
tools/listis filtered to the same grant. - OAuth grants, refresh tokens, and registered clients live in
OAUTH_KV. - The
CACHEKV namespace is separate and holds only derived, stale-tolerant values (review streak, global tag summary). It is never used for authorization; KV is eventually consistent. See the D1 cost model §11. - OAuth consent uses
__Host-cookies; test the full flow over HTTPS. - OAuth and API keys resolve to the same user and the same MCP scope model.
Media
- R2 object keys are owner-prefixed.
- Uploads start with a metadata row; the object is written only after the owner is checked.
- Reads go through
/media/:id, which checks ownership before streaming. - The Worker does not expose R2 directly.
MCP
- The MCP package depends on
@caly/sharedand the injectedMcpServicesinterface. It never imports D1 or R2. - Mutating tools write to
mcp_audit_events. - There is no code execution tool in v0.1.
MCP_DEV_NO_AUTHis ignored unlessENVIRONMENT=development.
Hardening checklist
- Set a unique
SESSION_SECRETwith 32+ random bytes. - Set
OWNER_EMAILor a completeALLOWED_EMAILSlist. - Remove or rotate
SETUP_TOKENafter the owner is created. - Use HTTPS and connect MCP clients to the canonical origin.
- Restrict Cloudflare dashboard and API-token access.
- Keep D1 and R2 permissions scoped to this Worker.
- Review active sessions and API keys periodically.
- Watch the daily cleanup job for expired auth artifacts and orphaned media.
- Keep dependencies current; Dependabot opens weekly PRs.
Out of scope
Misconfiguration of your own deployment, volumetric denial of service, social
engineering, and automated scanner reports without a demonstrated impact are
out of scope. See SECURITY.md for the full policy.