// Package auth carries the authenticated principal across the HTTP stack // so handlers do not re-decode the Pocket-ID session cookie on each call. // The Pocket-ID verification lives in services/admin-api/internal/auth; the // approval-service accepts a Principal already stamped by the gateway or // its own future middleware wrap (Phase 2). package auth import ( "context" "errors" ) type ctxKey int const ( principalKey ctxKey = iota sessionKey ) // Principal is the subject identifier (Pocket-ID `sub`) of the browser user // driving the approval. Attestations will carry it as `performed_by.sub`. type Principal struct { Sub string Groups []string } // ErrNoPrincipal is returned by MustPrincipal when the request context did // not carry an authenticated principal. var ErrNoPrincipal = errors.New("auth: request is not authenticated") // WithPrincipal returns a copy of ctx carrying p. func WithPrincipal(ctx context.Context, p Principal) context.Context { return context.WithValue(ctx, principalKey, p) } // FromContext returns the Principal stamped by WithPrincipal. func FromContext(ctx context.Context) (Principal, bool) { p, ok := ctx.Value(principalKey).(Principal) return p, ok } // MustFromContext is a convenience that returns ErrNoPrincipal when no // principal is present. func MustFromContext(ctx context.Context) (Principal, error) { p, ok := FromContext(ctx) if !ok || p.Sub == "" { return Principal{}, ErrNoPrincipal } return p, nil } // WithSession binds the Pocket-ID `sid` claim to ctx. Approval-sessions // scope themselves to the browser login via this value. func WithSession(ctx context.Context, sid string) context.Context { return context.WithValue(ctx, sessionKey, sid) } // SessionFromContext returns the browser sid carried in ctx. func SessionFromContext(ctx context.Context) (string, bool) { s, ok := ctx.Value(sessionKey).(string) return s, ok && s != "" }