From 680d6386f3940cc66ab03436c69d7549f07c7124 Mon Sep 17 00:00:00 2001 From: Sterister Date: Fri, 8 May 2026 00:55:57 +0200 Subject: [PATCH] release(v4.8.1): SHADE_DISABLE_RATE_LIMIT env var for single-tenant deploys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- CHANGELOG.md | 44 +++++++++++++++++++ docs/DEPLOYMENT.md | 1 + packages/shade-cli/package.json | 2 +- packages/shade-core/package.json | 2 +- packages/shade-crypto-web/package.json | 2 +- packages/shade-dashboard/package.json | 2 +- packages/shade-files/package.json | 2 +- packages/shade-inbox-server/package.json | 2 +- packages/shade-inbox/package.json | 2 +- packages/shade-key-transparency/package.json | 2 +- packages/shade-keychain/package.json | 2 +- packages/shade-observability/package.json | 2 +- packages/shade-observer/package.json | 2 +- packages/shade-proto/package.json | 2 +- packages/shade-recovery/package.json | 2 +- packages/shade-sdk/package.json | 2 +- packages/shade-server/package.json | 2 +- packages/shade-server/src/standalone.ts | 23 +++++++++- .../shade-server/tests/rate-limit.test.ts | 42 ++++++++++++++++++ packages/shade-storage-encrypted/package.json | 2 +- packages/shade-storage-indexeddb/package.json | 2 +- packages/shade-storage-postgres/package.json | 2 +- packages/shade-storage-sqlite/package.json | 2 +- packages/shade-streams/package.json | 2 +- packages/shade-transfer/package.json | 2 +- packages/shade-transport-bridge/package.json | 2 +- packages/shade-transport-webrtc/package.json | 2 +- packages/shade-transport/package.json | 2 +- packages/shade-widgets/package.json | 2 +- 29 files changed, 134 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fd1eec..4046bbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,50 @@ All notable changes to Shade are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [4.8.1] — 2026-05-08 — `SHADE_DISABLE_RATE_LIMIT` env var for single-tenant deploys + +The standalone server's `routes.ts` and `inbox-server`'s +`createInboxRoutes` already accepted a `disableRateLimit?: boolean` +option, but the standalone entry just didn't read it from environment. +Self-hosted single-tenant deploys (Prism's relay is a typical case — +only Prism PC clients + their paired browsers) tripped the +`REGISTER_LIMIT` (5/hour per IP) every dev iteration: ~6 pair attempts +in an hour from the same IP plus the sidecar's register call killed +the dev loop until the bucket refilled (~1 token per 12 minutes). + +Reported by Prism. Two-line plumbing fix: `standalone.ts` now reads +`SHADE_DISABLE_RATE_LIMIT=1` and forwards `disableRateLimit` to both +`createPrekeyRoutes` and `createInboxRoutes`. + +### Added + +#### `@shade/server` +- `SHADE_DISABLE_RATE_LIMIT=1` env var disables IP rate-limits on every + prekey + inbox route in `standalone.ts`. Logged as a `WARN` on startup + (`SHADE_DISABLE_RATE_LIMIT=1 — IP rate limits OFF on prekey + inbox + routes`) so operators see it in stderr/log aggregation. +- **Single-tenant deployments only** — multi-tenant relays must leave + this unset. The rate-limit defends multi-tenant relays against abuse; + flipping it off is appropriate for self-hosted single-team setups + where every caller is a known client. Documented in + [`docs/DEPLOYMENT.md`](./docs/DEPLOYMENT.md) under "Environment variable + reference". + +### Tests +- `packages/shade-server/tests/rate-limit.test.ts` — the existing + "register endpoint rate-limits per IP" test verifies the default-on + path; a new sister test exercises + `createPrekeyServer({ disableRateLimit: true })` and confirms 12 + consecutive register calls from the same IP all return 200 (no 429). + The env-var → option conversion in `standalone.ts` is a one-liner + verified by inspection. + +### Migration + +None. Default is unchanged (rate limits stay ON). Self-hosted +single-tenant operators add `SHADE_DISABLE_RATE_LIMIT=1` to their +deployment env to flip it off. + ## [4.8.0] — 2026-05-08 — Sender-fingerprint attribution + `Inbox.start()` race fix Two unblocking changes for first-contact flows. First, the relay now diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 7b500e5..5bd31e8 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -81,6 +81,7 @@ Tables will be created automatically with the `shade_server_*` prefix, so they c | `SHADE_CLEANUP_INTERVAL_HOURS` | `24` | Cleanup cycle interval | | `SHADE_LOG_LEVEL` | `info` | `debug` / `info` / `warn` / `error` | | `SHADE_OTEL_ENABLED` | unset | Set to `1`/`true` to enable OpenTelemetry tracing on `withTracer()`-configured deployments. See [`observability.md`](./observability.md). | +| `SHADE_DISABLE_RATE_LIMIT` | unset | Set to `1` to disable IP rate-limits on every prekey + inbox route. **Single-tenant deployments only** — multi-tenant relays must leave this unset to keep the abuse defenses on. | ## Health and observability diff --git a/packages/shade-cli/package.json b/packages/shade-cli/package.json index 4c8b846..0f4113a 100644 --- a/packages/shade-cli/package.json +++ b/packages/shade-cli/package.json @@ -1,6 +1,6 @@ { "name": "@shade/cli", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/cli.ts", "bin": { diff --git a/packages/shade-core/package.json b/packages/shade-core/package.json index 4989a27..ebae657 100644 --- a/packages/shade-core/package.json +++ b/packages/shade-core/package.json @@ -1,6 +1,6 @@ { "name": "@shade/core", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-crypto-web/package.json b/packages/shade-crypto-web/package.json index 2c2fbfc..845ea27 100644 --- a/packages/shade-crypto-web/package.json +++ b/packages/shade-crypto-web/package.json @@ -1,6 +1,6 @@ { "name": "@shade/crypto-web", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-dashboard/package.json b/packages/shade-dashboard/package.json index c9042d4..e69b8f0 100644 --- a/packages/shade-dashboard/package.json +++ b/packages/shade-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@shade/dashboard", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "scripts": { "dev": "vite", diff --git a/packages/shade-files/package.json b/packages/shade-files/package.json index 1823cb5..05524ee 100644 --- a/packages/shade-files/package.json +++ b/packages/shade-files/package.json @@ -1,6 +1,6 @@ { "name": "@shade/files", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-inbox-server/package.json b/packages/shade-inbox-server/package.json index 0ebbc6e..6c6ee65 100644 --- a/packages/shade-inbox-server/package.json +++ b/packages/shade-inbox-server/package.json @@ -1,6 +1,6 @@ { "name": "@shade/inbox-server", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-inbox/package.json b/packages/shade-inbox/package.json index b34930d..d21385a 100644 --- a/packages/shade-inbox/package.json +++ b/packages/shade-inbox/package.json @@ -1,6 +1,6 @@ { "name": "@shade/inbox", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-key-transparency/package.json b/packages/shade-key-transparency/package.json index 527e820..65edc23 100644 --- a/packages/shade-key-transparency/package.json +++ b/packages/shade-key-transparency/package.json @@ -1,6 +1,6 @@ { "name": "@shade/key-transparency", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-keychain/package.json b/packages/shade-keychain/package.json index 5c4b2e9..004269a 100644 --- a/packages/shade-keychain/package.json +++ b/packages/shade-keychain/package.json @@ -1,6 +1,6 @@ { "name": "@shade/keychain", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-observability/package.json b/packages/shade-observability/package.json index 598c1c2..bc7fffc 100644 --- a/packages/shade-observability/package.json +++ b/packages/shade-observability/package.json @@ -1,6 +1,6 @@ { "name": "@shade/observability", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-observer/package.json b/packages/shade-observer/package.json index 8ea4c67..24c4506 100644 --- a/packages/shade-observer/package.json +++ b/packages/shade-observer/package.json @@ -1,6 +1,6 @@ { "name": "@shade/observer", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-proto/package.json b/packages/shade-proto/package.json index 3e165af..887452d 100644 --- a/packages/shade-proto/package.json +++ b/packages/shade-proto/package.json @@ -1,6 +1,6 @@ { "name": "@shade/proto", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-recovery/package.json b/packages/shade-recovery/package.json index 3afa9a5..a346f89 100644 --- a/packages/shade-recovery/package.json +++ b/packages/shade-recovery/package.json @@ -1,6 +1,6 @@ { "name": "@shade/recovery", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-sdk/package.json b/packages/shade-sdk/package.json index 66edf15..1d6783f 100644 --- a/packages/shade-sdk/package.json +++ b/packages/shade-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@shade/sdk", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-server/package.json b/packages/shade-server/package.json index c02735e..a4aa700 100644 --- a/packages/shade-server/package.json +++ b/packages/shade-server/package.json @@ -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", diff --git a/packages/shade-server/src/standalone.ts b/packages/shade-server/src/standalone.ts index af5213b..f983ab6 100644 --- a/packages/shade-server/src/standalone.ts +++ b/packages/shade-server/src/standalone.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 diff --git a/packages/shade-server/tests/rate-limit.test.ts b/packages/shade-server/tests/rate-limit.test.ts index 873dc33..ad34895 100644 --- a/packages/shade-server/tests/rate-limit.test.ts +++ b/packages/shade-server/tests/rate-limit.test.ts @@ -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() }); diff --git a/packages/shade-storage-encrypted/package.json b/packages/shade-storage-encrypted/package.json index 0aa3b80..b44e42a 100644 --- a/packages/shade-storage-encrypted/package.json +++ b/packages/shade-storage-encrypted/package.json @@ -1,6 +1,6 @@ { "name": "@shade/storage-encrypted", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-storage-indexeddb/package.json b/packages/shade-storage-indexeddb/package.json index 10f2e00..5c755a8 100644 --- a/packages/shade-storage-indexeddb/package.json +++ b/packages/shade-storage-indexeddb/package.json @@ -1,6 +1,6 @@ { "name": "@shade/storage-indexeddb", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-storage-postgres/package.json b/packages/shade-storage-postgres/package.json index 177934e..c34cf7f 100644 --- a/packages/shade-storage-postgres/package.json +++ b/packages/shade-storage-postgres/package.json @@ -1,6 +1,6 @@ { "name": "@shade/storage-postgres", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-storage-sqlite/package.json b/packages/shade-storage-sqlite/package.json index 033d6dd..7e395cb 100644 --- a/packages/shade-storage-sqlite/package.json +++ b/packages/shade-storage-sqlite/package.json @@ -1,6 +1,6 @@ { "name": "@shade/storage-sqlite", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-streams/package.json b/packages/shade-streams/package.json index 44a251a..ca0936a 100644 --- a/packages/shade-streams/package.json +++ b/packages/shade-streams/package.json @@ -1,6 +1,6 @@ { "name": "@shade/streams", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-transfer/package.json b/packages/shade-transfer/package.json index 26ea932..dcecc30 100644 --- a/packages/shade-transfer/package.json +++ b/packages/shade-transfer/package.json @@ -1,6 +1,6 @@ { "name": "@shade/transfer", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-transport-bridge/package.json b/packages/shade-transport-bridge/package.json index 3be7796..f94d2ea 100644 --- a/packages/shade-transport-bridge/package.json +++ b/packages/shade-transport-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@shade/transport-bridge", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-transport-webrtc/package.json b/packages/shade-transport-webrtc/package.json index 98e7df4..fe1a9d1 100644 --- a/packages/shade-transport-webrtc/package.json +++ b/packages/shade-transport-webrtc/package.json @@ -1,6 +1,6 @@ { "name": "@shade/transport-webrtc", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-transport/package.json b/packages/shade-transport/package.json index f36961c..d26a59c 100644 --- a/packages/shade-transport/package.json +++ b/packages/shade-transport/package.json @@ -1,6 +1,6 @@ { "name": "@shade/transport", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-widgets/package.json b/packages/shade-widgets/package.json index b01f67f..25aa100 100644 --- a/packages/shade-widgets/package.json +++ b/packages/shade-widgets/package.json @@ -1,6 +1,6 @@ { "name": "@shade/widgets", - "version": "4.8.0", + "version": "4.8.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts",