release(v4.8.1): SHADE_DISABLE_RATE_LIMIT env var for single-tenant deploys
Plumbing fix only — both createPrekeyRoutes and createInboxRoutes already accepted disableRateLimit; standalone.ts just didn't read the env. Now SHADE_DISABLE_RATE_LIMIT=1 turns off IP rate-limits on every prekey + inbox route, with a WARN log on startup so operators see it. Single-tenant deployments only — multi-tenant relays must leave it unset. Documented in docs/DEPLOYMENT.md. Reported by Prism: ~6 pair attempts/hour from a single dev IP + the sidecar's register call tripped the 5/hour REGISTER_LIMIT every dev iteration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@shade/server",
|
||||
"version": "4.8.0",
|
||||
"version": "4.8.1",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
|
||||
@@ -154,6 +154,20 @@ const inboxEvents = new InboxServerEvents();
|
||||
// SHADE_KT_PG_URL (or SHADE_PREKEY_PG_URL) is set, else memory.
|
||||
const kt = await maybeCreateKT();
|
||||
|
||||
// V4.8.1 — `SHADE_DISABLE_RATE_LIMIT=1` turns off the IP-based
|
||||
// register/replenish/fetch token-buckets on every prekey + inbox
|
||||
// route. INTENDED FOR SELF-HOSTED SINGLE-TEAM (DEV / SINGLE-TENANT)
|
||||
// DEPLOYMENTS ONLY — the rate-limit defends multi-tenant relays
|
||||
// against abuse, so a public/shared deployment must leave this
|
||||
// unset. Without it, the existing 5/hour REGISTER_LIMIT etc. apply
|
||||
// unchanged.
|
||||
const disableRateLimit = process.env.SHADE_DISABLE_RATE_LIMIT === '1';
|
||||
if (disableRateLimit) {
|
||||
logger.warn(
|
||||
'SHADE_DISABLE_RATE_LIMIT=1 — IP rate limits OFF on prekey + inbox routes. Use only for single-tenant deployments.',
|
||||
);
|
||||
}
|
||||
|
||||
// Compose the full app: metrics middleware + health + metrics + prekey routes
|
||||
const app = new Hono();
|
||||
app.use('*', metricsMiddleware());
|
||||
@@ -164,10 +178,17 @@ app.route(
|
||||
'/',
|
||||
createPrekeyRoutes(store, crypto, {
|
||||
events,
|
||||
disableRateLimit,
|
||||
...(kt ? { keyTransparency: kt } : {}),
|
||||
}),
|
||||
);
|
||||
app.route('/', createInboxRoutes(inboxStore, crypto, { events: inboxEvents }));
|
||||
app.route(
|
||||
'/',
|
||||
createInboxRoutes(inboxStore, crypto, {
|
||||
events: inboxEvents,
|
||||
disableRateLimit,
|
||||
}),
|
||||
);
|
||||
|
||||
// V3.7 transport-bridge — SSE / long-poll / WS fallbacks for the inbox.
|
||||
// Held as a top-level reference so the WebSocket handler can be passed to
|
||||
|
||||
@@ -102,6 +102,48 @@ describe('Rate limiting integration with routes', () => {
|
||||
expect(results.filter((s) => s === 429).length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
// V4.8.1 — `SHADE_DISABLE_RATE_LIMIT=1` in standalone.ts is plumbed
|
||||
// through to `createPrekeyServer({ disableRateLimit })`. This test
|
||||
// covers the "what happens when the flag is true" path; the env-var
|
||||
// → option conversion in standalone.ts is a one-liner verified by
|
||||
// inspection.
|
||||
test('register endpoint allows >5/hour from a single IP when disableRateLimit is set', async () => {
|
||||
const app = createPrekeyServer({
|
||||
crypto,
|
||||
store: new MemoryPrekeyStore(),
|
||||
disableRateLimit: true,
|
||||
});
|
||||
|
||||
async function doRegister(addressSuffix: number) {
|
||||
const identity = await generateIdentityKeyPair(crypto);
|
||||
const body: any = {
|
||||
address: `user${addressSuffix}`,
|
||||
identitySigningKey: Buffer.from(identity.signingPublicKey).toString('base64'),
|
||||
identityDHKey: Buffer.from(identity.dhPublicKey).toString('base64'),
|
||||
signedPreKey: {
|
||||
keyId: 1,
|
||||
publicKey: Buffer.from(crypto.randomBytes(32)).toString('base64'),
|
||||
signature: Buffer.from(crypto.randomBytes(64)).toString('base64'),
|
||||
},
|
||||
};
|
||||
const signed = await signPayload(crypto, identity.signingPrivateKey, body);
|
||||
return app.request('/v1/keys/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'x-forwarded-for': '203.0.113.1' },
|
||||
body: JSON.stringify(signed),
|
||||
});
|
||||
}
|
||||
|
||||
const results: number[] = [];
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const res = await doRegister(i);
|
||||
results.push(res.status);
|
||||
}
|
||||
// No 429 anywhere — the limit is OFF.
|
||||
expect(results.filter((s) => s === 429).length).toBe(0);
|
||||
expect(results.filter((s) => s === 200).length).toBe(12);
|
||||
});
|
||||
|
||||
test('rate limit returns Retry-After header', async () => {
|
||||
const app = createPrekeyServer({ crypto, store: new MemoryPrekeyStore() });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user