diff --git a/.changeset/25329-rate-limit-cache-warning-remedy.md b/.changeset/25329-rate-limit-cache-warning-remedy.md new file mode 100644 index 00000000000..55c2615c8ee --- /dev/null +++ b/.changeset/25329-rate-limit-cache-warning-remedy.md @@ -0,0 +1,12 @@ +--- +'@objectstack/plugin-auth': patch +--- + +fix: the degraded rate-limit warning names the remedy that boots: a shared `ICacheService` registered as `cache` + +Clause-②: no +Metadata-protocol: none + +With no `cache` service registered, the lazy counter store falls back to a per-process store and logs one warning. That warning told a multi-node deployment to use "Redis via @objectstack/service-cache". But `CacheServicePlugin` throws at `init` when `adapter: 'redis'` is set, so an operator who followed it got a boot failure. + +The warning now names the working path: an `ICacheService` backed by a store every node reaches, registered as the `cache` service (`ctx.registerService('cache', impl)`). It no longer names a bundled adapter, so the text cannot go stale when one ships. The ADR-0069 D2 reference and the per-caller impact suffix are unchanged. The same applies to every counter built on `createLazyCounterStore`: better-auth's rate limit, the per-number OTP send budget, the SMS quotas and the runtime's inbound and endpoint limiters. diff --git a/packages/plugins/plugin-auth/src/otp-send-guard.ts b/packages/plugins/plugin-auth/src/otp-send-guard.ts index 8f7f9ee53e7..67d3c74f818 100644 --- a/packages/plugins/plugin-auth/src/otp-send-guard.ts +++ b/packages/plugins/plugin-auth/src/otp-send-guard.ts @@ -44,7 +44,7 @@ * send is checked: * * - the kernel `cache` service when one is registered — shared across nodes - * iff the cache is (Redis via `@objectstack/service-cache`); + * iff the registered `cache` service is backed by a store every node reaches; * - a host-supplied better-auth `secondaryStorage`, when the host wired one * deliberately (adapted by {@link counterStoreFromKv}); * - otherwise a bounded per-process store — still enforced, per node, and diff --git a/packages/plugins/plugin-auth/src/rate-limit-storage.test.ts b/packages/plugins/plugin-auth/src/rate-limit-storage.test.ts index deffe3e2189..6bf0a06b450 100644 --- a/packages/plugins/plugin-auth/src/rate-limit-storage.test.ts +++ b/packages/plugins/plugin-auth/src/rate-limit-storage.test.ts @@ -176,6 +176,13 @@ describe('createLazyCacheRateLimitStorage — no cache service at all (#4772 acc const msg = logger.warn.mock.calls[0][0] as string; expect(msg).toContain('no `cache` service registered at all'); expect(msg).toContain('per-process store'); + // The remedy is the path that boots today: a shared ICacheService + // registered as `cache`. It never names @objectstack/service-cache as the + // Redis remedy — that package's Redis adapter refuses to start. + expect(msg).toContain("registered as the `cache` service (ctx.registerService('cache', impl))"); + expect(msg).not.toContain('@objectstack/service-cache'); + expect(msg).not.toMatch(/redis/i); + expect(msg).toContain('(ADR-0069 D2)'); expect(logger.info).not.toHaveBeenCalled(); }); diff --git a/packages/plugins/plugin-auth/src/rate-limit-storage.ts b/packages/plugins/plugin-auth/src/rate-limit-storage.ts index 488b56e551e..109b2dec52d 100644 --- a/packages/plugins/plugin-auth/src/rate-limit-storage.ts +++ b/packages/plugins/plugin-auth/src/rate-limit-storage.ts @@ -217,8 +217,9 @@ export function createLazyCounterStore(opts: LazyCounterStoreOptions): () => Pro degradedWarned = true; opts.logger?.warn?.( `${prefix} ${opts.subject}: no cache service to count in — falling back to a per-process store. ` + - 'This deployment has no `cache` service registered at all; a multi-node deployment needs a shared cache ' + - '(Redis via @objectstack/service-cache) or each node enforces the limit independently (ADR-0069 D2)' + + 'This deployment has no `cache` service registered at all; a multi-node deployment needs a shared cache — ' + + 'an ICacheService backed by a store every node reaches, registered as the `cache` service ' + + "(ctx.registerService('cache', impl)) — or each node enforces the limit independently (ADR-0069 D2)" + (opts.degradedImpact ? `. ${opts.degradedImpact}` : ''), ); } diff --git a/packages/runtime/src/security/inbound-rate-limit.test.ts b/packages/runtime/src/security/inbound-rate-limit.test.ts index 2dc5d7904e1..bf1712e13ae 100644 --- a/packages/runtime/src/security/inbound-rate-limit.test.ts +++ b/packages/runtime/src/security/inbound-rate-limit.test.ts @@ -311,7 +311,10 @@ describe('degradation is announced once, with consequence and remedy (ADR-0069 D expect(message).toMatch(/^\[dispatcher\]/); expect(message).toMatch(/inbound rate-limit buckets/); expect(message).toMatch(/MULTIPLIED BY the number of nodes/); - expect(message).toMatch(/Redis via @objectstack\/service-cache/); + // The remedy names the path that works today (a shared ICacheService + // registered as `cache`), never a bundled adapter that refuses to boot. + expect(message).toMatch(/registerService\('cache', impl\)/); + expect(message).not.toMatch(/@objectstack\/service-cache/); }); it('announces the healthy bind once instead of staying silent about it', async () => {