fix(pier): widen returning identities, warm the Redis store, end stateless listen streams, refuse personal tokens without a TokenID

- Identity.fingerprint covers scopes, both subjects, the ceiling and the
  token kind; a stateful lookup or stateless hit whose fingerprint changed
  re-filters the server with the event diff under its own mutex (A-WR-04)
- Mount warms a Redis store in the background with a 1 s to 30 s backoff
  and every request kicks a pending open without waiting, so stateless-only
  instances subscribe to fan-out (A-WR-05)
- a stateless subscriptions/listen POST re-verifies its bearer every
  recheck interval; an LRU-evicted stateless server closes its sessions
  (A-WR-06)
- resolvePersonalToken refuses a subject with TokenID 0, and the
  pact.MCPPersonalTokenResolver contract says one resolver per application,
  false means unauthorized and TokenID keys the binding (A-WR-09, A-IN-03)
This commit is contained in:
Jakub Zych
2026-10-23 15:59:54 +02:00
parent c1c0c77ddc
commit 240457291f
10 changed files with 217 additions and 39 deletions

View File

@@ -232,10 +232,12 @@ func (s *redisStore) Subscribe(ctx context.Context, fn func(Event)) error {
// (route:list and tests never dial) and a missing redis.url is a 503 on the
// first request, logged once, not a boot failure (cooler's rule).
type lazyStore struct {
open func(ctx context.Context) (Store, error)
mu sync.Mutex
store Store
warned atomic.Bool
open func(ctx context.Context) (Store, error)
mu sync.Mutex
store Store
warned atomic.Bool
ready atomic.Bool // the store is open
opening atomic.Bool // a kick is opening it
}
// get returns the store, opening it on the first call and retrying on a
@@ -254,9 +256,43 @@ func (l *lazyStore) get(ctx context.Context) (Store, error) {
return nil, err
}
l.store = s
l.ready.Store(true)
return s, nil
}
// kick opens the store in the background when it is not open yet and no
// other kick is opening it; the caller never waits.
func (l *lazyStore) kick() {
if l.ready.Load() || !l.opening.CompareAndSwap(false, true) {
return
}
go func() {
defer l.opening.Store(false)
_, _ = l.get(context.Background())
}()
}
// warm opens the store at mount in the background, retrying with a backoff
// from one second doubling to thirty, so the event subscription exists
// before traffic arrives (A-WR-05). It returns once the store is open or
// ctx ends; each failure is logged once per backoff step.
func (l *lazyStore) warm(ctx context.Context) {
backoff := time.Second
for {
if _, err := l.get(ctx); err == nil {
return
} else if ctx.Err() == nil {
logger().Warn("pier: session store not open yet, retrying", "retry_in", backoff.String(), "error", err)
}
select {
case <-ctx.Done():
return
case <-time.After(backoff):
}
backoff = min(backoff*2, 30*time.Second)
}
}
func (l *lazyStore) Put(ctx context.Context, id string, rec Record, ttl time.Duration) error {
s, err := l.get(ctx)
if err != nil {