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/authchecks 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.golists routes/scopes for local API tokens and restricted demo scratch identities. Unlisted routes stay session-only, including for admins. Publicdocuments:read,documents:writeandevents:readare separate grants; feed-only tokens cannot read documents. Token management requires browser/OIDC identity; admin-token metrics is an explicit exception. CatalogDELETE /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 withgo 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.RecordInTxcaptures outcomes without logging/fan-out;Record.Emitlogs 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. - Account identity:
distro/appsupplies 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 exactownertemplate 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/authzchecks 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-siteandcross-siteFetch Metadata requests. Headerless anonymous form logins require a same-hostOrigin; headerless JSON credential routes requireapplication/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 beyondselffor form submissions so that the same-origin reauthentication POST can follow its provider redirect; non-SPA responses remainform-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.
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 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
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 accepteddemo-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.
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 owns the user-facing egress/data contract.