mirror of
https://github.com/hanzoai/dataroom.git
synced 2026-10-30 18:41:07 +00:00
* KV-optional: in-process backend when KV_URL unset Make dataroom run WITH and WITHOUT external Hanzo KV, uniform with commerce infra/kv.go. KV_URL unset -> in-process MemoryKV (single-replica correct: cache, rate-limit, tus upload locks, export/download job stores, digest queues all work with no external datastore). KV_URL set -> external Hanzo KV over ioredis (multi-replica HA). Malformed KV_URL fails CLOSED (throws) -- never a silent in-process fallback. - lib/kv/url.ts: resolveKvUrl -- fail-closed parse, kv:// brand scheme -> redis:// wire scheme, password redaction in errors. - lib/kv/memory-kv.ts: in-process ioredis-compatible store (strings+TTL, zset, set, hash, list, pipeline) -- the exact surface dataroom uses. - lib/kv/select.ts: the one backend selector (mirrors NewKVClient/FromURL). - lib/redis.ts: use createKvClient() for redis + lockerRedisClient; drop the redis://localhost:6379 silent default (the WITHOUT-KV break) and vestigial REDIS_URL; Upstash-compat shims unchanged. - Tests (node --test, dep-free): 25 pass proving selection + in-process correctness incl NX lock mutual-exclusion, TTL eviction, zset ordering, pipeline shape, fail-closed URL, cross-instance isolation (multi-replica split-brain contract). * kv: warn once when falling back to in-process (non-silent split-brain) Emit a one-time startup warning when KV_URL is unset so the WITHOUT-KV mode is never silent — a multi-replica deploy with KV_URL unset splits sessions, rate-limit windows and tus locks across replicas (single-replica only). --------- Co-authored-by: Hanzo Dev <[email protected]>
80 lines
3.1 KiB
TypeScript
80 lines
3.1 KiB
TypeScript
/**
|
|
* KV_URL resolution — the ONE place that decides the KV backend.
|
|
*
|
|
* Contract (uniform across every Hanzo KV consumer; mirrors commerce infra/kv.go):
|
|
* - KV_URL unset/empty ⇒ returns null ⇒ caller uses the in-process backend
|
|
* (Base/in-memory). This is the WITHOUT-KV mode, which
|
|
* is single-replica correct.
|
|
* - KV_URL set ⇒ returns a normalized `redis://`/`rediss://` DSN for the
|
|
* external Hanzo KV instance (RESP wire protocol).
|
|
* - KV_URL malformed ⇒ THROWS. Fail CLOSED — never silently fall back to the
|
|
* in-process backend, which would mask a misconfigured
|
|
* external KV as a "working" single-replica cache
|
|
* (silent split-brain across replicas).
|
|
*
|
|
* Brand law: the env var is `KV_URL` and its canonical scheme is the Hanzo KV
|
|
* brand scheme `kv://` (`kvs://` for TLS). RESP is the wire protocol, so we map
|
|
* the brand scheme onto `redis://`/`rediss://` — the ONE allowed "redis" token,
|
|
* confined to the wire URL. A raw `redis://`/`rediss://` DSN (what commerce's
|
|
* KMS-synced secret carries) is accepted verbatim for cross-service uniformity.
|
|
*/
|
|
|
|
/**
|
|
* resolveKvUrl maps the raw KV_URL env value to either null (⇒ in-process) or a
|
|
* validated `redis://`/`rediss://` DSN (⇒ external Hanzo KV). Throws on a
|
|
* malformed URL — the fail-closed contract.
|
|
*/
|
|
export function resolveKvUrl(raw: string | undefined | null): string | null {
|
|
const value = (raw ?? "").trim();
|
|
if (value === "") {
|
|
return null;
|
|
}
|
|
|
|
// Brand scheme kv:// / kvs:// → RESP wire scheme redis:// / rediss://.
|
|
const normalized = value.startsWith("kv://")
|
|
? "redis://" + value.slice("kv://".length)
|
|
: value.startsWith("kvs://")
|
|
? "rediss://" + value.slice("kvs://".length)
|
|
: value;
|
|
|
|
// Parse to validate. `new URL` throws on a malformed URL → propagates as the
|
|
// fail-closed error (we never swallow it into a null/in-process fallback).
|
|
let parsed: URL;
|
|
try {
|
|
parsed = new URL(normalized);
|
|
} catch (cause) {
|
|
throw new Error(
|
|
`KV_URL is malformed and cannot be parsed as a redis:// DSN (got ${redact(value)})`,
|
|
{ cause },
|
|
);
|
|
}
|
|
|
|
if (parsed.protocol !== "redis:" && parsed.protocol !== "rediss:") {
|
|
throw new Error(
|
|
`KV_URL has unsupported scheme '${parsed.protocol}' — expected kv://, kvs://, redis:// or rediss:// (got ${redact(value)})`,
|
|
);
|
|
}
|
|
|
|
if (parsed.hostname === "") {
|
|
throw new Error(
|
|
`KV_URL is missing a host — expected e.g. redis://:<password>@hanzo-kv:6379 (got ${redact(value)})`,
|
|
);
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
/**
|
|
* redact strips any userinfo (password) from a KV_URL before it appears in an
|
|
* error message or log line — a malformed DSN must never leak its credential.
|
|
*/
|
|
function redact(value: string): string {
|
|
const at = value.lastIndexOf("@");
|
|
if (at === -1) {
|
|
return value;
|
|
}
|
|
const schemeEnd = value.indexOf("://");
|
|
const schemePrefix = schemeEnd === -1 ? "" : value.slice(0, schemeEnd + 3);
|
|
return `${schemePrefix}<redacted>@${value.slice(at + 1)}`;
|
|
}
|