package authn import ( "context" "errors" "strings" "sourcecraft.dev/bigbes/sr-ht-core/auth" ) // ErrInvalidToken is the sentinel wrapped by every *permanent* credential // rejection: a malformed/expired/forged token, a username mismatch, or a // revoked token. Callers (e.g. the remotesapi interceptors) map errors.Is(err, // ErrInvalidToken) to an authentication failure (HTTP 401 / gRPC // Unauthenticated). A resolution error that does NOT wrap ErrInvalidToken is a // *transient* backend failure (meta.sr.ht unreachable, database error) and // should be surfaced as "try again later" (HTTP 500 / gRPC Unavailable), never // as a hard credential rejection — this mirrors core-go's auth.OAuth2, which // distinguishes its temporary-error path from an invalid-token path. var ErrInvalidToken = errors.New("authn: invalid or expired credentials") // MetaBackend abstracts the two meta.sr.ht-backed lookups the resolvers need: // mirroring a user into the local "user" table and checking whether a token has // been revoked. The production implementation (coreMetaBackend) delegates to // core-go, which reads the database and config from the request context; tests // swap in an in-memory stub so they need neither Postgres nor the network. type MetaBackend interface { // LookupUser fills out with the user identified by username, mirroring the // profile from meta.sr.ht into the local database on first sight. Mirrors // core-go's auth.LookupUser semantics. LookupUser(ctx context.Context, username string, out *auth.AuthContext) error // IsRevoked reports whether the personal access token with the given sha512 // hash (and, for OAuth clients, clientID) has been revoked on meta.sr.ht. // Mirrors core-go's auth.LookupTokenRevocation. IsRevoked(ctx context.Context, username string, hash [64]byte, clientID string) (bool, error) } // coreMetaBackend is the production MetaBackend: it forwards to core-go, whose // implementations read database.ForContext / config.ServiceName from ctx and // (on a local miss) fetch the profile from meta.sr.ht over internal GraphQL. type coreMetaBackend struct{} func (coreMetaBackend) LookupUser(ctx context.Context, username string, out *auth.AuthContext) error { return auth.LookupUser(ctx, username, out) } func (coreMetaBackend) IsRevoked(ctx context.Context, username string, hash [64]byte, clientID string) (bool, error) { return auth.LookupTokenRevocation(ctx, username, hash, clientID) } // meta is the backend used by the resolution functions. It defaults to the real // meta.sr.ht implementation; tests reassign it (white-box) and restore it. var meta MetaBackend = coreMetaBackend{} // SetMetaBackend swaps the package-level meta backend used by the resolvers and // returns a function that restores the previous one. It is a wiring/test seam: // integration tests living in OTHER packages (e.g. remoteapi) need to inject an // in-memory MetaBackend so they can exercise the full auth stack without a live // meta.sr.ht or the internal-network trust it requires. Production code never // calls it, and it is not safe for concurrent use — a test installs a backend, // runs, and restores it via the returned func (typically with t.Cleanup). func SetMetaBackend(b MetaBackend) (restore func()) { prev := meta meta = b return func() { meta = prev } } // equalUsername reports whether two SourceHut usernames refer to the same user, // ignoring a leading "~" (the canonical-name sigil) and ASCII case. func equalUsername(a, b string) bool { return strings.EqualFold(strings.TrimPrefix(a, "~"), strings.TrimPrefix(b, "~")) }