// Package authn answers one question for spec.sr.ht — "who is making this
// request?" — and builds the git provenance that answers "who made this write?"
// forever after.
//
// The design has exactly two principals that carry authority:
//
// - the human owner, recognised by the shared `sr.ht.unified-login.v1`
// cookie carrying the instance's [sr.ht] owner-name;
// - an agent, recognised by a bearer token.
//
// Everything else is anonymous. Single-user does not mean "no authorization";
// it relocates it onto agents, which is why Principal distinguishes those two
// and nothing finer. The boundary that bounds an agent's damage is the refs rule
// (agents may only write proposals/*), and that lives in gitx; nothing here
// replaces it.
//
// # Two agent credential planes
//
// An agent is recognised on either of two planes, and Principal.Plane says
// which:
//
// - PlaneLocal — spec's own agent_token row: one instance-wide shared secret,
// hashed at rest, with no owner, no expiry and no grants. This is v1's
// credential and it keeps working exactly as it did.
// - PlaneInstance — a tokens.sr.ht working token: signed by the instance,
// expiring, owned by a meta.sr.ht account, and carrying a grant set
// (ActionPropose, ActionRead). It is validated by sr-ht-ecore's bearer
// package and is present only when the instance config has a
// [tokens.sr.ht] section; where it does not, the plane is absent and the
// local one is the only door, which is a supported configuration.
//
// The instance plane is tried first and falls back to the local one on exactly
// two refusals — see Resolver.resolveInstanceToken, where the reasoning lives.
//
// The two planes are not interchangeable, and the difference this package has to
// carry is that only one of them has an owner. The local token is a secret with
// no user behind it; an instance token names one. Principal.Owner therefore
// keeps meaning "the human this agent acts for", which on this single-owner
// instance is always [sr.ht] owner-name — a token belonging to anybody else is
// refused rather than admitted as a second identity, because every consumer of
// that field (the provenance committer, the refs rule's principal kind, the
// coreauth AuthContext) is written for one human.
//
// Grants are orthogonal to the refs rule and to provenance, and replace neither.
// A grant says what an instance token was minted for; the refs rule still says
// where an agent may point a ref, and provenance is still mandatory on every
// agent write, on both planes.
//
// The cookie and the bearer planes are deliberately asymmetric:
//
// - A cookie that is missing, forged, expired or unreadable yields an
// anonymous principal and never an error. Browsing must keep working.
// - A bearer token that is present but unknown, revoked or corrupt is a hard
// failure. An agent that presented an explicit credential must not be
// silently downgraded to a reader; it would then fail confusingly at the
// write instead of clearly at the door.
//
// Provenance is the other half. Agent identity and session ID are mandatory on
// every agent write, and are recorded in the commit itself so that a plain
// `git log` on any clone carries the audit trail:
//
// Author: claude-code/spec-writer (for bigbes) <agent@spec.srht.bigb.es>
// Committer: bigbes <bigbes@gmail.com>
//
// Add storage model section
//
// X-Agent-Session: 8fb9c9a4-b078-4af1-89eb-d97c522f9921
// X-Agent-Base: 1f0c1d1a1e2b3c4d5e6f708192a3b4c5d6e7f809
//
// A write missing either field is rejected rather than defaulted: a commit
// stamped with a guessed session is worse than no commit, because it launders
// unattributable output as attributed.
//
// This package owns no storage and opens no connections. The agent_token table
// lives in db/, injected through the TokenStore interface declared here; the
// "user" row an instance token's owner resolves to is reached through
// UserLookup, and the tokens.sr.ht validator through BearerValidator. authn
// never imports db and never calls core-go's auth.LookupUser itself, so the
// dependency arrow keeps pointing downward and the whole package stays testable
// with no Postgres and no daemon to talk to.
package authn
import (
"errors"
"sourcecraft.dev/bigbes/sr-ht-ecore/bearer"
)
// Sentinel errors. Callers compare with errors.Is. The split that matters is
// permanent (the credential is bad — 401/403) versus transient (the store could
// not answer — 503); IsAuthFailure draws it.
var (
// ErrNoToken is returned when a bearer credential was expected but the
// request carried no Authorization header, or one in another scheme.
ErrNoToken = errors.New("no agent token presented")
// ErrUnknownToken is the contract a TokenStore must honour: it is what
// LookupAgentToken returns (possibly wrapped) when no row matches the
// presented hash. Any other error is treated as transient, so a Postgres
// outage reads as "try again", never as "your token is bad".
ErrUnknownToken = errors.New("unknown agent token")
// ErrInvalidToken marks a presented credential that is malformed, or a
// stored row whose hash does not actually match what was presented.
ErrInvalidToken = errors.New("invalid agent token")
// ErrRevokedToken marks a token that resolved to a real row which has been
// revoked. Distinct from ErrUnknownToken so operators can tell "you are
// using a token I deliberately killed" from "that token never existed".
ErrRevokedToken = errors.New("revoked agent token")
// ErrNotAgent is returned when agent provenance is demanded of a principal
// that is not an agent — the human push path builds no trailers.
ErrNotAgent = errors.New("principal is not an agent")
// ErrMissingProvenance marks an agent write that omits the agent identity,
// the session ID, or the base revision. Mandatory on every agent write; the
// design is explicit that these are not defaultable.
ErrMissingProvenance = errors.New("missing agent provenance")
// ErrInvalidProvenance marks provenance whose values are present but
// unusable: control characters or angle brackets that would forge a git
// signature line or inject an extra trailer, an over-long field, or a base
// revision that is not a hex object name.
ErrInvalidProvenance = errors.New("invalid agent provenance")
// ErrMissingConfig is returned by InstanceFromConfig when the instance
// config lacks a key the provenance identities are built from. It is a
// startup failure, not a request failure.
ErrMissingConfig = errors.New("missing instance config key")
// ErrNotInstanceOwner marks a valid tokens.sr.ht working token whose owner is
// somebody other than the instance owner. 403: the credential verifies and
// the holder is who they say they are, there is simply nothing on this
// single-owner instance to grant them. Deliberately not an IsAuthFailure —
// presenting it again will not help and neither will logging in.
ErrNotInstanceOwner = errors.New("token owner is not the instance owner")
// ErrMissingGrant marks an instance token that authenticated fine but does
// not carry the action being attempted. 403, for the reason
// bearer.ErrForbidden is: what the holder needs is a wider grant, not another
// login.
//
// It is raised by Principal.Authorize, at the layer that knows the action —
// never by the resolver, which runs before the router and so knows none.
ErrMissingGrant = errors.New("token does not grant this action")
)
// IsAuthFailure reports whether err is a permanent credential failure — the
// caller should answer 401/403 — as opposed to a transient backend failure,
// which should answer 503 and be retried. Everything not in this set is
// transient by definition, which is the fail-closed direction: a store outage
// never reads as a valid credential.
//
// The instance plane's two permanent refusals are in the set for the same
// reason the local plane's are: a signature that does not verify and a token
// tokens.sr.ht has withdrawn are both "this credential is bad", whichever door
// it was presented at. bearer.ErrUnavailable is pointedly absent — see
// StatusFor, which is what surfaces should map with.
func IsAuthFailure(err error) bool {
return errors.Is(err, ErrNoToken) ||
errors.Is(err, ErrUnknownToken) ||
errors.Is(err, ErrInvalidToken) ||
errors.Is(err, ErrRevokedToken) ||
errors.Is(err, bearer.ErrInvalid) ||
errors.Is(err, bearer.ErrRevoked)
}