Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/25329-rate-limit-cache-warning-remedy.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion packages/plugins/plugin-auth/src/otp-send-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions packages/plugins/plugin-auth/src/rate-limit-storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand Down
5 changes: 3 additions & 2 deletions packages/plugins/plugin-auth/src/rate-limit-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}` : ''),
);
}
Expand Down
5 changes: 4 additions & 1 deletion packages/runtime/src/security/inbound-rate-limit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Loading