Skip to content

Commit ff4e36d

Browse files
committed
fix(plugin-auth): degraded rate-limit warning names the cache registration that boots
The per-process fallback warning prescribed "Redis via @objectstack/service-cache", whose Redis adapter throws at init. It now names the working path: a shared ICacheService registered as `cache` (ctx.registerService('cache', impl)), and promises no bundled adapter. ADR-0069 D2 reference and degradedImpact suffix kept. Pins flipped in plugin-auth and runtime; otp-send-guard doc comment corrected. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy
1 parent 925fd07 commit ff4e36d

5 files changed

Lines changed: 27 additions & 4 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
---
4+
5+
fix: the degraded rate-limit warning names the remedy that boots: a shared `ICacheService` registered as `cache`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
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.
11+
12+
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.

‎packages/plugins/plugin-auth/src/otp-send-guard.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
* send is checked:
4545
*
4646
* - the kernel `cache` service when one is registered — shared across nodes
47-
* iff the cache is (Redis via `@objectstack/service-cache`);
47+
* iff the registered `cache` service is backed by a store every node reaches;
4848
* - a host-supplied better-auth `secondaryStorage`, when the host wired one
4949
* deliberately (adapted by {@link counterStoreFromKv});
5050
* - otherwise a bounded per-process store — still enforced, per node, and

‎packages/plugins/plugin-auth/src/rate-limit-storage.test.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,13 @@ describe('createLazyCacheRateLimitStorage — no cache service at all (#4772 acc
176176
const msg = logger.warn.mock.calls[0][0] as string;
177177
expect(msg).toContain('no `cache` service registered at all');
178178
expect(msg).toContain('per-process store');
179+
// The remedy is the path that boots today: a shared ICacheService
180+
// registered as `cache`. It never names @objectstack/service-cache as the
181+
// Redis remedy — that package's Redis adapter refuses to start.
182+
expect(msg).toContain("registered as the `cache` service (ctx.registerService('cache', impl))");
183+
expect(msg).not.toContain('@objectstack/service-cache');
184+
expect(msg).not.toMatch(/redis/i);
185+
expect(msg).toContain('(ADR-0069 D2)');
179186
expect(logger.info).not.toHaveBeenCalled();
180187
});
181188

‎packages/plugins/plugin-auth/src/rate-limit-storage.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,9 @@ export function createLazyCounterStore(opts: LazyCounterStoreOptions): () => Pro
217217
degradedWarned = true;
218218
opts.logger?.warn?.(
219219
`${prefix} ${opts.subject}: no cache service to count in — falling back to a per-process store. ` +
220-
'This deployment has no `cache` service registered at all; a multi-node deployment needs a shared cache ' +
221-
'(Redis via @objectstack/service-cache) or each node enforces the limit independently (ADR-0069 D2)' +
220+
'This deployment has no `cache` service registered at all; a multi-node deployment needs a shared cache — ' +
221+
'an ICacheService backed by a store every node reaches, registered as the `cache` service ' +
222+
"(ctx.registerService('cache', impl)) — or each node enforces the limit independently (ADR-0069 D2)" +
222223
(opts.degradedImpact ? `. ${opts.degradedImpact}` : ''),
223224
);
224225
}

‎packages/runtime/src/security/inbound-rate-limit.test.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,10 @@ describe('degradation is announced once, with consequence and remedy (ADR-0069 D
311311
expect(message).toMatch(/^\[dispatcher\]/);
312312
expect(message).toMatch(/inbound rate-limit buckets/);
313313
expect(message).toMatch(/MULTIPLIED BY the number of nodes/);
314-
expect(message).toMatch(/Redis via @objectstack\/service-cache/);
314+
// The remedy names the path that works today (a shared ICacheService
315+
// registered as `cache`), never a bundled adapter that refuses to boot.
316+
expect(message).toMatch(/registerService\('cache', impl\)/);
317+
expect(message).not.toMatch(/@objectstack\/service-cache/);
315318
});
316319

317320
it('announces the healthy bind once instead of staying silent about it', async () => {

0 commit comments

Comments
 (0)