Files
xarmian 0877b260c1 feat(redis): namespace every Redis keyspace from one shared config value (BUG-2724)
Every Redis key and channel Pad uses was flat — pad:events:, pad:event_seq,
pad:watchevents*, pad:session:* — so two Pad installations pointed at one
Redis endpoint cross-feed each other's notifications and merge each other's
session-presence registries. Different logical DB numbers do not help:
Redis pub/sub is not namespaced by DB at all.

The exposure is narrow but real. Delivery is filtered per caller on user
id, and user ids are per-installation UUIDs, so cross-feed needs the same
id in both installations — a CLONED database, such as a staging
environment restored from a production dump. For that case it is a genuine
cross-tenant leak: foreign sessions listed in the picker, and a private
push deliverable across installations.

Fixed the way internal/watchevents' existing ruling demanded: not by one
package growing a prefix the others lack, but through internal/redisns —
one value parsed in cmd/pad/cmd_server.go and passed into all three
constructors. The three cannot drift because there is nothing to drift
from, and the operator rule is stateable in one sentence for every
keyspace.

PAD_REDIS_NAMESPACE defaults to empty, which reproduces the historical
names byte for byte, so an existing deployment keeps addressing its own
replay buffers, counters and presence entries across the upgrade. Tests
assert both directions per keyspace — present under the namespace AND
absent under the historical names — because an implementation that wrote
both would still cross-feed while passing a one-directional test.

Namespaces are validated at startup, and a colon is rejected specifically:
it is Pad's own separator, so namespace "a:events" would build
pad:a:events:<ws> and collide with installation "a"'s channel —
reintroducing the cross-feed through the mechanism meant to fix it.

Names are built through a function rather than assembled from a literal at
each site, and redisns' doc says why: "pad:" also begins Pad's OAuth SCOPE
values (pad:read / pad:write / pad:admin) in four files, so a grep-driven
prefix sweep would break authorization.

Not included, deliberately: hash tags for Redis Cluster. BUG-2724's trail
recommended shipping them alongside on cost-sharing grounds; that premise
is falsified by publishScript, which spans four keys in one EVAL and fails
CROSSSLOT exactly as presence's MGET does. There is no cheap half, and no
cluster client here to exercise tagged keys against, so they would ship
untested by construction. Cluster stays documented as unsupported and the
future unit is named on the trail.

Renaming is a CUTOVER for the buses (the seq and epoch keys carry
Last-Event-ID meaning, so connected clients resync) and free for presence
(90s TTL). Both stated in docs/deployment.md and at the constructors.

Claude-Session: https://claude.ai/code/session_01JVDBKbgn3Xt7ndW1YoYd8X
2026-08-22 01:41:31 +00:00
..