@@ -8,53 +8,39 @@ import (
88 "fmt"
99 "sync"
1010 "time"
11+
12+ "github.com/stacklok/toolhive/pkg/authserver/storage"
1113)
1214
1315// dcrStaleAgeThreshold is the age beyond which a cached DCR resolution is
1416// considered stale and logged as such by higher-level wiring. The store itself
1517// does not expire or evict entries — RFC 7591 client registrations are
16- // long-lived and are only purged by explicit RFC 7592 deregistration. This
17- // threshold is consumed by Step 2g observability logs introduced in the next
18- // PR in the DCR stack (sub-issue C, #5039); 5042 only defines the constant
19- // so the consumer can land without a cross-PR cycle.
20- //
21- //nolint:unused // consumed by lookupCachedResolution in #5039
18+ // long-lived and are only purged by explicit RFC 7592 deregistration.
2219const dcrStaleAgeThreshold = 90 * 24 * time .Hour
2320
24- // DCRKey is the canonical lookup key for a DCR resolution. The tuple is
25- // designed so a future Redis-backed store can serialise it into a single key
26- // segment (Phase 3) without redefining the canonical form. ScopesHash rather
27- // than the raw scope slice is used so the key is comparable and order-
28- // insensitive.
29- type DCRKey struct {
30- // Issuer is *this* auth server's issuer identifier — the local issuer
31- // of the embedded authorization server that performed the registration,
32- // NOT the upstream's. The cache is keyed by this value because two
33- // different local issuers registering against the same upstream are
34- // distinct OAuth clients and must not share credentials. The upstream's
35- // issuer is used only for RFC 8414 §3.3 metadata verification inside
36- // the resolver and is not part of the cache key.
37- Issuer string
38-
39- // RedirectURI is the redirect URI registered with the upstream
40- // authorization server. Lives on the local issuer's origin since it is
41- // where the upstream sends the user back to us after authentication.
42- RedirectURI string
21+ // DCRKey is a re-export of storage.DCRKey, kept as a package-local alias so
22+ // existing runner-side callers continue to compile against runner.DCRKey
23+ // while the canonical definition lives in pkg/authserver/storage. The
24+ // canonical form (and its ScopesHash constructor) MUST live in a single place
25+ // so any future Redis backend hashes keys identically to the in-memory
26+ // backend; see storage.DCRKey for the field documentation.
27+ type DCRKey = storage.DCRKey
4328
44- // ScopesHash is the SHA-256 hex digest of the sorted scope list.
45- // See scopesHash in dcr.go for the canonical form.
46- ScopesHash string
47- }
48-
49- // DCRCredentialStore caches RFC 7591 Dynamic Client Registration resolutions
29+ // dcrResolutionCache caches RFC 7591 Dynamic Client Registration resolutions
5030// keyed by the (Issuer, RedirectURI, ScopesHash) tuple. Implementations must
5131// be safe for concurrent use.
5232//
53- // The store is an in-memory cache of long-lived registrations — it is not a
54- // durable store, and entries are never expired or evicted by the store
55- // itself. Callers are responsible for invalidating entries when the
56- // underlying registration is revoked (e.g., via RFC 7592 deregistration).
57- type DCRCredentialStore interface {
33+ // This is a runner-internal cache of *DCRResolution values; it is distinct
34+ // from the persistent storage.DCRCredentialStore (which holds *DCRCredentials
35+ // and is the durable contract sub-issue 3 wires the resolver to use). Naming
36+ // them differently keeps the two interfaces unambiguous to readers and grep
37+ // tooling while both exist during the Phase 3 migration.
38+ //
39+ // The cache is in-memory and holds long-lived registrations — entries are
40+ // never expired or evicted by the cache itself. Callers are responsible for
41+ // invalidating entries when the underlying registration is revoked (e.g.,
42+ // via RFC 7592 deregistration).
43+ type dcrResolutionCache interface {
5844 // Get returns the cached resolution for key, or (nil, false, nil) if the
5945 // key is not present. An error is returned only on backend failure.
6046 Get (ctx context.Context , key DCRKey ) (* DCRResolution , bool , error )
@@ -66,17 +52,21 @@ type DCRCredentialStore interface {
6652 Put (ctx context.Context , key DCRKey , resolution * DCRResolution ) error
6753}
6854
69- // NewInMemoryDCRCredentialStore returns a thread-safe in-memory
70- // DCRCredentialStore intended for tests and single-replica development
55+ // newInMemoryDCRResolutionCache returns a thread-safe in-memory
56+ // dcrResolutionCache intended for tests and single-replica development
7157// deployments. Production deployments should use the Redis-backed store
7258// introduced in Phase 3, which addresses the cross-replica sharing,
7359// durability, and cross-process coordination gaps documented below.
7460//
7561// Entries are retained for the process lifetime; there is no TTL and no
76- // background cleanup goroutine. The usual concern about an unbounded
77- // cache leaking memory does not apply here because the key space is
78- // bounded by the operator-configured upstream count, and this
79- // implementation is not the production answer.
62+ // background cleanup goroutine. Growth is bounded by upstream count ×
63+ // distinct scope sets ever registered for each upstream during the
64+ // process lifetime — for a stable configuration this collapses to the
65+ // upstream count, but rotating scope sets (operator-driven scope
66+ // changes, or upstream scopes_supported rotations re-derived by the
67+ // resolver) accumulate stale entries that survive until restart. This
68+ // implementation is not the production answer; the Redis backend
69+ // introduced in Phase 3 mitigates the rotation case via SetEX TTL.
8070//
8171// What this enables: serialises Get/Put against a single in-process map so
8272// concurrent callers within one authserver process see a consistent view of
@@ -97,23 +87,23 @@ type DCRCredentialStore interface {
9787// that side, the loser becomes orphaned. The
9888// resolveDCRCredentials-level singleflight in dcr.go only deduplicates
9989// within one process.
100- func NewInMemoryDCRCredentialStore () DCRCredentialStore {
101- return & inMemoryDCRCredentialStore {
90+ func newInMemoryDCRResolutionCache () dcrResolutionCache {
91+ return & inMemoryDCRResolutionCache {
10292 entries : make (map [DCRKey ]* DCRResolution ),
10393 }
10494}
10595
106- // inMemoryDCRCredentialStore is the default DCRCredentialStore backed by a
96+ // inMemoryDCRResolutionCache is the default dcrResolutionCache backed by a
10797// plain map guarded by sync.RWMutex. Modelled on
10898// pkg/authserver/storage/memory.go but stripped of TTL bookkeeping — DCR
10999// resolutions are long-lived.
110- type inMemoryDCRCredentialStore struct {
100+ type inMemoryDCRResolutionCache struct {
111101 mu sync.RWMutex
112102 entries map [DCRKey ]* DCRResolution
113103}
114104
115- // Get implements DCRCredentialStore .
116- func (s * inMemoryDCRCredentialStore ) Get (_ context.Context , key DCRKey ) (* DCRResolution , bool , error ) {
105+ // Get implements dcrResolutionCache .
106+ func (s * inMemoryDCRResolutionCache ) Get (_ context.Context , key DCRKey ) (* DCRResolution , bool , error ) {
117107 s .mu .RLock ()
118108 defer s .mu .RUnlock ()
119109
@@ -128,13 +118,13 @@ func (s *inMemoryDCRCredentialStore) Get(_ context.Context, key DCRKey) (*DCRRes
128118 return & cp , true , nil
129119}
130120
131- // Put implements DCRCredentialStore .
121+ // Put implements dcrResolutionCache .
132122//
133123// A nil resolution is rejected rather than silently no-oped: a caller
134124// passing nil would otherwise get a successful return, observe a miss on
135125// the next Get, and have no error trail to debug from. Failing loudly at
136126// the boundary makes such bugs visible at the first call.
137- func (s * inMemoryDCRCredentialStore ) Put (_ context.Context , key DCRKey , resolution * DCRResolution ) error {
127+ func (s * inMemoryDCRResolutionCache ) Put (_ context.Context , key DCRKey , resolution * DCRResolution ) error {
138128 if resolution == nil {
139129 return fmt .Errorf ("dcr: resolution must not be nil" )
140130 }
0 commit comments