> ## Documentation Index
> Fetch the complete documentation index at: https://docs.suchi.page/llms.txt
> Use this file to discover all available pages before exploring further.

# HTTP and security boundaries

> Middleware order, authentication and local two-factor boundaries, the share-only listener, token, role, identity, object and browser-origin gates, pairing, demo identities, and blob delivery.

Part of the [architecture guides](https://docs.suchi.page/architecture.md). This page owns request handling and the access gates every route passes through. Confirm contracts against source and tests.

## Listeners and middleware

`distro/app/serve_middleware.go: buildHTTPHandler` wraps main-listener requests,
including rate-limit rejections, with request IDs, security headers, access logs
and metrics. Limiters run before body limits, authentication, token policy,
Fetch Metadata checks and the normalized mux. Credential/pairing, main-share and
demo POST traffic use distinct limiter instances.

An optional share listener is a separate socket and fresh mux, not a Host-header
branch. `api.ShareSurface` captures `PublicOnly` and the configured origin at
registration. The main mux uses `PUBLIC_URL` and serves Network and Public links;
the share mux uses `SHARE_PUBLIC_URL`, serves only valid Public links, and
registers only recipient metadata/password/download routes. Every other route is
404\. Its middleware is limited to request ID, security headers, body limit,
Fetch Metadata, access log, metrics and an isolated share limiter—no session
authentication or API-token resolution. Unknown, malformed, Network-only,
expired and revoked bearers are indistinguishable on that socket. Request Host,
source IP and forwarded headers never select visibility or origin.

Credential/pairing and demo POST limiters classify the **decoded URL path**
consistently with API trailing-slash normalization: ordinary, slash and escaped
aliases receive the same policy without rewriting the request. Main share routes
use matched mux patterns. Test the assembled handlers.
Local password hashing and verification share a process-wide two-slot bound
across account creation, shares, login, token exchange, and account-email
reauthentication. Login takes a slot before the account lookup, so overload
fails quickly and identically for known and unknown accounts while limiting
Argon2 working memory. Email change returns the same retryable saturation
contract without entering the writer.

## Local two-factor authentication

`plugins/local-auth` owns the complete local-password second-factor contract.
`distro/app` supplies the existing `.decrypt-key` sealer and registers the
management routes only when local auth owns sign-in; OIDC and demo assembly do
not expose them. `local_auth_totp` stores only an AEAD-sealed 20-byte TOTP seed,
the exact last accepted counter, the consecutive-failure count, the block
deadline, and enable time. Ten 120-bit recovery codes are
shown once; `local_auth_recovery_codes` stores only their SHA-256 digests.

Local JSON login, the server-rendered login form, and password-to-token exchange
all accept `two_factor_code`. Password verification happens first. For JSON
only, a correct password with no factor returns `two_factor_required`; wrong
passwords, wrong/replayed TOTP values, and wrong recovery codes share
`invalid_credentials`. TOTP follows RFC 6238 with HMAC-SHA1, six digits, a
30-second period, and a ±1-step window. Accepting a TOTP advances its exact
counter; accepting a recovery code deletes its digest. Factor consumption and
session/token issuance share the writer transaction, so a failed issuance does
not burn a credential. Password-only issuance rechecks the absence of a factor
in that transaction; enablement that commits during credential preparation
therefore prevents the stale exchange from creating a session or token (JSON
routes answer 401, the HTML form redirects with `two_factor_required`).
Enablement deletes the user's pending mobile pairing in its own transaction,
and `core/api` token and pairing creation recheck the caller's `sessions` row in
the writer transaction, so a session revoked by enablement cannot mint a
durable credential after the fact.

Factor failures are counted on the account row, so restarts do not reset them.
Only a request with the correct password reaches the factor check, so strangers
cannot lock an account. The first five failures are free; each later failure
blocks the factor for 30 seconds, doubling up to one hour, and a blocked request
is rejected with `Retry-After` before any code is compared or counted. At 100
consecutive failures (NIST SP 800-63B §5.2.2) the factor locks with
`two_factor_locked` until an administrator or offline reset removes it. Any
accepted code clears the count. The bounded Argon2 worker still limits password
attempts separately.

`POST /api/users/me/email` asks for the same factor after its password check
through `api.Server.VerifySecondFactor`, which `distro/app` wires to local auth.
The route reports a taken address first, then spends the code in its own
transaction; a later email-change failure leaves that code spent.

`GET /api/account/two-factor` and all lifecycle POSTs require a live local
browser session, reject bearer tokens and demo identities, use
`Cache-Control: no-store`, and require exact same-origin provenance for writes.
Enrollment rechecks the password and returns a ten-minute AEAD-sealed,
user-and-session-bound enrollment token plus the Base32 seed and `otpauth` URI;
the seed is not stored until a valid current TOTP enables it. Enable,
recovery-code regeneration, and disable rotate every browser session while
planting one replacement cookie. API/mobile tokens remain valid; enable returns
`active_api_tokens` so the user can review tokens minted before the factor
existed. Each operation and its retained audit event commit together.

An administrator can reset another user's factor only by reauthenticating with
the administrator's own password and factor. Reset deletes the target's factor,
recovery codes, and browser sessions in one audited transaction while preserving
API/mobile tokens. Host recovery uses `suchi auth reset-two-factor --email
EMAIL` with the server stopped. It takes the normal archive lock, does not
decrypt the seed or require `.decrypt-key`, and writes a retained system-actor
audit event.

## Access gates

These gates remain distinct:

* **Authentication:** `core/auth` checks OIDC before local auth and anonymous
  demo last. OIDC delegates Suchi's exact API-token shape to local auth; invalid
  credentials cannot fall back to session cookies. Both OIDC paths require
  signed verified-email claims, but returning users resolve only by exact issuer
  and subject. Browser callbacks alone may provision an unused verified email;
  direct Bearer authentication is resolution-only. Existing unbound rows require
  an explicit live-session synchronization rather than email fallback.
* **Token authority:** `distro/app/serve_token_policy.go` lists routes/scopes for local API
  tokens and restricted demo scratch identities. Unlisted routes stay session-only,
  including for admins. Public `documents:read`, `documents:write` and `events:read`
  are separate grants; feed-only tokens cannot read documents. Token management
  requires browser/OIDC identity; admin-token metrics is an explicit exception.
  Catalog `DELETE /api/tags/` is deliberately absent from the token allowlist:
  an admin-role write token cannot use it. The registered handler rechecks the
  current writer principal and selected system before deleting. Verify the
  assembled policy with `go test ./distro/app -run
  TestTokenPolicyProtectsDocumentAndAccountHandlers`.
* **Roles/capabilities:** scopes grant neither admin status nor feature capabilities.
  User creation rechecks admin authority under the writer after password hashing.
  Updates read current role/capabilities there, reject self-disable/last-admin loss,
  store no hidden member grants on admins and derive revocations from effective
  access. Promotion must not quarantine still-authorized resources, nor demotion
  revive hidden grants. Local mailbox/share/view revocations and required
  capability audit writes share the transaction; either cascade or audit failure
  rolls back the transition. `core/audit.RecordInTx` captures outcomes without
  logging/fan-out; `Record.Emit` logs failures or delivers committed events
  afterward. Persisted rows retain writer order; post-commit sink delivery can
  interleave.
  Disable/membership OAuth invalidation occurs only after successful commit, with
  the flow-store mutex spanning commit/invalidation in writer-to-flow lock order.
  Rollback preserves flows; re-enable/readmission/start cannot overtake invalidation.
  Logs, sink delivery and supervisor reload stay outside the writer. See
  [Account safeguards](https://docs.suchi.page/permissions.md#account-safeguards).
* **Account identity:** `distro/app` supplies one mode evaluator to self
  responses and both mutation paths. Password verification, random replacement
  session preparation, and OIDC network exchange happen before the writer.
  Inside one write transaction the handler rechecks the canonical user, mode,
  initiating session, and local password hash or OIDC binding; updates the same
  user ID; queues rerenders for that user's folder layouts that use the exact
  `owner` template variable; rotates browser sessions when identity
  changed; and writes the retained audit event. Watched-folder ownership is a
  numeric user ID, so an email change neither rewrites its setting nor reloads
  the watcher. A no-change OIDC check performs no rotation or false audit write.
  The replacement cookie and audit sinks run after commit. API/mobile tokens are
  intentionally not rotated.
* **Objects:** `core/authz` checks existence, explicit/token system boundary,
  active-user system entry, then owner/admin/direct/group permission. Admins bypass
  membership/ACL only. Lists/counts/search/similarity filter system and visibility
  in SQL before pagination.
* **Browser origin:** cookie mutations reject sibling-origin `same-site` and
  `cross-site` Fetch Metadata requests. Headerless anonymous form logins require
  a same-host `Origin`; headerless JSON credential routes require
  `application/json`. Account-email and two-factor management POSTs additionally
  require exact same-origin provenance and a live browser session, with no
  bearer-header exemption. CORS alone is insufficient.
  The SPA CSP admits only the discovered OIDC authorization endpoint's HTTP(S)
  origin beyond `self` for form submissions so that the same-origin
  reauthentication POST can follow its provider redirect; non-SPA responses
  remain `form-action 'self'`.

## Filing-system scope and first-party clients

Sessions select `?system=CODE`, defaulting to original system 1; token binding is
its default and ceiling. Unqualified numeric objects resolve their actual system.
There is no server last-selected context or accessible-system fallback. Unknown,
inaccessible and mismatched targets return indistinguishable 404-style errors.
Writer-held authorization refreshes actor/role, membership and token existence/
binding before mutation or replay. Membership removal atomically revokes shares,
tokens and pairings; re-addition does not revive them. See [Permissions](https://docs.suchi.page/permissions.md).

First-party clients probe public `/api/handshake` before credentials. `/api/whoami`
reports actual scopes/build identity and, for tokens, the bound filing-system ID,
name, and possibly-empty code. It reports only whether public-share ingress is
available, never either listener address. `/api/token/` creates tokens,
`/api/login` creates sessions; mobile reads use existing ACL-filtered API
projections. [HTTP API](https://docs.suchi.page/api.md) owns handshake, payload, scope and mobile-read
details.

## Mobile pairing

`core/api/mobile_pairing.go` transfers a browser/OIDC identity to a device.
Creation binds the short-lived code to configured `PUBLIC_URL`, never forwarded
hosts. The server permits HTTPS or its narrow local-HTTP pairing boundary
(localhost/loopback/private literal IPs, excluding broader local DNS/link-local
egress allowances); Companion permits that HTTP subset only in debug builds.
Origin validation runs before replacing pending state; storage keeps only the
code digest, owner/system, fallback
name and expiry. Exchange validates optional device naming before consumption,
then consumes the code, rechecks enabled-user system entry and calls `TokenIssuer`
in one writer transaction. Token digest, fixed read/write scopes and server-selected
`mobile_pairing` provenance commit together. Failure restores consumability;
a lost successful response needs a new pairing. Cancellation matches user/system/
digest; no secret is logged or cacheable. [Pairing API](https://docs.suchi.page/api.md#post-apimobilepairing)
owns timing, QR, names and connected-app projection contracts.

## Demo identities, blob delivery, and secrets

Demo anonymous cookies are signed/stateless; first write obtains a digest-backed
session bounded by scratch TTL through CLI-injected local issuance. Local auth
preserves restricted scratch scope on API and direct resources, without a separate
credential store. Demo Calendar is a local intelligence-handler exception: only
accepted `demo-corpus` dates with source/trash SQL filtering before pagination;
no extraction/review/chat grant. The generic seeder validates manifest facts and
inserts them with document/job; the sibling demo repository owns content. See
[Demo instance](https://docs.suchi.page/demo-instance.md).

Preview/download aliases share `core/ui` object checks, Reveal, ETags and CSP.
Trashed bytes require owner/admin; ordinary ACL readers and public shares cannot
fetch them. Thumbnails have a separate live-document handler. Sensitivity is a
Reveal posture, not another access boundary: authorized API reads may contain
text; untouched `raw=1` downloads require an admin session.

Seal mailbox secrets, OAuth caches, PDF passwords, model keys, and TOTP seeds;
digest session IDs, API tokens, and recovery codes. Exclude document bodies and
credentials/share bearers from audit.
Logs/metrics use matched route patterns, propagated outward through request copies.
[Privacy](https://docs.suchi.page/privacy.md) owns the user-facing egress/data contract.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.