package service
import (
"context"
"errors"
"fmt"
"sourcecraft.dev/bigbes/sr-ht-spec/authn"
"sourcecraft.dev/bigbes/sr-ht-spec/db"
)
// AgentTokenLookup is the one db/ method the token adapter needs. It is an
// interface rather than a *db.Store so the error mapping below — the part that
// actually carries a contract — can be tested against a fake instead of a
// Postgres instance. *db.Store satisfies it.
type AgentTokenLookup interface {
AgentTokenByHash(ctx context.Context, hash []byte) (*db.AgentToken, error)
}
// TokenStore adapts db/'s agent_token queries to authn.TokenStore. It exists
// because authn must not import db: authn declares the sliver of persistence it
// needs, and service/ — the layer that is allowed to know about both — wires
// them together.
//
// The whole of the adaptation is the error contract, and it is not cosmetic.
// authn's contract is that "no such token" is an error satisfying
// errors.Is(err, authn.ErrUnknownToken) and that everything else is transient.
// db/ spells the same condition ErrNotFound, which authn has never heard of, so
// an unmapped pass-through would make an unknown token look like a Postgres
// outage: a 503 telling an agent to retry a credential that will never work.
type TokenStore struct {
lookup AgentTokenLookup
}
// NewTokenStore wires a db.Store in as authn's TokenStore.
func NewTokenStore(lookup AgentTokenLookup) *TokenStore {
return &TokenStore{lookup: lookup}
}
// TokenStore returns the adapter the resolver authenticates agents through.
func (s *Service) TokenStore() *TokenStore { return s.tokens }
// LookupAgentToken implements authn.TokenStore.
//
// A revoked row is returned rather than refused: authn is what turns it into a
// refusal, so the refusal can say "revoked" instead of "unknown" and an
// operator can tell a token they deliberately killed from one that never
// existed. Any other failure is returned wrapped and unclassified, which authn
// reads as transient — the fail-closed direction, since a store outage must
// never read as a valid credential.
func (t *TokenStore) LookupAgentToken(ctx context.Context, hash []byte) (authn.AgentToken, error) {
if t.lookup == nil {
return authn.AgentToken{}, errors.New("service: TokenStore has no backing store")
}
row, err := t.lookup.AgentTokenByHash(ctx, hash)
if err != nil {
if errors.Is(err, db.ErrNotFound) {
return authn.AgentToken{}, fmt.Errorf("%w: no agent_token row matches the presented token",
authn.ErrUnknownToken)
}
return authn.AgentToken{}, fmt.Errorf("service: look up agent token: %w", err)
}
if row == nil {
// db/ never returns (nil, nil); a store that did would otherwise
// authenticate a nil row as a valid token.
return authn.AgentToken{}, errors.New("service: agent token lookup returned no row and no error")
}
return authn.AgentToken{
ID: int64(row.ID),
Name: row.Name,
Hash: row.Hash,
Created: row.Created,
Revoked: row.Revoked,
}, nil
}